emittermixin.js 11 KB

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