| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652 |
- /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module utils/emittermixin
- */
- import EventInfo from './eventinfo';
- import uid from './uid';
- import priorities from './priorities';
- // To check if component is loaded more than once.
- import './version';
- const _listeningTo = Symbol( 'listeningTo' );
- const _emitterId = Symbol( 'emitterId' );
- /**
- * Mixin that injects the {@link ~Emitter events API} into its host.
- *
- * @mixin EmitterMixin
- * @implements module:utils/emittermixin~Emitter
- */
- const EmitterMixin = {
- /**
- * @inheritDoc
- */
- on( event, callback, options = {} ) {
- this.listenTo( this, event, callback, options );
- },
- /**
- * @inheritDoc
- */
- once( event, callback, options ) {
- let wasFired = false;
- const onceCallback = function( event, ...args ) {
- // Ensure the callback is called only once even if the callback itself leads to re-firing the event
- // (which would call the callback again).
- if ( !wasFired ) {
- wasFired = true;
- // Go off() at the first call.
- event.off();
- // Go with the original callback.
- callback.call( this, event, ...args );
- }
- };
- // Make a similar on() call, simply replacing the callback.
- this.listenTo( this, event, onceCallback, options );
- },
- /**
- * @inheritDoc
- */
- off( event, callback ) {
- this.stopListening( this, event, callback );
- },
- /**
- * @inheritDoc
- */
- listenTo( emitter, event, callback, options = {} ) {
- let emitterInfo, eventCallbacks;
- // _listeningTo contains a list of emitters that this object is listening to.
- // This list has the following format:
- //
- // _listeningTo: {
- // emitterId: {
- // emitter: emitter,
- // callbacks: {
- // event1: [ callback1, callback2, ... ]
- // ....
- // }
- // },
- // ...
- // }
- if ( !this[ _listeningTo ] ) {
- this[ _listeningTo ] = {};
- }
- const emitters = this[ _listeningTo ];
- if ( !_getEmitterId( emitter ) ) {
- _setEmitterId( emitter );
- }
- const emitterId = _getEmitterId( emitter );
- if ( !( emitterInfo = emitters[ emitterId ] ) ) {
- emitterInfo = emitters[ emitterId ] = {
- emitter,
- callbacks: {}
- };
- }
- if ( !( eventCallbacks = emitterInfo.callbacks[ event ] ) ) {
- eventCallbacks = emitterInfo.callbacks[ event ] = [];
- }
- eventCallbacks.push( callback );
- // Finally register the callback to the event.
- createEventNamespace( emitter, event );
- const lists = getCallbacksListsForNamespace( emitter, event );
- const priority = priorities.get( options.priority );
- const callbackDefinition = {
- callback,
- priority
- };
- // Add the callback to all callbacks list.
- for ( const callbacks of lists ) {
- // Add the callback to the list in the right priority position.
- let added = false;
- for ( let i = 0; i < callbacks.length; i++ ) {
- if ( callbacks[ i ].priority < priority ) {
- callbacks.splice( i, 0, callbackDefinition );
- added = true;
- break;
- }
- }
- // Add at the end, if right place was not found.
- if ( !added ) {
- callbacks.push( callbackDefinition );
- }
- }
- },
- /**
- * @inheritDoc
- */
- stopListening( emitter, event, callback ) {
- const emitters = this[ _listeningTo ];
- let emitterId = emitter && _getEmitterId( emitter );
- const emitterInfo = emitters && emitterId && emitters[ emitterId ];
- const eventCallbacks = emitterInfo && event && emitterInfo.callbacks[ event ];
- // Stop if nothing has been listened.
- if ( !emitters || ( emitter && !emitterInfo ) || ( event && !eventCallbacks ) ) {
- return;
- }
- // All params provided. off() that single callback.
- if ( callback ) {
- removeCallback( emitter, event, callback );
- }
- // Only `emitter` and `event` provided. off() all callbacks for that event.
- else if ( eventCallbacks ) {
- while ( ( callback = eventCallbacks.pop() ) ) {
- removeCallback( emitter, event, callback );
- }
- delete emitterInfo.callbacks[ event ];
- }
- // Only `emitter` provided. off() all events for that emitter.
- else if ( emitterInfo ) {
- for ( event in emitterInfo.callbacks ) {
- this.stopListening( emitter, event );
- }
- delete emitters[ emitterId ];
- }
- // No params provided. off() all emitters.
- else {
- for ( emitterId in emitters ) {
- this.stopListening( emitters[ emitterId ].emitter );
- }
- delete this[ _listeningTo ];
- }
- },
- /**
- * @inheritDoc
- */
- fire( eventOrInfo, ...args ) {
- const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo( this, eventOrInfo );
- const event = eventInfo.name;
- let callbacks = getCallbacksForEvent( this, event );
- // Record that the event passed this emitter on its path.
- eventInfo.path.push( this );
- // Handle event listener callbacks first.
- if ( callbacks ) {
- // Arguments passed to each callback.
- const callbackArgs = [ eventInfo, ...args ];
- // Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks
- // are added while processing other callbacks. Previous solution involved adding counters (unique ids) but
- // failed if callbacks were added to the queue before currently processed callback.
- // If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same
- // event is currently processed. Then, `.fire()` at the end, would have to add all stored events.
- callbacks = Array.from( callbacks );
- for ( let i = 0; i < callbacks.length; i++ ) {
- callbacks[ i ].callback.apply( this, callbackArgs );
- // Remove the callback from future requests if off() has been called.
- if ( eventInfo.off.called ) {
- // Remove the called mark for the next calls.
- delete eventInfo.off.called;
- removeCallback( this, event, callbacks[ i ].callback );
- }
- // Do not execute next callbacks if stop() was called.
- if ( eventInfo.stop.called ) {
- break;
- }
- }
- }
- // Delegate event to other emitters if needed.
- if ( this._delegations ) {
- const destinations = this._delegations.get( event );
- const passAllDestinations = this._delegations.get( '*' );
- if ( destinations ) {
- fireDelegatedEvents( destinations, eventInfo, args );
- }
- if ( passAllDestinations ) {
- fireDelegatedEvents( passAllDestinations, eventInfo, args );
- }
- }
- return eventInfo.return;
- },
- /**
- * @inheritDoc
- */
- delegate( ...events ) {
- return {
- to: ( emitter, nameOrFunction ) => {
- if ( !this._delegations ) {
- this._delegations = new Map();
- }
- // Originally there was a for..of loop which unfortunately caused an error in Babel that didn't allow
- // build an application. See: https://github.com/ckeditor/ckeditor5-react/issues/40.
- events.forEach( eventName => {
- const destinations = this._delegations.get( eventName );
- if ( !destinations ) {
- this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) );
- } else {
- destinations.set( emitter, nameOrFunction );
- }
- } );
- }
- };
- },
- /**
- * @inheritDoc
- */
- stopDelegating( event, emitter ) {
- if ( !this._delegations ) {
- return;
- }
- if ( !event ) {
- this._delegations.clear();
- } else if ( !emitter ) {
- this._delegations.delete( event );
- } else {
- const destinations = this._delegations.get( event );
- if ( destinations ) {
- destinations.delete( emitter );
- }
- }
- }
- };
- export default EmitterMixin;
- /**
- * Emitter/listener interface.
- *
- * Can be easily implemented by a class by mixing the {@link module:utils/emittermixin~EmitterMixin} mixin.
- *
- * @interface Emitter
- */
- /**
- * Registers a callback function to be executed when an event is fired.
- *
- * Shorthand for {@link #listenTo `this.listenTo( this, event, callback, options )`} (it makes the emitter
- * listen on itself).
- *
- * @method #on
- * @param {String} event The name of the event.
- * @param {Function} callback The function to be called on event.
- * @param {Object} [options={}] Additional options.
- * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
- * the priority value the sooner the callback will be fired. Events having the same priority are called in the
- * order they were added.
- */
- /**
- * Registers a callback function to be executed on the next time the event is fired only. This is similar to
- * calling {@link #on} followed by {@link #off} in the callback.
- *
- * @method #once
- * @param {String} event The name of the event.
- * @param {Function} callback The function to be called on event.
- * @param {Object} [options={}] Additional options.
- * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
- * the priority value the sooner the callback will be fired. Events having the same priority are called in the
- * order they were added.
- */
- /**
- * Stops executing the callback on the given event.
- * Shorthand for {@link #stopListening `this.stopListening( this, event, callback )`}.
- *
- * @method #off
- * @param {String} event The name of the event.
- * @param {Function} callback The function to stop being called.
- */
- /**
- * Registers a callback function to be executed when an event is fired in a specific (emitter) object.
- *
- * Events can be grouped in namespaces using `:`.
- * When namespaced event is fired, it additionally fires all callbacks for that namespace.
- *
- * // myEmitter.on( ... ) is a shorthand for myEmitter.listenTo( myEmitter, ... ).
- * myEmitter.on( 'myGroup', genericCallback );
- * myEmitter.on( 'myGroup:myEvent', specificCallback );
- *
- * // genericCallback is fired.
- * myEmitter.fire( 'myGroup' );
- * // both genericCallback and specificCallback are fired.
- * myEmitter.fire( 'myGroup:myEvent' );
- * // genericCallback is fired even though there are no callbacks for "foo".
- * myEmitter.fire( 'myGroup:foo' );
- *
- * An event callback can {@link module:utils/eventinfo~EventInfo#stop stop the event} and
- * set the {@link module:utils/eventinfo~EventInfo#return return value} of the {@link #fire} method.
- *
- * @method #listenTo
- * @param {module:utils/emittermixin~Emitter} emitter The object that fires the event.
- * @param {String} event The name of the event.
- * @param {Function} callback The function to be called on event.
- * @param {Object} [options={}] Additional options.
- * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
- * the priority value the sooner the callback will be fired. Events having the same priority are called in the
- * order they were added.
- */
- /**
- * Stops listening for events. It can be used at different levels:
- *
- * * To stop listening to a specific callback.
- * * To stop listening to a specific event.
- * * To stop listening to all events fired by a specific object.
- * * To stop listening to all events fired by all objects.
- *
- * @method #stopListening
- * @param {module:utils/emittermixin~Emitter} [emitter] The object to stop listening to. If omitted, stops it for all objects.
- * @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
- * for all events from `emitter`.
- * @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
- * `event`.
- */
- /**
- * Fires an event, executing all callbacks registered for it.
- *
- * The first parameter passed to callbacks is an {@link module:utils/eventinfo~EventInfo} object,
- * followed by the optional `args` provided in the `fire()` method call.
- *
- * @method #fire
- * @param {String|module:utils/eventinfo~EventInfo} eventOrInfo The name of the event or `EventInfo` object if event is delegated.
- * @param {...*} [args] Additional arguments to be passed to the callbacks.
- * @returns {*} By default the method returns `undefined`. However, the return value can be changed by listeners
- * through modification of the {@link module:utils/eventinfo~EventInfo#return `evt.return`}'s property (the event info
- * is the first param of every callback).
- */
- /**
- * Delegates selected events to another {@link module:utils/emittermixin~Emitter}. For instance:
- *
- * emitterA.delegate( 'eventX' ).to( emitterB );
- * emitterA.delegate( 'eventX', 'eventY' ).to( emitterC );
- *
- * then `eventX` is delegated (fired by) `emitterB` and `emitterC` along with `data`:
- *
- * emitterA.fire( 'eventX', data );
- *
- * and `eventY` is delegated (fired by) `emitterC` along with `data`:
- *
- * emitterA.fire( 'eventY', data );
- *
- * @method #delegate
- * @param {...String} events Event names that will be delegated to another emitter.
- * @returns {module:utils/emittermixin~EmitterMixinDelegateChain}
- */
- /**
- * Stops delegating events. It can be used at different levels:
- *
- * * To stop delegating all events.
- * * To stop delegating a specific event to all emitters.
- * * To stop delegating a specific event to a specific emitter.
- *
- * @method #stopDelegating
- * @param {String} [event] The name of the event to stop delegating. If omitted, stops it all delegations.
- * @param {module:utils/emittermixin~Emitter} [emitter] (requires `event`) The object to stop delegating a particular event to.
- * If omitted, stops delegation of `event` to all emitters.
- */
- /**
- * Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter.
- * If not, returns `null`.
- *
- * @protected
- * @param {module:utils/emittermixin~EmitterMixin} listeningEmitter Emitter that listens.
- * @param {String} listenedToEmitterId Unique emitter id of emitter listened to.
- * @returns {module:utils/emittermixin~EmitterMixin|null}
- */
- export function _getEmitterListenedTo( listeningEmitter, listenedToEmitterId ) {
- if ( listeningEmitter[ _listeningTo ] && listeningEmitter[ _listeningTo ][ listenedToEmitterId ] ) {
- return listeningEmitter[ _listeningTo ][ listenedToEmitterId ].emitter;
- }
- return null;
- }
- /**
- * Sets emitter's unique id.
- *
- * **Note:** `_emitterId` can be set only once.
- *
- * @protected
- * @param {module:utils/emittermixin~EmitterMixin} emitter Emitter for which id will be set.
- * @param {String} [id] Unique id to set. If not passed, random unique id will be set.
- */
- export function _setEmitterId( emitter, id ) {
- if ( !emitter[ _emitterId ] ) {
- emitter[ _emitterId ] = id || uid();
- }
- }
- /**
- * Returns emitter's unique id.
- *
- * @protected
- * @param {module:utils/emittermixin~EmitterMixin} emitter Emitter which id will be returned.
- */
- export function _getEmitterId( emitter ) {
- return emitter[ _emitterId ];
- }
- // Gets the internal `_events` property of the given object.
- // `_events` property store all lists with callbacks for registered event names.
- // If there were no events registered on the object, empty `_events` object is created.
- function getEvents( source ) {
- if ( !source._events ) {
- Object.defineProperty( source, '_events', {
- value: {}
- } );
- }
- return source._events;
- }
- // Creates event node for generic-specific events relation architecture.
- function makeEventNode() {
- return {
- callbacks: [],
- childEvents: []
- };
- }
- // Creates an architecture for generic-specific events relation.
- // If needed, creates all events for given eventName, i.e. if the first registered event
- // is foo:bar:abc, it will create foo:bar:abc, foo:bar and foo event and tie them together.
- // It also copies callbacks from more generic events to more specific events when
- // specific events are created.
- function createEventNamespace( source, eventName ) {
- const events = getEvents( source );
- // First, check if the event we want to add to the structure already exists.
- if ( events[ eventName ] ) {
- // If it exists, we don't have to do anything.
- return;
- }
- // In other case, we have to create the structure for the event.
- // Note, that we might need to create intermediate events too.
- // I.e. if foo:bar:abc is being registered and we only have foo in the structure,
- // we need to also register foo:bar.
- // Currently processed event name.
- let name = eventName;
- // Name of the event that is a child event for currently processed event.
- let childEventName = null;
- // Array containing all newly created specific events.
- const newEventNodes = [];
- // While loop can't check for ':' index because we have to handle generic events too.
- // In each loop, we truncate event name, going from the most specific name to the generic one.
- // I.e. foo:bar:abc -> foo:bar -> foo.
- while ( name !== '' ) {
- if ( events[ name ] ) {
- // If the currently processed event name is already registered, we can be sure
- // that it already has all the structure created, so we can break the loop here
- // as no more events need to be registered.
- break;
- }
- // If this event is not yet registered, create a new object for it.
- events[ name ] = makeEventNode();
- // Add it to the array with newly created events.
- newEventNodes.push( events[ name ] );
- // Add previously processed event name as a child of this event.
- if ( childEventName ) {
- events[ name ].childEvents.push( childEventName );
- }
- childEventName = name;
- // If `.lastIndexOf()` returns -1, `.substr()` will return '' which will break the loop.
- name = name.substr( 0, name.lastIndexOf( ':' ) );
- }
- if ( name !== '' ) {
- // If name is not empty, we found an already registered event that was a parent of the
- // event we wanted to register.
- // Copy that event's callbacks to newly registered events.
- for ( const node of newEventNodes ) {
- node.callbacks = events[ name ].callbacks.slice();
- }
- // Add last newly created event to the already registered event.
- events[ name ].childEvents.push( childEventName );
- }
- }
- // Gets an array containing callbacks list for a given event and it's more specific events.
- // I.e. if given event is foo:bar and there is also foo:bar:abc event registered, this will
- // return callback list of foo:bar and foo:bar:abc (but not foo).
- function getCallbacksListsForNamespace( source, eventName ) {
- const eventNode = getEvents( source )[ eventName ];
- if ( !eventNode ) {
- return [];
- }
- let callbacksLists = [ eventNode.callbacks ];
- for ( let i = 0; i < eventNode.childEvents.length; i++ ) {
- const childCallbacksLists = getCallbacksListsForNamespace( source, eventNode.childEvents[ i ] );
- callbacksLists = callbacksLists.concat( childCallbacksLists );
- }
- return callbacksLists;
- }
- // Get the list of callbacks for a given event, but only if there any callbacks have been registered.
- // If there are no callbacks registered for given event, it checks if this is a specific event and looks
- // for callbacks for it's more generic version.
- function getCallbacksForEvent( source, eventName ) {
- let event;
- if ( !source._events || !( event = source._events[ eventName ] ) || !event.callbacks.length ) {
- // There are no callbacks registered for specified eventName.
- // But this could be a specific-type event that is in a namespace.
- if ( eventName.indexOf( ':' ) > -1 ) {
- // If the eventName is specific, try to find callback lists for more generic event.
- return getCallbacksForEvent( source, eventName.substr( 0, eventName.lastIndexOf( ':' ) ) );
- } else {
- // If this is a top-level generic event, return null;
- return null;
- }
- }
- return event.callbacks;
- }
- // Fires delegated events for given map of destinations.
- //
- // @private
- // * @param {Map.<utils.Emitter>} destinations A map containing `[ {@link utils.Emitter}, "event name" ]` pair destinations.
- // * @param {utils.EventInfo} eventInfo The original event info object.
- // * @param {Array.<*>} fireArgs Arguments the original event was fired with.
- function fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
- for ( let [ emitter, name ] of destinations ) {
- if ( !name ) {
- name = eventInfo.name;
- } else if ( typeof name == 'function' ) {
- name = name( eventInfo.name );
- }
- const delegatedInfo = new EventInfo( eventInfo.source, name );
- delegatedInfo.path = [ ...eventInfo.path ];
- emitter.fire( delegatedInfo, ...fireArgs );
- }
- }
- // Removes callback from emitter for given event.
- //
- // @param {module:utils/emittermixin~Emitter} emitter
- // @param {String} event
- // @param {Function} callback
- function removeCallback( emitter, event, callback ) {
- const lists = getCallbacksListsForNamespace( emitter, event );
- for ( const callbacks of lists ) {
- for ( let i = 0; i < callbacks.length; i++ ) {
- if ( callbacks[ i ].callback == callback ) {
- // Remove the callback from the list (fixing the next index).
- callbacks.splice( i, 1 );
- i--;
- }
- }
- }
- }
- /**
- * The return value of {@link ~EmitterMixin#delegate}.
- *
- * @interface module:utils/emittermixin~EmitterMixinDelegateChain
- */
- /**
- * Selects destination for {@link module:utils/emittermixin~EmitterMixin#delegate} events.
- *
- * @method #to
- * @param {module:utils/emittermixin~Emitter} emitter An `EmitterMixin` instance which is the destination for delegated events.
- * @param {String|Function} [nameOrFunction] A custom event name or function which converts the original name string.
- */
|