Report an issue

guideIntegrating comments with your application

The comments feature provides an API that lets you add, remove, and update comments in the editor. To save and access all these changes in your database, you first need to integrate this feature.

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

# Before you start

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

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

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 comments plugin, what data structure the comments use and what the comments 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.

# Activating the feature

To use this premium feature, you need to activate it with proper credentials. Refer to the License key and activation guide for details.

After you have successfully obtained all the credentials needed, let’s create a custom editor and configure it.

# Prepare a custom build

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

git clone -b stable https://github.com/ckeditor/ckeditor5
cd ckeditor5/packages/ckeditor5-build-classic

npm install

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

npm install --save-dev @ckeditor/ckeditor5-comments

To make comments work in the editor, you need to import the Comments plugin and add it to the list of plugins. The Comments plugin imports a few other plugins responsible for various areas of the comments feature. CommentsRepository is also included and will be available in the editor.

Then, you need to specify the comments editor configuration - as the comments editor is also an instance of Editor class, it can be also configured for your needs similarly to how the main editor is configured.

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

import { ClassicEditor as ClassicEditorBase } from '@ckeditor/ckeditor5-editor-classic';

import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
import { Image } from '@ckeditor/ckeditor5-image';
import { List } from '@ckeditor/ckeditor5-list';
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';

import { Comments } from '@ckeditor/ckeditor5-comments';

export default class ClassicEditor extends ClassicEditorBase {}

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

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

    // Provide the configuration for the comments feature.
    comments: {
        editorConfig: {
            // The list of plugins that will be included in the comments editors.
            extraPlugins: [ Bold, Italic, List, Autoformat ]
        }
    }
};

It is possible to use the default configuration for the comments editor by passing {} to the comments.editorConfig property.

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 comment threads. If you want to use the inline display mode, remove parts of the snippets that set up the sidebar.

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

  • Set up a two-column layout.
  • Add the sidebar configuration.
  • Add the comment button to the toolbar. If you use the ImageToolbar plugin, also add the comment button to the image toolbar.
  • Add the commentsArchive button to the toolbar.
  • Add your licenseKey. If you do not have a key yet, please contact us. If you have already purchased a license, the key can be accessed via the user dashboard.

