Report an issue

guideIntegrating track changes with custom features

The track changes feature was designed with custom features in mind. In this guide, you will learn how to integrate your plugins with the track changes feature.

# Enabling commands

CKEditor 5 uses commands to change the editor content. Most modifications of the editor content are done through command execution. Also, commands are usually connected with toolbar buttons that represent their state. For example, if a given command is disabled at the moment, the toolbar button is disabled as well.

By default, a command is disabled when tracking changes is turned on. This is to prevent errors and incorrect behavior of a command that has not been prepared to work in the suggestion mode. The first step to integrating your command is to enable it:

const trackChangesEditing = editor.plugins.get( 'TrackChangesEditing' );

trackChangesEditing.enableCommand( 'commandName' );

Now the command will be enabled in the track changes mode. However, this on its own does not introduce any additional functionality related to integration with the track changes feature. The command will work as though track changes is turned off.

If your command does not introduce any change to the document model (for example, it shows a UI component), it may be enough to just simply enable it.

Further integration steps will depend on what kind of change your command performs.

# Insertions and deletions

Many custom features are focused on introducing a new kind of element or a widget. It is recommended to use model.insertContent() to insert such object into the document content. This method, apart from taking care of maintaining a proper document structure, is also integrated with the track changes feature. Any content inserted with this method will be automatically marked with an insertion suggestion.

Similarly, removing is also integrated with track changes through the integration with model.deleteContent(). Calling this method while in the suggestion mode will create a deletion suggestion instead. The same goes for actions that remove content, for example using the Backspace and Delete keys, typing over selected content, or pasting over the selected content.

In summary, if your command inserts or removes a widget, chances are it will work in the suggestion mode out-of-the-box.

One thing that may still need your attention is suggestion description. To generate a proper suggestion description, you need to register a label for the introduced element. The description mechanism supports translations and different labels for single and multiple added/removed elements.

An example of integration for the page break element:

const t = editor.t;

trackChangesEditing.descriptionFactory.registerElementLabel(
    'pageBreak',

    quantity => t( {
        string: 'page break',
        plural: '%0 page breaks',
        id: 'ELEMENT_PAGE_BREAK'
    }, quantity )
);

After registering an element label, it will be used in suggestion descriptions. For the above example, the description may be “Remove: page break” or, for example, “Insert: 3 page breaks.” Using the translation system and adding translations for custom features is described in the Localization guide.

If you don’t need to support multiple languages, you can skip using translations system:

trackChangesEditing.descriptionFactory.registerElementLabel(
    'customElement',

    quantity => quantity == 1 ? 'custom element' : quantity + ' custom elements'
);

If you don’t specify a label, then the model element is used by default.

If for some reason your feature cannot use model.insertContent() and model.deleteContent(), or you need a more advanced integration, you can assign a custom callback that will be called whenever the command is executed while suggestion mode is on:

trackChangesEditing.enableCommand( 'commandName', ( executeCommand, options ) => {
    // Here you can overwrite what happens when the command is executed in the suggestion mode.
    // See the API documentation to learn more about the parameters passed to the callback.
    // ...
} );

Then you will need to use one of the following methods from the track changes API:

# Attribute changes

Another category of changes is attribute changes. If your custom feature introduces attribute that is applied to text or can be changed by the user, you may want to implement this type of integration.

To enable tracking attribute changes, use enableDefaultAttributesIntegration() method. It is a simple helper, designed to handle most of the scenarios related to attribute changes. It will enable the command, so you don’t need to additionally use enableCommand().

Additionally, you will need to register every attribute key that you will want to track. This is done using registerInlineAttribute() and registerBlockAttribute() methods.

For example, if you have a customCommand that changes the value of customAttribute set on text, use the following snippet:

trackChangesEditing.enableDefaultAttributesIntegration( 'customCommand' );
trackChangesEditing.registerInlineAttribute( 'customAttribute' );

