Event system
Emitters are objects that can fire events. They also provide means to listen to other emitters’ events.
Emitters are heavily used throughout the entire editor architecture. They are the building blocks for mechanisms such as the observables, engine’s view observers, and conversion.
Any class can become an event emitter. All you need to do is mix the EmitterMixin
into it:
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
class AnyClass {
// ...
}
mix( AnyClass, EmitterMixin );
# Listening to events
Adding a callback to an event is simple. You can listen directly on the emitter object and use an anonymous function:
emitter.on( 'eventName', ( eventInfo, ...args ) => { ... } );
However, a function object is needed if you want to be able to remove the event listener:
emitter.off( 'eventName', handler );
There is also another way to add an event listener — by using listenTo()
. This way one emitter can listen to events on another emitter:
foo.listenTo( bar, 'eventName', ( eventInfo, ...args ) => { ... } );
Now you can easily detach the foo
from bar
simply by stopListening()
.
// Stop listening to a specific handler.
foo.stopListening( bar, 'eventName', handler );
// Stop listening to a specific event.
foo.stopListening( bar, 'eventName' );
// Stop listening to all events fired by a specific emitter.
foo.stopListening( bar );
// Stop listening to all events fired by all bound emitters.
foo.stopListening();
The on()
and off()
methods are shorthands for listenTo( this, ... )
and stopListening( this, ... )
(the emitter is bound to itself).
# Listener priorities
By default, all listeners are bound on the normal
priority, but you can specify the priority when registering a listener:
this.on( 'eventName', () => { ... }, { priority: 'high' } );
this.listenTo( emitter, 'eventName', () => { ... }, { priority: 'high' } );
There are 5 named priorities:
highest
high
normal
low
lowest
Listeners are triggered in the order of these priorities (first highest
, then high
, etc.). For multiple listeners attached on the same priority, they are fired in the order of the registration.
Note: If any listener stops the event, no other listeners including those on lower priorities will be called.
It is possible to use relative priorities priorities.get( 'high' ) + 10
but this is strongly discouraged.
# Stopping events and returned value
The first argument passed to an event handler is always an instance of the EventInfo
. There you can check the event name
, the source
emitter of the event, and you can stop()
the event from further processing.
emitter.on( 'eventName', ( eventInfo, data ) => {
console.log( 'foo' );
eventInfo.stop();
} );
emitter.on( 'eventName', ( eventInfo, data ) => {
console.log( 'bar' ); // This won't be called.
} );
emitter.fire( 'eventName' ); // Logs "foo" only.
Listeners can set the return
value. This value will be returned by fire()
after all callbacks are processed.
emitter.on( 'eventName', ( eventInfo, data ) => {
eventInfo.return = 123;
} );
emitter.fire( 'eventName' ); // -> 123
# Listening on namespaced events
The event system supports namespaced events to give you the possibility to build a structure of callbacks. Namespacing is achieved by using :
in the event name:
this.fire( 'foo:bar:baz', data );
Then the listeners can be bound to a specific event or the whole namespace:
this.on( 'foo', () => { ... } );
this.on( 'foo:bar', () => { ... } );
this.on( 'foo:bar:baz', () => { ... } );
This way you can have more general events, listening to a broader event ('foo'
in this case), or more detailed callbacks listening to specified events ('foo:bar'
or 'foo:bar:baz'
).
This mechanism is used for instance in the conversion, where thanks to events named as 'insert:<elementName>'
you can listen to the insertion of a specific element (e.g. 'insert:p'
) or all elements insertion ('insert'
).
Note: Listeners registered on the same priority will be fired in the order of the registration (no matter if listening to a whole namespace or to a specific event).
# Firing events
Once you mix the EmitterMixin
into your class, you can fire events the following way:
this.fire( 'eventName', argA, argB, ... );
All passed arguments will be available in all listeners that are added to the event.
Note: Most base classes (like Command
or Plugin
) are emitters already and fire their own events.
# Stopped events
It is sometimes useful to know if an event was stopped by any of the listeners. There is an alternative way of firing an event just for that:
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
// Prepare the event info...
const eventInfo = new EventInfo( this, 'eventName' );
// ...and fire the event.
this.fire( eventInfo, argA, argB, ... );
// Here you can check if the event was stopped.
if ( eventInfo.stop.called ) {
// The event was stopped.
}
Note that EventInfo
expects the source object in the first parameter as the origin of the event.
# Event return value
If any handler set the eventInfo.return
field, this value will be returned by fire()
after all callbacks are processed.
emitter.on( 'eventName', ( eventInfo, ...args ) => {
eventInfo.return = 123;
} );
const result = emitter.fire( 'eventName', argA, argB, ... );
console.log( result ); // -> 123
# Delegating events
The Emitter
interface also provides the event delegation mechanism, so that selected events are fired by another Emitter
.
# Setting events delegation
Delegate specific events to another emitter:
emitterA.delegate( 'foo' ).to( emitterB );
emitterA.delegate( 'foo', 'bar' ).to( emitterC );
You can delegate events with a different name:
emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterA.delegate( 'foo' ).to( emitterB, name => `delegated:${ name }` );
It is also possible to delegate all the events:
emitterA.delegate( '*' ).to( emitterB );
Note: Delegated events are fired from the target emitter no matter if they were stopped in any handler on the source emitter.
# Stopping delegation
You can stop delegation by calling the stopDelegating()
method. It can be used at different levels:
// Stop delegating all events.
emitterA.stopDelegating();
// Stop delegating a specific event to all emitters.
emitterA.stopDelegating( 'foo' );
// Stop delegating a specific event to a specific emitter.
emitterA.stopDelegating( 'foo', emitterB );
# Delegated event info
The delegated events provide the path
of emitters that this event met along the delegation path.
emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterB.delegate( 'bar' ).to( emitterC, 'baz' );
emitterA.on( 'foo', eventInfo => console.log( 'event', eventInfo.name, 'emitted by A; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterB.on( 'bar', eventInfo => console.log( 'event', eventInfo.name, 'emitted by B; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterC.on( 'baz', eventInfo => console.log( 'event', eventInfo.name, 'emitted by C; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterA.fire( 'foo' );
// Outputs:
// event "foo" emitted by A; source: emitterA; path: [ emitterA ]
// event "bar" emitted by B; source: emitterA; path: [ emitterA, emitterB ]
// event "baz" emitted by C; source: emitterA; path: [ emitterA, emitterB, emitterC ]
# View events bubbling
The view.Document
is not only an Observable and an emitter but it also implements the special BubblingEmitter
interface (implemented by BubblingEmitterMixin
). It provides a mechanism for bubbling events over the virtual DOM tree.
It is different from the bubbling that you know from the DOM tree event bubbling. You do not register listeners on specific instances of the elements in the view document tree. Instead, you can register handlers for specific contexts. A context is either a name of an element, or one of the virtual contexts ('$capture'
, '$text'
, '$root'
, '$document'
), or a callback to match desired nodes.
# Listening to bubbling events
Listeners registered in the context of the view element names:
this.listenTo( view.document, 'enter', ( evt, data ) => {
// ...
}, { context: 'blockquote' } );
this.listenTo( view.document, 'enter', ( evt, data ) => {
// ...
}, { context: 'li' } );
Listeners registered in the virtual contexts:
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
// ...
}, { context: '$text', priority: 'high' } );
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
// ...
}, { context: '$root' } );
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
// ...
}, { context: '$capture' } );
Listeners registered in the context of a custom callback function:
import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
// ...
}, { context: isWidget } );
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
// ...
}, { context: isWidget, priority: 'high' } );
Note: Without specifying the context
, events are bound to the '$document'
context.
# Bubbling events flow
Bubbling always starts from the virtual '$capture'
context. All listeners attached to this context are triggered first (and in the order of their priorities).
Then, the real bubbling starts from the selection position (either its anchor or focus — depending on what is deeper).
If text nodes are allowed at the selection position, then the first context is '$text'
. Then the event bubbles through all elements up to the '$root'
and finally '$document'
.
In all contexts listeners can be registered at desired priorities. If a listener stops an event, this event is not fired for the remaining contexts.
# Examples
Assuming the given content and selection:
<blockquote>
<p>
Foo[]bar
</p>
</blockquote>
Events will be fired for the following contexts:
'$capture'
'$text'
'p'
'blockquote'
'$root'
'$document'
Assuming the given content and selection (on a widget):
<blockquote>
<p>
Foo
[<img />] // enhanced with toWidget()
bar
</p>
</blockquote>
Events will be fired for the following contexts:
'$capture'
'img'
- widget (assuming a custom matcher was used)
'p'
'blockquote'
'$root'
'$document'
An even more complex example:
<blockquote>
<figure class="table"> // enhanced with toWidget()
<table>
<tr>
<td>
<p>
foo[]bar
</p>
</td>
</tr>
</table>
</figure>
</blockquote>
Events that will be fired:
'$capture'
'$text'
'p'
'td'
'tr'
'table'
'figure'
- widget (assuming a custom matcher was used)
'blockquote'
'$root'
'$document'
# BubblingEventInfo
In some events the first parameter is not the standard EventInfo
, but BubblingEventInfo
. This is an extension that provides the current eventPhase
and currentTarget
.
Currently, this information is available for the following events:
Hence the events from the above example would be extended with the following eventPhase
data:
'$capture'
- capturing'$text'
- atTarget'p'
- bubbling'td'
- bubbling'tr'
- bubbling'table'
- bubbling'figure'
- bubbling- widget - bubbling
'blockquote'
- bubbling'$root'
- bubbling'$document'
- bubbling
And for the example with the widget selected:
<blockquote>
<p>
Foo
[<img />] // enhanced with toWidget()
bar
</p>
</blockquote>
Events that will be fired:
'$capture'
- capturing'img'
- atTarget- widget - atTarget (assuming a custom matcher was used)
'p'
- bubbling'blockquote'
- bubbling'$root'
- bubbling'$document'
- bubbling
Every day, we work hard to keep our documentation complete. Have you spotted an outdated information? Is something missing? Please report it via our issue tracker.