Report an issue

guideIntegrating track changes with your application

This guide describes integrating track changes as a standalone plugin. If you are using real-time-collaboration, refer to the Real-time collaborative features integration guide.

The track changes plugin provides an API for managing suggestions added to the document. Through this API you can manage suggestions in the rich-text editor. To load and save the suggestions data from your database you need to provide a proper integration.

This guide will discuss two ways to integrate CKEditor 5 with your suggestions data source:

The adapter integration is the recommended one because it gives you better control over the data.

It is also recommended to use the track changes plugin together with the comments plugin. Check how to integrate the comments plugin with your WYSIWYG editor.

# Before you start

Track changes is a commercial plugin and a license key is needed to authenticate. If you do not have one, please contact us.

Before you start creating an integration, there are a few concepts you should be familiar with. This guide will explain how to create a custom build with the track changes plugin, what data structure the suggestions use and what the track changes plugin API looks like.

Make sure that you understand all of these concepts before you proceed to the integration.

Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or as a starting point for your own integration.

# Prepare a custom build

The track changes plugin is not included in any CKEditor 5 build. To enable it, you need to create a custom CKEditor 5 build that includes the track changes plugin.

git clone -b stable https://github.com/ckeditor/ckeditor5-build-classic.git ckeditor5-with-track-changes
cd ckeditor5-with-track-changes
npm install

You need to install the @ckeditor5/ckeditor5-track-changes package using npm:

npm install --save-dev @ckeditor/ckeditor5-track-changes

To make track changes work, you need to import the TrackChanges plugin and add it to the list of plugins.

An updated src/ckeditor.js should look like this:

import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';

import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import Image from '@ckeditor/ckeditor5-image/src/image';

import TrackChanges from '@ckeditor/ckeditor5-track-changes/src/trackchanges';

export default class ClassicEditor extends ClassicEditorBase {}

// Plugins to include in the build.
ClassicEditor.builtinPlugins = [ Essentials, Paragraph, Bold, Italic, Image, TrackChanges ];

// The editor configuration.
ClassicEditor.defaultConfig = {
    language: 'en'
};

Note that your custom build needs to be bundled using webpack.

npm run build

Read more about installing plugins.

# Core setup

For completeness sake, examples below implement the wide sidebar display mode for suggestion annotations. If you want to use the inline display mode, remove parts of the snippets which set up the sidebar.

When you have the track changes package included in your custom build, prepare an HTML structure for the sidebar. After that you can enable the track changes plugin. Proceed as follows:

  • Set up a two-column layout.
  • Add the sidebar configuration.
  • Add the trackChanges dropdown to the toolbar.
  • Add your licenseKey. If you do not have a key yet, please contact us.

Edit the sample/index.html file as follows:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>CKEditor 5 collaboration with track changes</title>
    <style type="text/css">
     #container {
        /* To create the column layout. */
        display: flex;

        /* To make the container relative to its children. */
        position: relative;
     }

     #container .ck.ck-editor {
        /* To stretch the editor to max 700px
            (just to look nice for this example but it can be any size). */
        width: 100%;
        max-width: 700px;
     }

     #sidebar {
        /* Set some size for the sidebar (it can be any). */
        min-width: 300px;

        /* Add some distance. */
        padding: 0 10px;
     }
    </style>
</head>
<body>

<div id="container">
    <div id="editor"></div>
    <div id="sidebar"></div>
</div>
<script src="../build/ckeditor.js"></script>
<script>
    const initialData =
        `<h2>
            Bilingual Personality Disorder
        </h2>
        <p>
            This may be the first time you hear about this
            <suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="start"></suggestion>
            made-up<suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="end"></suggestion>
            disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you then you realise.
            According to the studies, the language a person speaks affects their cognition,
            <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="start"></suggestion>
            feelings, <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="end"></suggestion>
            behaviour, emotions and hence <strong>their personality</strong>.
        </p>
        <p>
            This shouldn’t come as a surprise
            <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
            that different regions of the brain becomes more active depending on the activity.
            Since structure, information and especially
            <suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="start"></suggestion>
            the culture of languages<suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="end"></suggestion>
            varies substantially
            and the language a person speaks is a essential element of daily life.
        </p>`;

    ClassicEditor.create( document.querySelector( '#editor' ), {
        initialData,
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'trackChanges' ]
        }
    } )
    .catch( error => console.error( error ) );