Attributes are categorized into two groups: inline (for attributes set on text) and block (for attributes set on model elements). This differentiation is necessary due to the distinct logic for handling suggestions made on different types of content.

Each command integrated this way will track all registered attributes and create suggestions for their changes. Each attribute should be registered only once.

For attribute formatting each attribute registered in track changes should also have its label registered:

const t = editor.t;

plugin.descriptionFactory.registerAttributeLabel( 'bold', t( 'bold' ) );

If you require more complex logic when evaluating the suggestion description, refer to Setting custom suggestion description section below.

# Other formatting changes

The above sections cover typical custom plugins and commands, which simply add a new element or change some attributes.

If your feature is more complex e.g. introduces multiple attributes which values depend on each other, or you command performs multiple related changes at once, then you may need to use a different approach.

In such cases, you will need to pass a custom callback to enableCommand() which will overwrite the default command behavior when track changes is on. In the callback you will need to use the track changes API:

  • markInlineFormat() – Marks a given range as an inline format suggestion. Used for attribute changes on inline elements and text.
  • markBlockFormat() – Marks a format suggestion on a block element.
  • markMultiRangeBlockFormat() – Used for format suggestions that contain multiple elements that are not next to one another.

Refer to the API documentation of these methods to learn more.

Format suggestions are strictly connected with commands. These suggestions data consists of: the range on which command was fired, the command name and the command options. When the suggestion is accepted, the specified command is executed on the suggestion range with the specified options. In other words, the original command execution is “replayed”.

The general approach to handle format suggestions for your custom feature is to use a similar logic as in your custom feature but instead of applying changes to the content, create a suggestion using the track changes API:

  1. Overwrite the command callback (using enableCommand()) so the command is not executed.
  2. Use the same logic as in your custom command to decide whether the command should do anything.
  3. Use the same logic as in your custom command to evaluate all command parameters that has not been set (and would use a default value if the command’s original code was executed). This is important: a suggestion must have all parameters set, so its execution does not rely on the current content state but on the content state at the time when the suggestion was created.
  4. Use the selection range or evaluate a more precise range(s) for the suggestion(s).
  5. Use markInlineFormat() or markBlockFormat() to create one or more suggestions using the previously evaluated variables.

Note, that if the command is integrated this way, the command change will not be actually executed (in other words, the action will not be reflected in the content). The command will be executed only after the suggestion is eventually accepted.

An example of an integration for imageTypeInline command:

plugin.enableCommand( 'imageTypeInline', ( executeCommand, options ) => {
    // Commands work on the current selection, so the track
    // changes integration also works on the current selection.
    // Find element that will be affected.
    const image = imageUtils.getClosestSelectedImageElement( editor.model.document.selection );

    editor.model.change( () => {
        plugin.markBlockFormat(
            image,
            {
                // The command to be executed when the suggestion is accepted.
                commandName: 'imageTypeInline',
                // Parameters for the command.
                commandParams: [ options ]
            },
            [],
            'convertBlockImageToInline' );
    } );
} );

See next section to learn how to generate labels for formatting suggestions.

# Setting custom suggestion description

To complete the integration of your command using the format suggestion or for more complex logic for attribute suggestion, you will need to provide a callback that will generate a description for such a suggestion. This is different from registering a label for insertion/deletion suggestions as format suggestions are more varied and complex.

An example of a description callback for the block quote command:

plugin.descriptionFactory.registerDescriptionCallback( suggestion => {
    const { data } = suggestion;

    if ( !data || data.commandName != 'blockQuote' ) {
        return;
    }

    if ( data.commandParams[ 0 ].forceValue ) {
        return {
            type: 'format',
            content: '*Set format:* block quote'
        };
    }

    return {
        type: 'format',
        content: '*Remove format:* block quote'
    };
} );

Note that the description callback can be also used for insertion and deletion suggestions if you want to overwrite the default description.