src/emittermixin.js
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @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';
import CKEditorError from './ckeditorerror';
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 ) {
try {
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;
} catch ( err ) {
// @if CK_DEBUG // throw err;
/* istanbul ignore next */
CKEditorError.rethrowUnexpectedError( err, this );
}
},
/**
* @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~Emitter} listeningEmitter An emitter that listens.
* @param {String} listenedToEmitterId Unique emitter id of emitter listened to.
* @returns {module:utils/emittermixin~Emitter|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~Emitter} emitter An 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~Emitter} emitter An 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 module:utils/emittermixin~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.
*/