</script>

</body>
</html>

When you open the sample in the browser, you should see the WYSIWYG editor with the track changes plugin. However, it still does not load or save any data, so you will get an error when you try to add any suggestion. You will learn how to add data to the track changes plugin later in this guide.

# Comments

Track changes uses the comments plugin to allow discussing in suggestions. You should be familiar with the comments integration guide before you start integrating suggestions.

# Suggestion data structure

Below is the data format of a suggestion stored in CKEditor 5.

{
    id: 'suggestion-1',         // String
    type: 'insertion',          // String
    authorId: 'user-1',         // String
    createdAt: new Date( ... ), // Date
    hasComments: false          // Boolean
    data: { ... }               // Object|null
}

The suggestion type may include a subtype, for example: formatInline:886cqig6g8rf. In case of format suggestions, the subtype is a hash of the data object. This allows for fast and easy comparison of two suggestions (whether they perform the same action).

# Track changes API

Below is the public API provided by the TrackChanges plugin.

/**
 * Adds suggestion data.
 *
 * Use this method to add suggestion data during the editor initialization
 * if you do not use the adapter integration.
 *
 * @param {Object} params Parameters of a suggestion to add.
 */
addSuggestion( params ) {}

/**
 * Iterates through suggestion data for all the suggestions that are
 * present in the editor content.
 *
 * @returns {Iterator.<Object>}
 */
* getSuggestions() {}

/**
 * An adapter object that should communicate with the data source,
 * provide data for suggestions and save suggestion data when it changes.
 *
 * The adapter is optional. If you decide to set it, it should be an object
 * that implements the methods described below.
 */
adapter

/**
 * Called each time the suggestion data is needed.
 *
 * The method should return a promise that resolves with the suggestion data object.
 *
 * @param {String} id The ID of the suggestion to get.
 * @returns {Promise}
 */
adapter.getSuggestion( id ) {}

/**
 * Called each time a new suggestion is created.
 *
 * The method should save the suggestion data in the database
 * and return a promise that should be resolved when the save is
 * completed.
 *
 * If the promise resolves with an object with the `createdAt` property, this
 * suggestion property will be updated in the suggestion in the editor.
 * This is to update the suggestion data with server-side information.
 *
 * The `params` object does not expect the `authorId` property.
 * For security reasons, the author of the suggestions should be set
 * on the server side.
 *
 * If `params.originalSuggestionId` is set, the new suggestion should
 * have the `authorId` property set to the same as the suggestion with
 * `originalSuggestionId`. This happens when one user breaks
 * another user's suggestion, creating a new suggestion as a result.
 *
 * In any other case, use the current (local) user to set `authorId`.
 *
 * The `params` object does not expect the `createdAt` property either.
 * You should use the server-side time generator to ensure that all users
 * see the same date.
 *
 * @param {Object} params
 * @param {String} params.id The suggestion ID.
 * @param {String} params.type The suggestion type. May include a subtype (then
 * the format is <type>:<subtype>).
 * @param {Boolean} params.hasComments Always `false`,
 * a new suggestion does not have comments.
 * @param {String} [params.originalSuggestionId] The ID of the suggestion from which
 * the `authorId` property should be taken.
 * @param {Object|null} [params.data] Additional suggestion data.
 * Used by format suggestions.
 * @returns {Promise}
 */
adapter.addSuggestion( params ) {}

/**
 * Called each time the suggestion data has changed. The only data that
 * may change is information whether a suggestion has comments or not, and the suggestion state.
 * When the first comment is added to the suggestion, the only
 * comment is removed from the suggestion or the suggestion was accepted or rejected, 
 * the `updateSuggestion()` method is called with proper data.
 *
 * For suggestions with `hasComments` set to `false`, the editor
 * will not try to fetch the comment thread through the comments adapter.
 *
 * The method should update the suggestion data in the database
 * and return a promise that should be resolved when the save is
 * completed.
 *
 * @param {String} id The suggestion ID.
 * @param {Object} params Can include the suggestion `state` (open|accepted|rejected) and the `hasComments` flag
 * that informs whether the suggestion has comments or not.
 * @param {Boolean} params.hasComments Defines if the suggestion has comments or not.
 * @param {'open'|'accepted'|'rejected'} params.state Sets the suggestion state and can be set to `open`, `accepted` or `rejected`.
 * @returns {Promise}
 */
