domemittermixin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import EmitterMixin from '../utils/emittermixin.js';
  6. import uid from '../utils/uid.js';
  7. import extend from '../utils/lib/lodash/extend.js';
  8. import isNative from '../utils/lib/lodash/isNative.js';
  9. /**
  10. * Mixin that injects the DOM events API into its host. It provides the API
  11. * compatible with {@link utils.EmitterMixin}.
  12. *
  13. * DOM emitter mixin is by default available in the {@link ui.View} class,
  14. * but it can also be mixed into any other class:
  15. *
  16. * import mix from '../utils/mix.js';
  17. * import DOMEmitterMixin from '../ui/domemittermixin.js';
  18. *
  19. * class SomeView {}
  20. * mix( SomeView, DOMEmitterMixin );
  21. *
  22. * const view = new SomeView();
  23. * view.listenTo( domElement, ( evt, domEvt ) => {
  24. * console.log( evt, domEvt );
  25. * } );
  26. *
  27. * @mixin ui.DOMEmitterMixin
  28. * @mixes utils.EmitterMixin
  29. * @implements ui.DOMEmitter
  30. */
  31. const DOMEmitterMixin = extend( {}, EmitterMixin, {
  32. /**
  33. * Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
  34. * It is backwards compatible with {@link utils.EmitterMixin#listenTo}.
  35. *
  36. * @param {utils.Emitter|Node} emitter The object that fires the event.
  37. * @param {String} event The name of the event.
  38. * @param {Function} callback The function to be called on event.
  39. * @param {Object} [options={}] Additional options.
  40. * @param {utils.PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
  41. * the priority value the sooner the callback will be fired. Events having the same priority are called in the
  42. * order they were added.
  43. * @param {Object} [options.context] The object that represents `this` in the callback. Defaults to the object firing the event.
  44. * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
  45. * listener before being dispatched to any EventTarget beneath it in the DOM tree.
  46. *
  47. * @method ui.DOMEmitterMixin#listenTo
  48. */
  49. listenTo() {
  50. const args = Array.prototype.slice.call( arguments );
  51. const emitter = args[ 0 ];
  52. // Check if emitter is an instance of DOM Node. If so, replace the argument with
  53. // corresponding ProxyEmitter (or create one if not existing).
  54. if ( isDomNode( emitter ) ) {
  55. args[ 0 ] = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
  56. }
  57. // Execute parent class method with Emitter (or ProxyEmitter) instance.
  58. EmitterMixin.listenTo.apply( this, args );
  59. },
  60. /**
  61. * Stops listening for events. It can be used at different levels:
  62. * It is backwards compatible with {@link utils.EmitterMixin#listenTo}.
  63. *
  64. * * To stop listening to a specific callback.
  65. * * To stop listening to a specific event.
  66. * * To stop listening to all events fired by a specific object.
  67. * * To stop listening to all events fired by all object.
  68. *
  69. * @param {utils.Emitter|Node} [emitter] The object to stop listening to. If omitted, stops it for all objects.
  70. * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
  71. * for all events from `emitter`.
  72. * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
  73. * `event`.
  74. *
  75. * @method ui.DOMEmitterMixin#stopListening
  76. */
  77. stopListening() {
  78. const args = Array.prototype.slice.call( arguments );
  79. const emitter = args[ 0 ];
  80. // Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
  81. if ( isDomNode( emitter ) ) {
  82. let proxy = this._getProxyEmitter( emitter );
  83. // Element has no listeners.
  84. if ( !proxy ) {
  85. return;
  86. }
  87. args[ 0 ] = proxy;
  88. }
  89. // Execute parent class method with Emitter (or ProxyEmitter) instance.
  90. EmitterMixin.stopListening.apply( this, args );
  91. },
  92. /**
  93. * Retrieves ProxyEmitter instance for given DOM Node residing in this Host.
  94. *
  95. * @param {Node} node DOM Node of the ProxyEmitter.
  96. * @method ui.DOMEmitterMixin#_getProxyEmitter
  97. * @return {ProxyEmitter} ProxyEmitter instance or null.
  98. */
  99. _getProxyEmitter( node ) {
  100. let proxy, emitters, emitterInfo;
  101. // Get node UID. It allows finding Proxy Emitter for this DOM Node.
  102. const uid = getNodeUID( node );
  103. // Find existing Proxy Emitter for this DOM Node among emitters.
  104. if ( ( emitters = this._listeningTo ) ) {
  105. if ( ( emitterInfo = emitters[ uid ] ) ) {
  106. proxy = emitterInfo.emitter;
  107. }
  108. }
  109. return proxy || null;
  110. }
  111. } );
  112. export default DOMEmitterMixin;
  113. /**
  114. * Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events
  115. * and any Host listening to them. It is backwards compatible with {@link utils.EmitterMixin#on}.
  116. *
  117. * listenTo( click, ... )
  118. * +-----------------------------------------+
  119. * | stopListening( ... ) |
  120. * +----------------------------+ | addEventListener( click, ... )
  121. * | Host | | +---------------------------------------------+
  122. * +----------------------------+ | | removeEventListener( click, ... ) |
  123. * | _listeningTo: { | +----------v-------------+ |
  124. * | UID: { | | ProxyEmitter | |
  125. * | emitter: ProxyEmitter, | +------------------------+ +------------v----------+
  126. * | callbacks: { | | events: { | | Node (HTMLElement) |
  127. * | click: [ callbacks ] | | click: [ callbacks ] | +-----------------------+
  128. * | } | | }, | | data-cke-expando: UID |
  129. * | } | | _domNode: Node, | +-----------------------+
  130. * | } | | _domListeners: {}, | |
  131. * | +------------------------+ | | _emitterId: UID | |
  132. * | | DOMEmitterMixin | | +--------------^---------+ |
  133. * | +------------------------+ | | | |
  134. * +--------------^-------------+ | +---------------------------------------------+
  135. * | | click (DOM Event)
  136. * +-----------------------------------------+
  137. * fire( click, DOM Event )
  138. *
  139. * @memberOf ui
  140. * @mixes utils.EmitterMixin
  141. * @implements ui.DOMEmitter
  142. */
  143. class ProxyEmitter {
  144. /**
  145. * @param {Node} node DOM Node that fires events.
  146. * @returns {Object} ProxyEmitter instance bound to the DOM Node.
  147. */
  148. constructor( node ) {
  149. // Set emitter ID to match DOM Node "expando" property.
  150. this._emitterId = getNodeUID( node );
  151. // Remember the DOM Node this ProxyEmitter is bound to.
  152. this._domNode = node;
  153. }
  154. }
  155. extend( ProxyEmitter.prototype, EmitterMixin, {
  156. /**
  157. * Collection of native DOM listeners.
  158. *
  159. * @private
  160. * @member {Object} ui.ProxyEmitter#_domListeners
  161. */
  162. /**
  163. * Registers a callback function to be executed when an event is fired.
  164. *
  165. * It attaches a native DOM listener to the DOM Node. When fired,
  166. * a corresponding Emitter event will also fire with DOM Event object as an argument.
  167. *
  168. * @param {String} event The name of the event.
  169. * @param {Function} callback The function to be called on event.
  170. * @param {Object} [options={}] Additional options.
  171. * @param {utils.PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
  172. * the priority value the sooner the callback will be fired. Events having the same priority are called in the
  173. * order they were added.
  174. * @param {Object} [options.context] The object that represents `this` in the callback. Defaults to the object firing the event.
  175. * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
  176. * listener before being dispatched to any EventTarget beneath it in the DOM tree.
  177. *
  178. * @method ui.ProxyEmitter#on
  179. */
  180. on( event, callback, options = {} ) {
  181. // Execute parent class method first.
  182. EmitterMixin.on.apply( this, arguments );
  183. // If the DOM Listener for given event already exist it is pointless
  184. // to attach another one.
  185. if ( this._domListeners && this._domListeners[ event ] ) {
  186. return;
  187. }
  188. const domListener = this._createDomListener( event );
  189. // Attach the native DOM listener to DOM Node.
  190. this._domNode.addEventListener( event, domListener, !!options.useCapture );
  191. if ( !this._domListeners ) {
  192. this._domListeners = {};
  193. }
  194. // Store the native DOM listener in this ProxyEmitter. It will be helpful
  195. // when stopping listening to the event.
  196. this._domListeners[ event ] = domListener;
  197. },
  198. /**
  199. * Stops executing the callback on the given event.
  200. *
  201. * @param {String} event The name of the event.
  202. * @param {Function} callback The function to stop being called.
  203. * @param {Object} [context] The context object to be removed, pared with the given callback. To handle cases where
  204. * the same callback is used several times with different contexts.
  205. *
  206. * @method ui.ProxyEmitter#off
  207. */
  208. off( event ) {
  209. // Execute parent class method first.
  210. EmitterMixin.off.apply( this, arguments );
  211. let events;
  212. // Remove native DOM listeners which are orphans. If no callbacks
  213. // are awaiting given event, detach native DOM listener from DOM Node.
  214. // See: {@link on}.
  215. if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
  216. this._domListeners[ event ].removeListener();
  217. }
  218. },
  219. /**
  220. * Create a native DOM listener callback. When the native DOM event
  221. * is fired it will fire corresponding event on this ProxyEmitter.
  222. * Note: A native DOM Event is passed as an argument.
  223. *
  224. * @param {String} event
  225. *
  226. * @method ui.ProxyEmitter#_createDomListener
  227. * @returns {Function} The DOM listener callback.
  228. */
  229. _createDomListener( event ) {
  230. const domListener = domEvt => {
  231. this.fire( event, domEvt );
  232. };
  233. // Supply the DOM listener callback with a function that will help
  234. // detach it from the DOM Node, when it is no longer necessary.
  235. // See: {@link off}.
  236. domListener.removeListener = () => {
  237. this._domNode.removeEventListener( event, domListener );
  238. delete this._domListeners[ event ];
  239. };
  240. return domListener;
  241. }
  242. } );
  243. // Gets an unique DOM Node identifier. The identifier will be set if not defined.
  244. //
  245. // @private
  246. // @param {Node} node
  247. // @return {Number} UID for given DOM Node.
  248. function getNodeUID( node ) {
  249. return node[ 'data-ck-expando' ] || ( node[ 'data-ck-expando' ] = uid() );
  250. }
  251. // Checks (naively) if given node is native DOM Node.
  252. //
  253. // @private
  254. // @param {Node} node
  255. // @return {Boolean} True when native DOM Node.
  256. function isDomNode( node ) {
  257. return node && isNative( node.addEventListener );
  258. }
  259. /**
  260. * Interface representing classes which mix in {@link ui.DOMEmitter}.
  261. *
  262. * @interface ui.DOMEmitter
  263. */