domemittermixin.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. /**
  7. * Mixin that injects the DOM events API into its host. It provides the API
  8. * compatible with {@link EmitterMixin}.
  9. *
  10. * listenTo( click, ... )
  11. * +-----------------------------------------+
  12. * | stopListening( ... ) |
  13. * +----------------------------+ | addEventListener( click, ... )
  14. * | Host | | +---------------------------------------------+
  15. * +----------------------------+ | | removeEventListener( click, ... ) |
  16. * | _listeningTo: { | +----------v-------------+ |
  17. * | UID: { | | ProxyEmitter | |
  18. * | emitter: ProxyEmitter, | +------------------------+ +------------v----------+
  19. * | callbacks: { | | events: { | | Node (HTMLElement) |
  20. * | click: [ callbacks ] | | click: [ callbacks ] | +-----------------------+
  21. * | } | | }, | | data-cke-expando: UID |
  22. * | } | | _domNode: Node, | +-----------------------+
  23. * | } | | _domListeners: {}, | |
  24. * | +------------------------+ | | _emitterId: UID | |
  25. * | | DOMEmitterMixin | | +--------------^---------+ |
  26. * | +------------------------+ | | | |
  27. * +--------------^-------------+ | +---------------------------------------------+
  28. * | | click (DOM Event)
  29. * +-----------------------------------------+
  30. * fire( click, DOM Event )
  31. *
  32. * @class DOMEmitterMixin
  33. * @extends EmitterMixin
  34. * @singleton
  35. */
  36. CKEDITOR.define( [ 'emittermixin', 'utils', 'log' ], function( EmitterMixin, utils, log ) {
  37. const DOMEmitterMixin = {
  38. /**
  39. * Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
  40. * It is backwards compatible with {@link EmitterMixin#listenTo}.
  41. *
  42. * @param {Emitter|Node} emitter The object that fires the event.
  43. * @param {String} event The name of the event.
  44. * @param {Function} callback The function to be called on event.
  45. * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to `emitter`.
  46. * @param {Number} [priority=10] The priority of this callback in relation to other callbacks to that same event.
  47. * Lower values are called first.
  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 ( emitter instanceof Node ) {
  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 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 {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. stopListening() {
  76. const args = Array.prototype.slice.call( arguments );
  77. const emitter = args[ 0 ];
  78. // Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
  79. if ( emitter instanceof Node ) {
  80. let proxy = this._getProxyEmitter( emitter );
  81. if ( proxy ) {
  82. args[ 0 ] = proxy;
  83. } else {
  84. log.error(
  85. 'domemittermixin-stoplistening: Stopped listening on a DOM Node that has no emitter or emitter is gone.',
  86. emitter
  87. );
  88. }
  89. }
  90. // Execute parent class method with Emitter (or ProxyEmitter) instance.
  91. EmitterMixin.stopListening.apply( this, args );
  92. },
  93. /**
  94. * Retrieves ProxyEmitter instance for given DOM Node residing in this Host.
  95. *
  96. * @param {Node} node DOM Node of the ProxyEmitter.
  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. /**
  113. * Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events
  114. * and any Host listening to them. It is backwards compatible with {@link EmitterMixin#on}.
  115. *
  116. * @class DOMEmitterMixin
  117. * @mixins EmitterMixin
  118. * @param {Node} node DOM Node that fires events.
  119. * @returns {Object} ProxyEmitter instance bound to the DOM Node.
  120. */
  121. class ProxyEmitter {
  122. constructor( node ) {
  123. // Set emitter ID to match DOM Node "expando" property.
  124. this._emitterId = getNodeUID( node );
  125. // Remember the DOM Node this ProxyEmitter is bound to.
  126. this._domNode = node;
  127. }
  128. }
  129. utils.extend( ProxyEmitter.prototype, EmitterMixin, {
  130. /**
  131. * Collection of native DOM listeners.
  132. *
  133. * @property {Object} _domListeners
  134. */
  135. /**
  136. * Registers a callback function to be executed when an event is fired.
  137. *
  138. * It attaches a native DOM listener to the DOM Node. When fired,
  139. * a corresponding Emitter event will also fire with DOM Event object as an argument.
  140. *
  141. * @param {String} event The name of the event.
  142. * @param {Function} callback The function to be called on event.
  143. * @param {Object} [ctx] The object that represents `this` in the callback. Defaults to the object firing the
  144. * event.
  145. * @param {Number} [priority=10] The priority of this callback in relation to other callbacks to that same event.
  146. * Lower values are called first.
  147. */
  148. on( event ) {
  149. // Execute parent class method first.
  150. EmitterMixin.on.apply( this, arguments );
  151. // If the DOM Listener for given event already exist it is pointless
  152. // to attach another one.
  153. if ( this._domListeners && this._domListeners[ event ] ) {
  154. return;
  155. }
  156. const domListener = this._createDomListener( event );
  157. // Attach the native DOM listener to DOM Node.
  158. this._domNode.addEventListener( event, domListener );
  159. if ( !this._domListeners ) {
  160. this._domListeners = {};
  161. }
  162. // Store the native DOM listener in this ProxyEmitter. It will be helpful
  163. // when stopping listening to the event.
  164. this._domListeners[ event ] = domListener;
  165. },
  166. /**
  167. * Stops executing the callback on the given event.
  168. *
  169. * @param {String} event The name of the event.
  170. * @param {Function} callback The function to stop being called.
  171. * @param {Object} [ctx] The context object to be removed, pared with the given callback. To handle cases where
  172. * the same callback is used several times with different contexts.
  173. */
  174. off( event ) {
  175. // Execute parent class method first.
  176. EmitterMixin.off.apply( this, arguments );
  177. let callbacks;
  178. // Remove native DOM listeners which are orphans. If no callbacks
  179. // are awaiting given event, detach native DOM listener from DOM Node.
  180. // See: {@link on}.
  181. if ( !( callbacks = this._events[ event ] ) || !callbacks.length ) {
  182. this._domListeners[ event ].removeListener();
  183. }
  184. },
  185. /**
  186. * Create a native DOM listener callback. When the native DOM event
  187. * is fired it will fire corresponding event on this ProxyEmitter.
  188. * Note: A native DOM Event is passed as an argument.
  189. *
  190. * @param {String} event
  191. * @returns {Function} The DOM listener callback.
  192. */
  193. _createDomListener( event ) {
  194. const domListener = domEvt => {
  195. this.fire( event, domEvt );
  196. };
  197. // Supply the DOM listener callback with a function that will help
  198. // detach it from the DOM Node, when it is no longer necessary.
  199. // See: {@link off}.
  200. domListener.removeListener = () => {
  201. this._domNode.removeEventListener( event, domListener );
  202. delete this._domListeners[ event ];
  203. };
  204. return domListener;
  205. }
  206. } );
  207. return DOMEmitterMixin;
  208. /**
  209. * Gets an unique DOM Node identifier. The identifier will be set if not defined.
  210. *
  211. * @param {Node} node
  212. * @return {Number} UID for given DOM Node.
  213. */
  214. function getNodeUID( node ) {
  215. return node[ 'data-ck-expando' ] || ( node[ 'data-ck-expando' ] = utils.uid() );
  216. }
  217. } );