adapter.updateSuggestion( id, params ) {}

To use this API you need to get the track changes plugin:

// Get the track changes plugin.
const trackChangesPlugin = editor.plugins.get( 'TrackChanges' );

// Add a suggestion.
trackChangesPlugin.addSuggestion( data );

// Get all suggestions.
Array.from( trackChangesPlugin.getSuggestions() );

// Set the adapter.
trackChangesPlugin.adapter = {
    ...
};

If editor.plugins.get( 'TrackChanges' ) returns undefined, make sure that your build contains the TrackChanges plugin.

Now you are ready to provide the integration.

# A simple “load and save” integration

In this solution, users and suggestions data is loaded during the editor initialization and suggestions data is saved after you finish working with the editor (for example when you submit the form containing the WYSIWYG editor).

This method is recommended only if you can trust your users or if you provide additional validation of the submitted data to make sure that the user changed their suggestions only.

Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or as a starting point for your own integration.

# Loading the data

When the track changes plugin is already included in the editor, you need to create a plugin which will initialize users and existing suggestions.

If your plugin needs to request the suggestions data from the server asynchronously, you can return a Promise from the Plugin.init method to make sure that the editor initialization waits for your data.

First, dump the users and the suggestions data to a variable that will be available for your plugin.

// Application data will be available under a global variable `appData`.
const appData = {
    // Users data.
    users: [
        {
            id: 'user-1',
            name: 'Joe Doe',
            // Note that the avatar is optional.
            avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
        },
        {
            id: 'user-2',
            name: 'Ella Harper',
            avatar: 'https://randomuser.me/api/portraits/thumb/women/65.jpg'
        }
    ],

    // The ID of the current user.
    userId: 'user-1',

    // Suggestions data.
    suggestions: [
        {
            id: 'suggestion-1',
            type: 'insertion',
            authorId: 'user-2',
            createdAt: new Date( 2019, 1, 13, 11, 20, 48 )
        },
        {
            id: 'suggestion-2',
            type: 'deletion',
            authorId: 'user-1',
            createdAt: new Date( 2019, 1, 14, 12, 7, 20 )
        },
        {
            id: 'suggestion-3',
            type: 'formatInline:886cqig6g8rf',
            authorId: 'user-1',
            createdAt: new Date( 2019, 2, 8, 10, 2, 7 ),
            data: {
                commandName: 'bold',
                commandParams: [ { forceValue: true } ]
            }
        }
    ],

    // Editor initial data.
    initialData:
        `<h2>
            Bilingual Personality Disorder
        </h2>
        <p>
            This may be the first time you hear about this
            <suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="start"></suggestion>
            made-up<suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="end"></suggestion>
            disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you then you realise.
            According to the studies, the language a person speaks affects their cognition,
            <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="start"></suggestion>
            feelings, <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="end"></suggestion>
            behaviour, emotions and hence <strong>their personality</strong>.
        </p>
        <p>
            This shouldn’t come as a surprise
            <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
            that different regions of the brain becomes more active depending on the activity.
            Since structure, information and especially
            <suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="start"></suggestion>
            the culture of languages<suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="end"></suggestion>
            varies substantially
            and the language a person speaks is a essential element of daily life.
        </p>`
};

Then, prepare a plugin that will read the data from appData and use the Users and TrackChanges API.

class TrackChangesIntegration {
    constructor( editor ) {
        this.editor = editor;
    }

    init() {
        const usersPlugin = this.editor.plugins.get( 'Users' );
        const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' );

        // Load the users data.
        for ( const user of appData.users ) {
            usersPlugin.addUser( user );
        }

        // Set the current user.
        usersPlugin.defineMe( appData.userId );

        // Load the suggestions data.
        for ( const suggestion of appData.suggestions ) {
            trackChangesPlugin.addSuggestion( suggestion );
        }

        // In order to load comments added to suggestions, you
        // should also configure the comments integration.
    }
}

Finally, add the plugin in the editor configuration.

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.initialData,
        extraPlugins: [ TrackChangesIntegration ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'trackChanges' ]
        }
    } )
    .catch( error => console.error( error ) );

