Event system

Contribute to this guide Show the table of contents

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 Emitter into it:

import { EmitterMixin, mix } from 'ckeditor5';

class AnyClass {
	// Class's code.
	// ...
}

mix( AnyClass, EmitterMixin );
Copy code

Listening to events

Copy link

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 ) => { /* ... */ } );
Copy code

However, a function object is needed if you want to be able to remove the event listener:

emitter.off( 'eventName', handler );
Copy code

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 ) => { /* ... */ } );
Copy code

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();
Copy code
Note

The on() and off() methods are shorthands for listenTo( this, /* ... */ ) and stopListening( this, /* ... */ ) (the emitter is bound to itself).

Listener priorities

Copy link

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' } );
Copy code

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

Copy link

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.
Copy code

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
Copy code

Listening on namespaced events

Copy link

The event system supports namespaced events to give you the possibility to build a structure of callbacks. You can achieve namespacing by using : in the event name:

this.fire( 'foo:bar:baz', data );
Copy code

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', () => { /* ... */ } );
Copy code

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 'insert:<elementName>' you can listen to the insertion of a specific element (like '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

Copy link

Once you mix the Emitter into your class, you can fire events the following way:

this.fire( 'eventName', argA, argB, /* ... */ );
Copy code

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

Copy link

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 'ckeditor5';

// 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.
}
Copy code

Note that EventInfo expects the source object in the first parameter as the origin of the event.

Event return value

Copy link

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
Copy code

Delegating events

Copy link

The Emitter interface also provides the event delegation mechanism, so that selected events are fired by another Emitter.

Setting events delegation

Copy link

Delegate specific events to another emitter:

emitterA.delegate( 'foo' ).to( emitterB );
emitterA.delegate( 'foo', 'bar' ).to( emitterC );
Copy code

You can delegate events with a different name:

emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterA.delegate( 'foo' ).to( emitterB, name => `delegated:${ name }` );
Copy code

It is also possible to delegate all the events:

emitterA.delegate( '*' ).to( emitterB );
Copy code

Note: Delegated events are fired from the target emitter no matter if they were stopped in any handler on the source emitter.

Stopping delegation

Copy link

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 );
Copy code

Delegated event info

Copy link

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 ]
Copy code

View events bubbling

Copy link

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

Copy link

Listeners registered in the context of the view element names:

this.listenTo( view.document, 'enter', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: 'blockquote' } );

this.listenTo( view.document, 'enter', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: 'li' } );
Copy code

Listeners registered in the virtual contexts:

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$text', priority: 'high' } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$root' } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$capture' } );
Copy code

Listeners registered in the context of a custom callback function:

import { isWidget } from 'ckeditor5';

this.listenTo( view.document, 'arrowKey', ( evt, data ) =&gt; {
	// Listener's code.
	// ...
}, { context: isWidget } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) =&gt; {
	// Listener's code.
	// ...
}, { context: isWidget, priority: 'high' } );
Copy code

Note: Without specifying the context, events are bound to the '$document' context.

Bubbling events flow

Copy link

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

Copy link

Assuming the given content and selection:

<blockquote>
    <p>
        Foo[]bar
    </p>
</blockquote>
Copy code

Events will be fired for the following contexts:

  1. '$capture'
  2. '$text'
  3. 'p'
  4. 'blockquote'
  5. '$root'
  6. '$document'

Assuming the given content and selection (on a widget):

<blockquote>
    <p>
        Foo
        [<img />]	// enhanced with toWidget()
        bar
    </p>
</blockquote>
Copy code

Events will be fired for the following contexts:

  1. '$capture'
  2. 'img'
  3. widget (assuming a custom matcher was used)
  4. 'p'
  5. 'blockquote'
  6. '$root'
  7. '$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>
Copy code

Events that will be fired:

  1. '$capture'
  2. '$text'
  3. 'p'
  4. 'td'
  5. 'tr'
  6. 'table'
  7. 'figure'
  8. widget (assuming a custom matcher was used)
  9. 'blockquote'
  10. '$root'
  11. '$document'

BubblingEventInfo

Copy link

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:

  1. '$capture' - capturing
  2. '$text' - at target
  3. 'p' - bubbling
  4. 'td' - bubbling
  5. 'tr' - bubbling
  6. 'table' - bubbling
  7. 'figure' - bubbling
  8. widget - bubbling
  9. 'blockquote' - bubbling
  10. '$root' - bubbling
  11. '$document' - bubbling

And for the example with the widget selected:

<blockquote>
    <p>
        Foo
        [<img />]	 // Enhanced with toWidget().
        bar
    </p>
</blockquote>
Copy code

Events that will be fired:

  1. '$capture' - capturing
  2. 'img' - at target
  3. widget - at target (assuming a custom matcher was used)
  4. 'p' - bubbling
  5. 'blockquote' - bubbling
  6. '$root' - bubbling
  7. '$document' - bubbling