emittermixin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module utils/emittermixin
  7. */
  8. import EventInfo from './eventinfo';
  9. import uid from './uid';
  10. import priorities from './priorities';
  11. const _listeningTo = Symbol( 'listeningTo' );
  12. const _emitterId = Symbol( 'emitterId' );
  13. /**
  14. * Mixin that injects the events API into its host.
  15. *
  16. * @mixin EmitterMixin
  17. * @implements module:utils/emittermixin~Emitter
  18. */
  19. const EmitterMixin = {
  20. /**
  21. * Registers a callback function to be executed when an event is fired. Events can be grouped in namespaces using `:`.
  22. * When namespaced event is fired, it additionaly fires all callbacks for that namespace.
  23. *
  24. * myEmitter.on( 'myGroup', genericCallback );
  25. * myEmitter.on( 'myGroup:myEvent', specificCallback );
  26. * myEmitter.fire( 'myGroup' ); // genericCallback is fired.
  27. * myEmitter.fire( 'myGroup:myEvent' ); // both genericCallback and specificCallback are fired.
  28. * myEmitter.fire( 'myGroup:foo' ); // genericCallback is fired even though there are no callbacks for "foo".
  29. *
  30. * @method #on
  31. * @param {String} event The name of the event.
  32. * @param {Function} callback The function to be called on event.
  33. * @param {Object} [options={}] Additional options.
  34. * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
  35. * the priority value the sooner the callback will be fired. Events having the same priority are called in the
  36. * order they were added.
  37. * @param {Object} [options.context] The object that represents `this` in the callback. Defaults to the object firing the event.
  38. */
  39. on( event, callback, options = {} ) {
  40. createEventNamespace( this, event );
  41. const lists = getCallbacksListsForNamespace( this, event );
  42. const priority = priorities.get( options.priority );
  43. callback = {
  44. callback: callback,
  45. context: options.context || this,
  46. priority: priority
  47. };
  48. // Add the callback to all callbacks list.
  49. for ( let callbacks of lists ) {
  50. // Add the callback to the list in the right priority position.
  51. let added = false;
  52. for ( let i = 0; i < callbacks.length; i++ ) {
  53. if ( callbacks[ i ].priority < priority ) {
  54. callbacks.splice( i, 0, callback );
  55. added = true;
  56. break;
  57. }
  58. }
  59. // Add at the end, if right place was not found.
  60. if ( !added ) {
  61. callbacks.push( callback );
  62. }
  63. }
  64. },
  65. /**
  66. * Registers a callback function to be executed on the next time the event is fired only. This is similar to
  67. * calling {@link #on} followed by {@link #off} in the callback.
  68. *
  69. * @method #once
  70. * @param {String} event The name of the event.
  71. * @param {Function} callback The function to be called on event.
  72. * @param {Object} [options={}] Additional options.
  73. * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
  74. * the priority value the sooner the callback will be fired. Events having the same priority are called in the
  75. * order they were added.
  76. * @param {Object} [options.context] The object that represents `this` in the callback. Defaults to the object firing the event.
  77. */
  78. once( event, callback, options ) {
  79. const onceCallback = function( event ) {
  80. // Go off() at the first call.
  81. event.off();
  82. // Go with the original callback.
  83. callback.apply( this, arguments );
  84. };
  85. // Make a similar on() call, simply replacing the callback.
  86. this.on( event, onceCallback, options );
  87. },
  88. /**
  89. * Stops executing the callback on the given event.
  90. *
  91. * @method #off
  92. * @param {String} event The name of the event.
  93. * @param {Function} callback The function to stop being called.
  94. * @param {Object} [context] The context object to be removed, pared with the given callback. To handle cases where
  95. * the same callback is used several times with different contexts.
  96. */
  97. off( event, callback, context ) {
  98. const lists = getCallbacksListsForNamespace( this, event );
  99. for ( let callbacks of lists ) {
  100. for ( let i = 0; i < callbacks.length; i++ ) {
  101. if ( callbacks[ i ].callback == callback ) {
  102. if ( !context || context == callbacks[ i ].context ) {
  103. // Remove the callback from the list (fixing the next index).
  104. callbacks.splice( i, 1 );
  105. i--;
  106. }
  107. }
  108. }
  109. }
  110. },
  111. /**
  112. * Registers a callback function to be executed when an event is fired in a specific (emitter) object.
  113. *
  114. * @method #listenTo
  115. * @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
  116. * @param {String} event The name of the event.
  117. * @param {Function} callback The function to be called on event.
  118. * @param {Object} [options={}] Additional options.
  119. * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
  120. * the priority value the sooner the callback will be fired. Events having the same priority are called in the
  121. * order they were added.
  122. * @param {Object} [options.context] The object that represents `this` in the callback. Defaults to the object firing the event.
  123. */
  124. listenTo( emitter, event, callback, options ) {
  125. let emitters, emitterId, emitterInfo, eventCallbacks;
  126. // _listeningTo contains a list of emitters that this object is listening to.
  127. // This list has the following format:
  128. //
  129. // _listeningTo: {
  130. // emitterId: {
  131. // emitter: emitter,
  132. // callbacks: {
  133. // event1: [ callback1, callback2, ... ]
  134. // ....
  135. // }
  136. // },
  137. // ...
  138. // }
  139. if ( !this[ _listeningTo ] ) {
  140. this[ _listeningTo ] = {};
  141. }
  142. emitters = this[ _listeningTo ];
  143. if ( !_getEmitterId( emitter ) ) {
  144. _setEmitterId( emitter );
  145. }
  146. emitterId = _getEmitterId( emitter );
  147. if ( !( emitterInfo = emitters[ emitterId ] ) ) {
  148. emitterInfo = emitters[ emitterId ] = {
  149. emitter: emitter,
  150. callbacks: {}
  151. };
  152. }
  153. if ( !( eventCallbacks = emitterInfo.callbacks[ event ] ) ) {
  154. eventCallbacks = emitterInfo.callbacks[ event ] = [];
  155. }
  156. eventCallbacks.push( callback );
  157. // Finally register the callback to the event.
  158. emitter.on( event, callback, options );
  159. },
  160. /**
  161. * Stops listening for events. It can be used at different levels:
  162. *
  163. * * To stop listening to a specific callback.
  164. * * To stop listening to a specific event.
  165. * * To stop listening to all events fired by a specific object.
  166. * * To stop listening to all events fired by all object.
  167. *
  168. * @method #stopListening
  169. * @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
  170. * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
  171. * for all events from `emitter`.
  172. * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
  173. * `event`.
  174. */
  175. stopListening( emitter, event, callback ) {
  176. let emitters = this[ _listeningTo ];
  177. let emitterId = emitter && _getEmitterId( emitter );
  178. let emitterInfo = emitters && emitterId && emitters[ emitterId ];
  179. let eventCallbacks = emitterInfo && event && emitterInfo.callbacks[ event ];
  180. // Stop if nothing has been listened.
  181. if ( !emitters || ( emitter && !emitterInfo ) || ( event && !eventCallbacks ) ) {
  182. return;
  183. }
  184. // All params provided. off() that single callback.
  185. if ( callback ) {
  186. emitter.off( event, callback );
  187. }
  188. // Only `emitter` and `event` provided. off() all callbacks for that event.
  189. else if ( eventCallbacks ) {
  190. while ( ( callback = eventCallbacks.pop() ) ) {
  191. emitter.off( event, callback );
  192. }
  193. delete emitterInfo.callbacks[ event ];
  194. }
  195. // Only `emitter` provided. off() all events for that emitter.
  196. else if ( emitterInfo ) {
  197. for ( event in emitterInfo.callbacks ) {
  198. this.stopListening( emitter, event );
  199. }
  200. delete emitters[ emitterId ];
  201. }
  202. // No params provided. off() all emitters.
  203. else {
  204. for ( emitterId in emitters ) {
  205. this.stopListening( emitters[ emitterId ].emitter );
  206. }
  207. delete this[ _listeningTo ];
  208. }
  209. },
  210. /**
  211. * Fires an event, executing all callbacks registered for it.
  212. *
  213. * The first parameter passed to callbacks is an {@link module:utils/eventinfo~EventInfo} object,
  214. * followed by the optional `args` provided in the `fire()` method call.
  215. *
  216. * @method #fire
  217. * @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated.
  218. * @param {...*} [args] Additional arguments to be passed to the callbacks.
  219. */
  220. fire( eventOrInfo, ...args ) {
  221. const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo( this, eventOrInfo );
  222. const event = eventInfo.name;
  223. let callbacks = getCallbacksForEvent( this, event );
  224. // Record that the event passed this emitter on its path.
  225. eventInfo.path.push( this );
  226. // Handle event listener callbacks first.
  227. if ( callbacks ) {
  228. // Arguments passed to each callback.
  229. const callbackArgs = [ eventInfo, ...args ];
  230. // Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks
  231. // are added while processing other callbacks. Previous solution involved adding counters (unique ids) but
  232. // failed if callbacks were added to the queue before currently processed callback.
  233. // If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same
  234. // event is currently processed. Then, `.fire()` at the end, would have to add all stored events.
  235. callbacks = Array.from( callbacks );
  236. for ( let i = 0; i < callbacks.length; i++ ) {
  237. callbacks[ i ].callback.apply( callbacks[ i ].context, callbackArgs );
  238. // Remove the callback from future requests if off() has been called.
  239. if ( eventInfo.off.called ) {
  240. // Remove the called mark for the next calls.
  241. delete eventInfo.off.called;
  242. this.off( event, callbacks[ i ].callback, callbacks[ i ].context );
  243. }
  244. // Do not execute next callbacks if stop() was called.
  245. if ( eventInfo.stop.called ) {
  246. break;
  247. }
  248. }
  249. }
  250. // Delegate event to other emitters if needed.
  251. if ( this._delegations ) {
  252. const destinations = this._delegations.get( event );
  253. const passAllDestinations = this._delegations.get( '*' );
  254. if ( destinations ) {
  255. fireDelegatedEvents( destinations, eventInfo, args );
  256. }
  257. if ( passAllDestinations ) {
  258. fireDelegatedEvents( passAllDestinations, eventInfo, args );
  259. }
  260. }
  261. },
  262. /**
  263. * Delegates selected events to another {@link module:utils/emittermixin~Emitter}. For instance:
  264. *
  265. * emitterA.delegate( 'eventX' ).to( emitterB );
  266. * emitterA.delegate( 'eventX', 'eventY' ).to( emitterC );
  267. *
  268. * then `eventX` is delegated (fired by) `emitterB` and `emitterC` along with `data`:
  269. *
  270. * emitterA.fire( 'eventX', data );
  271. *
  272. * and `eventY` is delegated (fired by) `emitterC` along with `data`:
  273. *
  274. * emitterA.fire( 'eventY', data );
  275. *
  276. * @method #delegate
  277. * @param {...String} events Event names that will be delegated to another emitter.
  278. * @returns {module:utils/emittermixin~EmitterMixinDelegateChain}
  279. */
  280. delegate( ...events ) {
  281. return {
  282. to: ( emitter, nameOrFunction ) => {
  283. if ( !this._delegations ) {
  284. this._delegations = new Map();
  285. }
  286. for ( let eventName of events ) {
  287. let destinations = this._delegations.get( eventName );
  288. if ( !destinations ) {
  289. this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) );
  290. } else {
  291. destinations.set( emitter, nameOrFunction );
  292. }
  293. }
  294. }
  295. };
  296. },
  297. /**
  298. * Stops delegating events. It can be used at different levels:
  299. *
  300. * * To stop delegating all events.
  301. * * To stop delegating a specific event to all emitters.
  302. * * To stop delegating a specific event to a specific emitter.
  303. *
  304. * @method #stopDelegating
  305. * @param {String} [event] The name of the event to stop delegating. If omitted, stops it all delegations.
  306. * @param {module:utils/emittermixin~Emitter} [emitter] (requires `event`) The object to stop delegating a particular event to.
  307. * If omitted, stops delegation of `event` to all emitters.
  308. */
  309. stopDelegating( event, emitter ) {
  310. if ( !this._delegations ) {
  311. return;
  312. }
  313. if ( !event ) {
  314. this._delegations.clear();
  315. } else if ( !emitter ) {
  316. this._delegations.delete( event );
  317. } else {
  318. const destinations = this._delegations.get( event );
  319. if ( destinations ) {
  320. destinations.delete( emitter );
  321. }
  322. }
  323. }
  324. };
  325. export default EmitterMixin;
  326. /**
  327. * Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter.
  328. * If not, returns `null`.
  329. *
  330. * @protected
  331. * @param {module:utils/emittermixin~EmitterMixin} listeningEmitter Emitter that listens.
  332. * @param {String} listenedToEmitterId Unique emitter id of emitter listened to.
  333. * @returns {module:utils/emittermixin~EmitterMixin|null}
  334. */
  335. export function _getEmitterListenedTo( listeningEmitter, listenedToEmitterId ) {
  336. if ( listeningEmitter[ _listeningTo ] && listeningEmitter[ _listeningTo ][ listenedToEmitterId ] ) {
  337. return listeningEmitter[ _listeningTo ][ listenedToEmitterId ].emitter;
  338. }
  339. return null;
  340. }
  341. /**
  342. * Sets emitter's unique id.
  343. *
  344. * **Note:** `_emitterId` can be set only once.
  345. *
  346. * @protected
  347. * @param {module:utils/emittermixin~EmitterMixin} emitter Emitter for which id will be set.
  348. * @param {String} [id] Unique id to set. If not passed, random unique id will be set.
  349. */
  350. export function _setEmitterId( emitter, id ) {
  351. if ( !emitter[ _emitterId ] ) {
  352. emitter[ _emitterId ] = id || uid();
  353. }
  354. }
  355. /**
  356. * Returns emitter's unique id.
  357. *
  358. * @protected
  359. * @param {module:utils/emittermixin~EmitterMixin} emitter Emitter which id will be returned.
  360. */
  361. export function _getEmitterId( emitter ) {
  362. return emitter[ _emitterId ];
  363. }
  364. // Gets the internal `_events` property of the given object.
  365. // `_events` property store all lists with callbacks for registered event names.
  366. // If there were no events registered on the object, empty `_events` object is created.
  367. function getEvents( source ) {
  368. if ( !source._events ) {
  369. Object.defineProperty( source, '_events', {
  370. value: {}
  371. } );
  372. }
  373. return source._events;
  374. }
  375. // Creates event node for generic-specific events relation architecture.
  376. function makeEventNode() {
  377. return {
  378. callbacks: [],
  379. childEvents: []
  380. };
  381. }
  382. // Creates an architecture for generic-specific events relation.
  383. // If needed, creates all events for given eventName, i.e. if the first registered event
  384. // is foo:bar:abc, it will create foo:bar:abc, foo:bar and foo event and tie them together.
  385. // It also copies callbacks from more generic events to more specific events when
  386. // specific events are created.
  387. function createEventNamespace( source, eventName ) {
  388. const events = getEvents( source );
  389. // First, check if the event we want to add to the structure already exists.
  390. if ( events[ eventName ] ) {
  391. // If it exists, we don't have to do anything.
  392. return;
  393. }
  394. // In other case, we have to create the structure for the event.
  395. // Note, that we might need to create intermediate events too.
  396. // I.e. if foo:bar:abc is being registered and we only have foo in the structure,
  397. // we need to also register foo:bar.
  398. // Currently processed event name.
  399. let name = eventName;
  400. // Name of the event that is a child event for currently processed event.
  401. let childEventName = null;
  402. // Array containing all newly created specific events.
  403. const newEventNodes = [];
  404. // While loop can't check for ':' index because we have to handle generic events too.
  405. // In each loop, we truncate event name, going from the most specific name to the generic one.
  406. // I.e. foo:bar:abc -> foo:bar -> foo.
  407. while ( name !== '' ) {
  408. if ( events[ name ] ) {
  409. // If the currently processed event name is already registered, we can be sure
  410. // that it already has all the structure created, so we can break the loop here
  411. // as no more events need to be registered.
  412. break;
  413. }
  414. // If this event is not yet registered, create a new object for it.
  415. events[ name ] = makeEventNode();
  416. // Add it to the array with newly created events.
  417. newEventNodes.push( events[ name ] );
  418. // Add previously processed event name as a child of this event.
  419. if ( childEventName ) {
  420. events[ name ].childEvents.push( childEventName );
  421. }
  422. childEventName = name;
  423. // If `.lastIndexOf()` returns -1, `.substr()` will return '' which will break the loop.
  424. name = name.substr( 0, name.lastIndexOf( ':' ) );
  425. }
  426. if ( name !== '' ) {
  427. // If name is not empty, we found an already registered event that was a parent of the
  428. // event we wanted to register.
  429. // Copy that event's callbacks to newly registered events.
  430. for ( let node of newEventNodes ) {
  431. node.callbacks = events[ name ].callbacks.slice();
  432. }
  433. // Add last newly created event to the already registered event.
  434. events[ name ].childEvents.push( childEventName );
  435. }
  436. }
  437. // Gets an array containing callbacks list for a given event and it's more specific events.
  438. // I.e. if given event is foo:bar and there is also foo:bar:abc event registered, this will
  439. // return callback list of foo:bar and foo:bar:abc (but not foo).
  440. // Returns empty array if given event has not been yet registered.
  441. function getCallbacksListsForNamespace( source, eventName ) {
  442. const eventNode = getEvents( source )[ eventName ];
  443. if ( !eventNode ) {
  444. return [];
  445. }
  446. let callbacksLists = [ eventNode.callbacks ];
  447. for ( let i = 0; i < eventNode.childEvents.length; i++ ) {
  448. let childCallbacksLists = getCallbacksListsForNamespace( source, eventNode.childEvents[ i ] );
  449. callbacksLists = callbacksLists.concat( childCallbacksLists );
  450. }
  451. return callbacksLists;
  452. }
  453. // Get the list of callbacks for a given event, but only if there any callbacks have been registered.
  454. // If there are no callbacks registered for given event, it checks if this is a specific event and looks
  455. // for callbacks for it's more generic version.
  456. function getCallbacksForEvent( source, eventName ) {
  457. let event;
  458. if ( !source._events || !( event = source._events[ eventName ] ) || !event.callbacks.length ) {
  459. // There are no callbacks registered for specified eventName.
  460. // But this could be a specific-type event that is in a namespace.
  461. if ( eventName.indexOf( ':' ) > -1 ) {
  462. // If the eventName is specific, try to find callback lists for more generic event.
  463. return getCallbacksForEvent( source, eventName.substr( 0, eventName.lastIndexOf( ':' ) ) );
  464. } else {
  465. // If this is a top-level generic event, return null;
  466. return null;
  467. }
  468. }
  469. return event.callbacks;
  470. }
  471. // Fires delegated events for given map of destinations.
  472. //
  473. // @private
  474. // * @param {Map.<utils.Emitter>} destinations A map containing `[ {@link utils.Emitter}, "event name" ]` pair destinations.
  475. // * @param {utils.EventInfo} eventInfo The original event info object.
  476. // * @param {Array.<*>} fireArgs Arguments the original event was fired with.
  477. function fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
  478. for ( let [ emitter, name ] of destinations ) {
  479. if ( !name ) {
  480. name = eventInfo.name;
  481. } else if ( typeof name == 'function' ) {
  482. name = name( eventInfo.name );
  483. }
  484. const delegatedInfo = new EventInfo( eventInfo.source, name );
  485. delegatedInfo.path = [ ...eventInfo.path ];
  486. emitter.fire( delegatedInfo, ...fireArgs );
  487. }
  488. }
  489. /**
  490. * Interface representing classes which mix in {@link module:utils/emittermixin~EmitterMixin}.
  491. *
  492. * @interface Emitter
  493. */
  494. /**
  495. * The return value of {@link ~EmitterMixin#delegate}.
  496. *
  497. * @interface module:utils/emittermixin~EmitterMixinDelegateChain
  498. */
  499. /**
  500. * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
  501. *
  502. * @method #to
  503. * @param {module:utils/emittermixin~Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events.
  504. * @param {String|Function} nameOrFunction A custom event name or function which converts the original name string.
  505. */