# Saving the data

To save the suggestions data you need to get it from the TrackChanges API first. To do this, use the getSuggestions() method.

Then, use the suggestions data to save it in your database in a way you prefer. See the example below.

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.initialData,
        extraPlugins: [ TrackChangesIntegration ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'trackChanges' ]
        }
    } )
    .then( editor => {
        // After the editor is initialized, add an action to be performed after a button is clicked.
        const trackChanges = editor.plugins.get( 'TrackChanges' );

        // Get the data on demand.
        document.querySelector( '#get-data' ).addEventListener( 'click', () => {
            const editorData = editor.data.get();
            const suggestionsData = Array.from( trackChanges.getSuggestions() );

            // Now, use `editorData` and `suggestionsData` to save the data in your application.
            // For example, you can set them as values of hidden input fields.
            console.log( editorData );
            console.log( suggestionsData );
        } );
    } )
    .catch( error => console.error( error ) );

# Full implementation

Below you can find the final solution.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>CKEditor 5 collaboration with track changes</title>
        <style type="text/css">
         #container {
            /* To create the column layout. */
            display: flex;

            /* To make the container relative to its children. */
            position: relative;
         }

         #container .ck.ck-editor {
            /* To stretch the editor to max 700px
                (just to look nice for this example but it can be any size). */
            width: 100%;
            max-width: 700px;
         }

         #sidebar {
            /* Set some size for the sidebar (it can be any). */
            min-width: 300px;

            /* Add some distance. */
            padding: 0 10px;
         }
        </style>
    </head>
    <body>
        <button id="get-data">Get editor data</button>

        <div id="container">
            <div id="editor"> </div>
            <div id="sidebar"></div>
        </div>
    </body>
    <script src="../build/ckeditor.js"></script>
    <script>
        // Application data will be available under a global variable `appData`.
        const appData = {
            // Users data.
            users: [
                {
                    id: 'user-1',
                    name: 'Joe Doe',
                    // Note that the avatar is optional.
                    avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
                },
                {
                    id: 'user-2',
                    name: 'Ella Harper',
                    avatar: 'https://randomuser.me/api/portraits/thumb/women/65.jpg'
                }
            ],

            // The ID of the current user.
            userId: 'user-1',

            // Suggestions data.
            suggestions: [
                {
                    id: 'suggestion-1',
                    type: 'insertion',
                    authorId: 'user-2',
                    createdAt: new Date( 2019, 1, 13, 11, 20, 48 )
                },
                {
                    id: 'suggestion-2',
                    type: 'deletion',
                    authorId: 'user-1',
                    createdAt: new Date( 2019, 1, 14, 12, 7, 20 )
                },
                {
                    id: 'suggestion-3',
                    type: 'formatInline:886cqig6g8rf',
                    authorId: 'user-1',
                    createdAt: new Date( 2019, 2, 8, 10, 2, 7 ),
                    data: {
                        commandName: 'bold',
                        commandParams: [ { forceValue: true } ]
                    }
                }
            ],

            // Editor initial data.
            initialData:
                `<h2>
                    Bilingual Personality Disorder
                </h2>
                <p>
                    This may be the first time you hear about this
                    <suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="start"></suggestion>
                    made-up<suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="end"></suggestion>
                    disorder but it actually isn’t so far from the truth.
                    As recent studies show, the language you speak has more effects on you then you realise.
                    According to the studies, the language a person speaks affects their cognition,
                    <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="start"></suggestion>
                    feelings, <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="end"></suggestion>
                    behaviour, emotions and hence <strong>their personality</strong>.
                </p>
                <p>
                    This shouldn’t come as a surprise
                    <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
                    that different regions of the brain becomes more active depending on the activity.
                    Since structure, information and especially
                    <suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="start"></suggestion>
                    the culture of languages<suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="end"></suggestion>
                    varies substantially
                    and the language a person speaks is a essential element of daily life.
                </p>`
        };

        class TrackChangesIntegration {
            constructor( editor ) {
                this.editor = editor;
            }

            init() {
                const usersPlugin = this.editor.plugins.get( 'Users' );
                const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' );

                // Load the users data.
                for ( const user of appData.users ) {
                    usersPlugin.addUser( user );
                }

                // Set the current user.
                usersPlugin.defineMe( appData.userId );

                // Load the suggestions data.
                for ( const suggestion of appData.suggestions ) {
                    trackChangesPlugin.addSuggestion( suggestion );
                }

                // In order to load comments added to suggestions, you
                // should also configure the comments integration.
            }
        }

        ClassicEditor
            .create( document.querySelector( '#editor' ), {
                initialData: appData.initialData,
                extraPlugins: [ TrackChangesIntegration ],
                licenseKey: 'your-license-key',
                sidebar: {
                    container: document.querySelector( '#sidebar' )
                },
                toolbar: {
                    items: [ 'bold', 'italic', '|', 'trackChanges' ]
                }
            } )
            .then( editor => {
                // After the editor is initialized, add an action
                // to be performed after a button is clicked.
                const trackChanges = editor.plugins.get( 'TrackChanges' );

                // Get the data on demand.
                document.querySelector( '#get-data' ).addEventListener( 'click', () => {
                    const editorData = editor.data.get();
                    const suggestionsData = Array.from( trackChanges.getSuggestions() );

                    // Now, use `editorData` and `suggestionsData` to save the data in your application.
                    // For example, you can set them as values of hidden input fields.
                    console.log( editorData );
                    console.log( suggestionsData );
                } );
            } )
            .catch( error => console.error( error ) );
    </script>
