domemittermixin.js 10 KB

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