Edit the sample/index.html file as follows:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>CKEditor&nbsp;5 collaboration with comments</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>
            <comment-start name="thread-1"></comment-start>
            Bilingual Personality Disorder
            <comment-end name="thread-1"></comment-end>
        </h2>
        <p>
            This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you than you realize.
            According to the studies, the language a person speaks affects their cognition,
            behavior, 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 become more active depending on the activity.
            The structure, information and especially <strong>the culture</strong> of languages varies substantially
            and the language a person speaks is an essential element of daily life.
        </p>`;

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

</body>
</html>

When you open the sample in the browser you should see the WYSIWYG editor with the comments plugin. However, it still does not load or save any data and the user is not defined so you will get a warning and the user will be defined as “Anonymous” when you try to add any comment. You will learn how to add data to the comments plugin later in this guide.

# Comments API

The integration below uses the comments API. Making yourself familiar with the API may help you understand the code snippets. In case of any problems, refer to the comments API documentation.

# A simple “load and save” integration

In this solution, users and comments data is loaded during the editor initialization, and comments 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 comments 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 comments plugin is already included in the editor, you need to create a plugin which will initialize users and existing comments.

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

If your application needs to request the comments data from the server asynchronously, instead of putting the data in the HTML source, you can create a plugin that will fetch the data from the database. In this case, your plugin should return a Promise from the Plugin.init method to make sure that the editor initialization waits for your data.

// Application data will be available under a global variable `appData`.
const appData = {
    // Users data.
    users: [
        {
            id: 'user-1',
            name: 'Mex Haddox'
        },
        {
            id: 'user-2',
            name: 'Zee Croce'
        }
    ],

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

    // Comment threads data.
    commentThreads: [
        {
            threadId: 'thread-1',
            comments: [
                {
                    commentId: 'comment-1',
                    authorId: 'user-1',
                    content: '<p>Are we sure we want to use a made-up disorder name?</p>',
                    createdAt: new Date( '09/20/2018 14:21:53' ),
                    attributes: {}
                },
                {
                    commentId: 'comment-2',
                    authorId: 'user-2',
                    content: '<p>Why not?</p>',
                    createdAt: new Date( '09/21/2018 08:17:01' ),
                    attributes: {}
                }
            ],
            context: {
                type: 'text',
                value: 'Bilingual Personality Disorder'
            },
            unlinkedAt: null,
            resolvedAt: null,
            resolvedBy: null,
            attributes: {}
        }
    ],

     // Editor initial data.
     initialData:
         `<h2>
             <comment-start name="thread-1"></comment-start>
             Bilingual Personality Disorder
             <comment-end name="thread-1"></comment-end>
         </h2>
         <p>
             This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
             As recent studies show, the language you speak has more effects on you than you realize.
             According to the studies, the language a person speaks affects their cognition,
             behavior, 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 become more active depending on the activity.
             The structure, information and especially <strong>the culture</strong> of languages varies substantially
             and the language a person speaks is an essential element of daily life.
         </p>`
};

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

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

    static get requires() {
        return [ 'CommentsRepository' ];
    }

    init() {
        const usersPlugin = this.editor.plugins.get( 'Users' );
        const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

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

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

        // Load the comment threads data.
        for ( const commentThread of appData.commentThreads ) {
            commentsRepositoryPlugin.addCommentThread( commentThread );
        }
    }
}

Finally, add the plugin in the editor configuration.

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.initialData,
        extraPlugins: [ CommentsIntegration ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [
                'undo', 'redo',
                '|', 'comment', 'commentsArchive',
                '|', 'heading',
                '|', 'bold', 'italic',
                '|', 'link',
                '|', 'bulletedList', 'numberedList'
            ]
        }
    } )
    .catch( error => console.error( error ) );

# Saving the data

To save the comments data, you need to get it using the CommentsRepository API first. To do this, use the getCommentThreads() method.

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

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData,
        extraPlugins: [ CommentsIntegration ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [
                'undo', 'redo',
                '|', 'comment', 'commentsArchive',
                '|', 'heading',
                '|', 'bold', 'italic',
                '|', 'link',
                '|', 'bulletedList', 'numberedList'
            ]
        }
    } )
    .then( editor => {
        // After the editor is initialized, add an action to be performed after a button is clicked.
        const commentsRepository = editor.plugins.get( 'CommentsRepository' );

        // Get the data on demand.
        document.querySelector( '#get-data' ).addEventListener( 'click', () => {
            const editorData = editor.data.get();
            const commentThreadsData = commentsRepository.getCommentThreads( {
                skipNotAttached: true,
                skipEmpty: true,
                toJSON: true
            } );

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

It is recommended to stringify the attributes value to JSON and save it as a string in your database and then to parse the value from JSON when loading comments.

# Full implementation

Below you can find the final solution.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>CKEditor&nbsp;5 collaboration with comments</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: 'Mex Haddox'
                },
                {
                    id: 'user-2',
                    name: 'Zee Croce'
                }
            ],

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

            // Comment threads data.
            commentThreads: [
                {
                    threadId: 'thread-1',
                    comments: [
                        {
                            commentId: 'comment-1',
                            authorId: 'user-1',
                            content: '<p>Are we sure we want to use a made-up disorder name?</p>',
                            createdAt: new Date( '09/20/2018 14:21:53' ),
                            attributes: {}
                        },
                        {
                            commentId: 'comment-2',
                            authorId: 'user-2',
                            content: '<p>Why not?</p>',
                            createdAt: new Date( '09/21/2018 08:17:01' ),
                            attributes: {}
                        }
                    ],
                    context: {
                        type: 'text',
                        value: 'Bilingual Personality Disorder'
                    },
                    unlinkedAt: null,
                    resolvedAt: null,
                    resolvedBy: null,
                    attributes: {}
                }
            ],

            // Editor initial data.
            initialData:
                `<h2>
                    <comment-start name="thread-1"></comment-start>
                    Bilingual Personality Disorder
                    <comment-end name="thread-1"></comment-end>
                </h2>
                <p>
                    This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
                    As recent studies show, the language you speak has more effects on you than you realize.
                    According to the studies, the language a person speaks affects their cognition,
                    behavior, 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 become more active depending on the activity.
                    The structure, information and especially <strong>the culture</strong> of languages varies substantially
                    and the language a person speaks is an essential element of daily life.
                </p>`
        };

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

            static get requires() {
                return [ 'CommentsRepository' ];
            }

            init() {
                const usersPlugin = this.editor.plugins.get( 'Users' );
                const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

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

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

                // Load the comment threads data.
                for ( const commentThread of appData.commentThreads ) {
                    commentsRepositoryPlugin.addCommentThread( commentThread );
                }
            }
        }

        ClassicEditor
            .create( document.querySelector( '#editor' ), {
                initialData: appData.initialData,
                extraPlugins: [ CommentsIntegration ],
                licenseKey: 'your-license-key',
                sidebar: {
                    container: document.querySelector( '#sidebar' )
                },
                toolbar: {
                    items: [
                        'undo', 'redo',
                        '|', 'comment', 'commentsArchive',
                        '|', 'heading',
                        '|', 'bold', 'italic',
                        '|', 'link',
                        '|', 'bulletedList', 'numberedList'
                    ]
                }
            } )
            .then( editor => {
                // After the editor is initialized, add an action to be performed after a button is clicked.
                const commentsRepository = editor.plugins.get( 'CommentsRepository' );

                // Get the data on demand.
                document.querySelector( '#get-data' ).addEventListener( 'click', () => {
                    const editorData = editor.data.get();
                    const commentThreadsData = commentsRepository.getCommentThreads( {
                        skipNotAttached: true,
                        skipEmpty: true,
                        toJSON: true
                    } );

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

# Demo

Console

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

# Adapter integration

Adapter integration uses an adapter object – provided by you – to immediately save changes in comments in your data store. This is the recommended way of integrating comments with your application because it lets you handle the client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side, like the comment creation date. You will see how to handle the server response in the following 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 CommentsRepository#adapter property. Adapter methods are called after the user makes a change in the comments. The adapter allows you to save the change in your database immediately. Each comment action has a separate adapter method that you should implement.

On the UI side, each change in comments 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 throws a CKEditorError error, which works nicely together with the watchdog feature. When you handle the server response you can decide if the promise should be resolved or rejected.

While any adapter action is being performed, 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: 'Mex Haddox'
        },
        {
            id: 'user-2',
            name: 'Zee Croce'
        }
    ],

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

    // Editor initial data.
    initialData:
        `<h2>
            <comment-start name="thread-1"></comment-start>
            Bilingual Personality Disorder
            <comment-end name="thread-1"></comment-end>
        </h2>
        <p>
            This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
            As recent studies show, the language you speak has more effects on you than you realize.
            According to the studies, the language a person speaks affects their cognition,
            behavior, 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 become more active depending on the activity.
            The structure, information and especially <strong>the culture</strong> of languages varies substantially
            and the language a person speaks is an essential element of daily life.
        </p>`

};

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

    static get requires() {
        return [ 'CommentsRepository' ];
    }

    init() {
        const usersPlugin = this.editor.plugins.get( 'Users' );
        const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

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

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

        // Set the adapter on the `CommentsRepository#adapter` property.
        commentsRepositoryPlugin.adapter = {
            addComment( data ) {
                console.log( 'Comment added', data );

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

            updateComment( data ) {
                console.log( 'Comment updated', data );

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

            removeComment( data ) {
                console.log( 'Comment removed', data );

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

            addCommentThread( data ) {
                console.log( 'Comment thread added', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve( {
                    threadId: data.threadId,
                    comments: data.comments.map( ( comment ) => ( { commentId: comment.commentId, createdAt: new Date() } ) ) // Should be set on the server side.
                } );
            },

            getCommentThread( data ) {
                console.log( 'Getting comment thread', data );

                // Write a request to your database here. The returned `Promise`
                // should resolve with the comment thread data.
                return Promise.resolve( {
                    threadId: data.threadId,
                    comments: [
                        {
                            commentId: 'comment-1',
                            authorId: 'user-2',
                            content: '<p>Are we sure we want to use a made-up disorder name?</p>',
                            createdAt: new Date(),
                            attributes: {}
                        }
                    ],
                    // It defines the value on which the comment has been created initially.
                    // If it is empty it will be set based on the comment marker.
                    context: {
                        type: 'text',
                        value: 'Bilingual Personality Disorder'
                    },
                    unlinkedAt: null,
                    resolvedAt: null,
                    resolvedBy: null,
                    attributes: {},
                    isFromAdapter: true
                } );
            },

            updateCommentThread( data ) {
                console.log( 'Comment thread updated', data );

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

            resolveCommentThread( data ) {
                console.log( 'Comment thread resolved', data );

                // Write a request to your database here. The returned `Promise`
                // should be resolved when the request has finished.
                return Promise.resolve( {
                    resolvedAt: new Date(), // Should be set on the server side.
                    resolvedBy: usersPlugin.me.id // Should be set on the server side.
                } );
            },

            reopenCommentThread( data ) {
                console.log( 'Comment thread reopened', data );

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

            removeCommentThread( data ) {
                console.log( 'Comment thread removed', data );

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

        };
    }
}

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.initialData,
        extraPlugins: [ CommentsAdapter ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar' )
        },
        toolbar: {
            items: [
                'undo', 'redo',
                '|', 'comment', 'commentsArchive',
                '|', 'heading',
                '|', 'bold', 'italic',
                '|', 'link',
                '|', 'bulletedList', 'numberedList'
            ]
        }
    } )
    .catch( error => console.error( error ) );

It is recommended to stringify the attributes value to JSON and save it as a string in your database and then to parse the value from JSON when loading comments.

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&nbsp;5 Collaboration with comments</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: 'Mex Haddox'
                },
                {
                    id: 'user-2',
                    name: 'Zee Croce'
                }
            ],

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

            // Editor initial data.
            initialData:
                `<h2>
                    <comment-start name="thread-1"></comment-start>
                    Bilingual Personality Disorder
                    <comment-end name="thread-1"></comment-end>
                </h2>
                <p>
                    This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
                    As recent studies show, the language you speak has more effects on you than you realize.
                    According to the studies, the language a person speaks affects their cognition,
                    behavior, 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 become more active depending on the activity.
                    The structure, information and especially <strong>the culture</strong> of languages varies substantially
                    and the language a person speaks is an essential element of daily life.
                </p>`
        };

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

            static get requires() {
                return [ 'CommentsRepository' ];
            }

            init() {
                const usersPlugin = this.editor.plugins.get( 'Users' );
                const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );

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

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

                // Set the adapter on the `CommentsRepository#adapter` property.
                commentsRepositoryPlugin.adapter = {
                    addComment( data ) {
                        console.log( 'Comment added', data );

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

                    updateComment( data ) {
                        console.log( 'Comment updated', data );

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

                    removeComment( data ) {
                        console.log( 'Comment removed', data );

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

                    addCommentThread( data ) {
                        console.log( 'Comment thread added', data );

                        // Write a request to your database here. The returned `Promise`
                        // should be resolved when the request has finished.
                        return Promise.resolve( {
                            threadId: data.threadId,
                            comments: data.comments.map( ( comment ) => ( { commentId: comment.commentId, createdAt: new Date() } ) ) // Should be set on the server side.
                        } );
                    },

                    getCommentThread( data ) {
                        console.log( 'Getting comment thread', data );

                        // Write a request to your database here. The returned `Promise`
                        // should resolve with the comment thread data.
                        return Promise.resolve( {
                            threadId: data.threadId,
                            comments: [
                                {
                                    commentId: 'comment-1',
                                    authorId: 'user-2',
                                    content: '<p>Are we sure we want to use a made-up disorder name?</p>',
                                    createdAt: new Date(),
                                    attributes: {}
                                }
                            ],
                            // It defines the value on which the comment has been created initially.
                            // If it is empty it will be set based on the comment marker.
                            context: {
                                type: 'text',
                                value: 'Bilingual Personality Disorder'
                            },
                            unlinkedAt: null,
                            resolvedAt: null,
                            resolvedBy: null,
                            attributes: {},
                            isFromAdapter: true
                        } );
                    },

                    updateCommentThread( data ) {
                        console.log( 'Comment thread updated', data );

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

                    resolveCommentThread( data ) {
                        console.log( 'Comment thread resolved', data );

                        // Write a request to your database here. The returned `Promise`
                        // should be resolved when the request has finished.
                        return Promise.resolve( {
                            resolvedAt: new Date(), // Should be set on the server side.
                            resolvedBy: usersPlugin.me.id // Should be set on the server side.
                        } );
                    },

                    reopenCommentThread( data ) {
                        console.log( 'Comment thread reopened', data );

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


                    removeCommentThread( data ) {
                        console.log( 'Comment thread removed', data );

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

        ClassicEditor
            .create( document.querySelector( '#editor' ), {
                initialData: appData.initialData,
                extraPlugins: [ CommentsAdapter ],
                licenseKey: 'your-license-key',
                sidebar: {
                    container: document.querySelector( '#sidebar' )
                },
                toolbar: {
                    items: [
                        'undo', 'redo',
                        '|', 'comment', 'commentsArchive',
                        '|', 'heading',
                        '|', 'bold', 'italic',
                        '|', 'link',
                        '|', 'bulletedList', 'numberedList'
                    ]
                }
            } )
            .catch( error => console.error( error ) );
    </script>
</html>

# Demo

Pending adapter actions console

// Add, remove or update a comment to see the result...

Since the comments adapter saves the comment changes 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 remove comment thread markers from the content?

Note that no remove event is fired when you remove the marker corresponding to the comment thread. Instead, the comment thread is resolved which triggers CommentsRepository#resolveCommentThread event. This operation can be restored using undo (Cmd+Z or Ctrl+Z), which will fire CommentsRepository#reopenCommentThread event.

However, you can still remove the comment thread by using available buttons in an annotation. Remember that the removal operation cannot be undone.

# PHP integration example

Please refer to 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.