</html>

Note that this sample does not handle comments saving and loading. Check the comments integration guide to learn how to build a complete solution. Also note that both snippets define the same list of users. Make sure to deduplicate this code and define the list of users only once to avoid errors.

# Live sample

Console

// Use the `Save data with track changes` button to see the result...

# Adapter integration

An adapter integration uses an adapter object — provided by you — to immediately save suggestions in your data store. This is a recommended way of integrating track changes with your application because it lets you handle the client-server communication in a more secure way. For example, you can check user permissions, validate sent data or update the data with information obtained on the server side, like the suggestion creation date. You will see how to handle the server response in the next steps.

Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or as a starting point for your own integration.

# Implementation

First, define the adapter using the TrackChanges#adapter setter. Adapter methods allow you to load and save changes in your database.

On the UI side each change in suggestions is performed immediately, however, all adapter actions are asynchronous and are performed in the background. Because of this all adapter methods need to return a Promise. When the promise is resolved, it means that everything went fine and a local change was successfully saved in the data store. When the promise is rejected, the editor fires the crash event. When you handle the server response you can decide if the promise should be resolved or rejected.

While the adapter is saving the suggestion data, a pending action is automatically added to the editor PendingActions plugin, so you do not have to worry that the editor will be destroyed before the adapter action has finished.

Now you are ready to implement the adapter.

// Application data will be available under a global variable `appData`.
const appData = {
    // Users data.
    users: [
        {
            id: 'user-1',
            name: 'Joe Doe',
            // Note that the avatar is optional.
            avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
        },
        {
            id: 'user-2',
            name: 'Ella Harper',
            avatar: 'https://randomuser.me/api/portraits/thumb/women/65.jpg'
        }
    ],

    // The ID of the current user.
    userId: 'user-1',

    // Editor initial data.
    initialData:
        `<h2>
            Bilingual Personality Disorder
        </h2>
        <p>
            This may be the first time you hear about this
            <suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="start"></suggestion>
            made-up<suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="end"></suggestion>
            disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you then you realise.
            According to the studies, the language a person speaks affects their cognition,
            <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="start"></suggestion>
            feelings, <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="end"></suggestion>
            behaviour, emotions and hence <strong>their personality</strong>.
        </p>
        <p>
            This shouldn’t come as a surprise
            <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
            that different regions of the brain becomes more active depending on the activity.
            Since structure, information and especially
            <suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="start"></suggestion>
            the culture of languages<suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="end"></suggestion>
            varies substantially
            and the language a person speaks is a essential element of daily life.
        </p>`
};

class TrackChangesAdapter {
    constructor( editor ) {
        this.editor = editor;
    }

    init() {
        const usersPlugin = this.editor.plugins.get( 'Users' );
        const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' );

        // Load the users data.
        for ( const user of appData.users ) {
            usersPlugin.addUser( user );
        }

        // Set the current user.
        usersPlugin.defineMe( appData.userId );

        // Set the adapter to the `TrackChanges#adapter` property.
        trackChangesPlugin.adapter = {
            getSuggestion: suggestionId => {
                console.log( 'Getting suggestion', suggestionId );

                // Write a request to your database here.
                // The returned `Promise` should be resolved with the suggestion
                // data object when the request has finished.
                switch ( suggestionId ) {
                    case 'suggestion-1':
                        return Promise.resolve( {
                            id: suggestionId,
                            type: 'insertion',
                            authorId: 'user-2',
                            createdAt: new Date()
                        } );
                    case 'suggestion-2':
                        return Promise.resolve( {
                            id: suggestionId,
                            type: 'deletion',
                            authorId: 'user-1',
                            createdAt: new Date()
                        } );
                    case 'suggestion-3':
                        return Promise.resolve( {
                            id: suggestionId,
                            type: 'formatInline:886cqig6g8rf',
                            authorId: 'user-1',
                            createdAt: new Date( 2019, 2, 8, 10, 2, 7 ),
                            data: {
                                commandName: 'bold',
                                commandParams: [ { forceValue: true } ]
                            }
                        } );
                }
            },

            addSuggestion: suggestionData => {
                console.log( 'Suggestion added', suggestionData );

                // Write a request to your database here.
                // The returned `Promise` should be resolved when the request
                // has finished. When the promise resolves with the suggestion data
                // object, it will update the editor suggestion using the provided data.
                return Promise.resolve( {
                    createdAt: new Date()       // Should be set on the server side.
                } );
            },

            updateSuggestion: ( id, suggestionData ) => {
                console.log( 'Suggestion updated', id, suggestionData );

                // Write a request to your database here.
                // The returned `Promise` should be resolved when the request
                // has finished.
                return Promise.resolve();
            }
        };

        // In order to load comments added to suggestions, you
        // should also integrate the comments adapter.
    }
}

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.initialData,
        extraPlugins: [ TrackChangesAdapter ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'trackChanges' ]
        }
    } )
    .catch( error => console.error( error ) );

The adapter is now ready to use with your rich-text editor.

Below is the final solution.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>CKEditor 5 Collaboration with track changes</title>
        <style type="text/css">
         #container {
            /* To create the column layout. */
            display: flex;

            /* To make the container relative to its children. */
            position: relative;
         }

         #container .ck.ck-editor {
            /* To stretch the editor to max 700px
                (just to look nice for this example but it can be any size). */
            width: 100%;
            max-width: 700px;
         }

         #sidebar {
            /* Set some size for the sidebar (it can be any). */
            min-width: 300px;

            /* Add some distance. */
            padding: 0 10px;
         }
        </style>
    </head>
    <body>
        <div id="container">
            <div id="editor"></div>
            <div id="sidebar"></div>
        </div>
    </body>
    <script src="../build/ckeditor.js"></script>
    <script>
        // Application data will be available under a global variable `appData`.
        const appData = {
            // Users data.
            users: [
                {
                    id: 'user-1',
                    name: 'Joe Doe',
                    // Note that the avatar is optional.
                    avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
                },
                {
                    id: 'user-2',
                    name: 'Ella Harper',
                    avatar: 'https://randomuser.me/api/portraits/thumb/women/65.jpg'
                }
            ],

            // The ID of the current user.
            userId: 'user-1',

            // Editor initial data.
            initialData:
                `<h2>
                    Bilingual Personality Disorder
                </h2>
                <p>
                    This may be the first time you hear about this
                    <suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="start"></suggestion>
                    made-up<suggestion id="suggestion-1:user-2" suggestion-type="insertion" type="end"></suggestion>
                    disorder but it actually isn’t so far from the truth.
                    As recent studies show, the language you speak has more effects on you then you realise.
                    According to the studies, the language a person speaks affects their cognition,
                    <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="start"></suggestion>
                    feelings, <suggestion id="suggestion-2:user-1" suggestion-type="deletion" type="end"></suggestion>
                    behaviour, emotions and hence <strong>their personality</strong>.
                </p>
                <p>
                    This shouldn’t come as a surprise
                    <a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
                    that different regions of the brain becomes more active depending on the activity.
                    Since structure, information and especially
                    <suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="start"></suggestion>
                    the culture of languages<suggestion id="suggestion-3:user-1" suggestion-type="formatInline:886cqig6g8rf" type="end"></suggestion>
                    varies substantially
                    and the language a person speaks is a essential element of daily life.
                </p>`
        };

        class TrackChangesAdapter {
            constructor( editor ) {
                this.editor = editor;
            }

            init() {
                const usersPlugin = this.editor.plugins.get( 'Users' );
                const trackChangesPlugin = this.editor.plugins.get( 'TrackChanges' );

                // Load the users data.
                for ( const user of appData.users ) {
                    usersPlugin.addUser( user );
                }

                // Set the current user.
                usersPlugin.defineMe( appData.userId );

                // Set the adapter to the `TrackChanges#adapter` property.
                trackChangesPlugin.adapter = {
                    getSuggestion: suggestionId => {
                        console.log( 'Getting suggestion', suggestionId );

                        // Write a request to your database here.
                        // The returned `Promise` should be resolved with the suggestion
                        // data object when the request has finished.
                        switch ( suggestionId ) {
                            case 'suggestion-1':
                                return Promise.resolve( {
                                    id: suggestionId,
                                    type: 'insertion',
                                    authorId: 'user-2',
                                    createdAt: new Date()
                                } );
                            case 'suggestion-2':
                                return Promise.resolve( {
                                    id: suggestionId,
                                    type: 'deletion',
                                    authorId: 'user-1',
                                    createdAt: new Date()
                                } );
                            case 'suggestion-3':
                                return Promise.resolve( {
                                    id: suggestionId,
                                    type: 'formatInline:886cqig6g8rf',
                                    authorId: 'user-1',
                                    createdAt: new Date( 2019, 2, 8, 10, 2, 7 ),
                                    data: {
                                        commandName: 'bold',
                                        commandParams: [ { forceValue: true } ]
                                    }
                                } );
                        }
                    },

                    addSuggestion: suggestionData => {
                        console.log( 'Suggestion added', suggestionData );

                        // Write a request to your database here.
                        // The returned `Promise` should be resolved with the suggestion
                        // data object when the request has finished.
                        return Promise.resolve( {
                            createdAt: new Date()       // Should be set on the server side.
                        } );
                    },

                    updateSuggestion: ( id, suggestionData ) => {
                        console.log( 'Suggestion updated', id, suggestionData );

                        // Write a request to your database here.
                        // The returned `Promise` should be resolved when the request
                        // has finished.
                        return Promise.resolve();
                    }
                };

                // In order to load comments added to suggestions, you
                // should also integrate the comments adapter.
            }
        }

        ClassicEditor
            .create( document.querySelector( '#editor' ), {
                initialData: appData.initialData,
                extraPlugins: [ TrackChangesAdapter ],
                licenseKey: 'your-license-key',
                sidebar: {
                    container: document.querySelector( '#sidebar' )
                },
                toolbar: {
                    items: [ 'bold', 'italic', '|', 'trackChanges' ]
                }
            } )
            .catch( error => console.error( error ) );
    </script>
</html>

Note that this sample does not contain the comments adapter. Check the comments integration guide to learn how to build a complete solution. Also note that both snippets define the same list of users. Make sure to deduplicate this code and define the list of users only once to avoid errors.

# Live sample

Pending adapter actions console

// Add a suggestion to see the result...

Since the track changes adapter saves suggestions immediately after they are performed, it is also recommended to use the Autosave plugin to save the editor content after each change.

# Why is there no event when I accept or reject a suggestion?

Note that similarly to removing comments, when you reject or accept a suggestion, no event is fired in the adapter. This is because the suggestion is never removed from the editor during the editing session. You are able to restore it using undo (Ctrl+Z or Cmd+Z). The same happens when you remove a paragraph with suggestions — there will be no event fired because no data is really removed.

However, to make sure that you do not keep outdated suggestions in your database, you should do a clean-up when the editor is destroyed or closed. You can compare suggestions stored in the document (the IDs of the <suggestion> tags) with suggestions stored in your database and remove all suggestions that no longer have a representation in your database.

# PHP integration example

Please refer to the Collaboration integration examples for CKEditor 5 repository to find a working end-to-end integration example of collaboration features in a PHP application. Note that it includes both comments and track changes adapters working together.