Report an issue

guideCustom template for annotations

Providing a custom template is a middle ground between using the default UI and providing a completely custom UI on your own.

This solution gives you the possibility to alter the HTML structure of the comment and suggestion annotations. Thanks to that you can, for example:

  • Add new CSS classes or HTML elements to enable more complex styling.
  • Re-arrange annotation views.
  • Add new UI elements, linked with your custom features.
  • Remove UI elements.

It is highly recommended to get familiar with the CKEditor 5 UI library API and guide before continuing.

# Views for comments and suggestions

The view classes used by default by CKEditor 5 collaboration features are:

These classes are exported in files in their respective repositories at the following paths:

  • @ckeditor/ckeditor5-comments/src/comments/ui/view/commentthreadview.js,
  • @ckeditor/ckeditor5-comments/src/comments/ui/view/commentview.js,
  • @ckeditor/ckeditor5-track-changes/src/ui/view/suggestionthreadview.js.

Note that the source code of these files and classes is closed.

It is highly recommended to get familiar with the comment view, comment thread view and the suggestion view API docs before continuing.

# Changing the view template

To use a custom view template, you need to:

  1. Create a new view class by extending the default view class.
  2. Provide (or extend) the template by overwriting the getTemplate() method.
  3. Set your custom view class through the editor configuration. We recommend setting it in the default configuration in the editor build file.

Creating a custom view:

// mycommentview.js

import CommentView from '@ckeditor/ckeditor5-comments/src/comments/ui/view/commentview';

// Create a new comment view class basing on the default view.
class MyCommentView extends CommentView {
    // Overwrite the method to provide a custom template.
    getTemplate() {
        return {
            // Provide the template definition here.
        };
    }
}

Setting up configuration in the build file:

// build.js

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

export default class ClassicEditor extends ClassicEditorBase {}

// ...

// Make sure the `defaultConfig` object exists.
ClassicEditor.defaultConfig.comments = {
    CommentView: MyCommentView
};

Custom comment and suggestion thread views can be set in a similar way:

ClassicEditor.defaultConfig.comments = {
    CommentThreadView: MyCommentThreadView
};

ClassicEditor.defaultConfig.trackChanges = {
    SuggestionThreadView: MySuggestionThreadView
};

# Example: Adding a new button

Below is an example of how you can provide a simple custom feature that requires creating a new UI element and additional styling.

The proposed feature should allow for marking some comments as important. An important comment should have a yellow border on the right.

To bring this feature, you need:

  • A CSS class to change the border of an important comment.
  • A button to toggle the comment model state.
  • An integration between the model state and the view template.

# Styling for an important comment

This step is easy. Simply create a CSS file and add a CSS rule:

/* importantcomment.css */

/* Yellow border for an important comment. */
.ck-comment--important {
    border-right: 3px solid hsl( 55, 98%, 48% );
}

# Creating a button and adding it to the template

In this step, you will add some new elements to the template.

Keep in mind that you do not need to overwrite the whole template. You can extend it instead.

// importantcommentview.js

import CommentView from '@ckeditor/ckeditor5-comments/src/comments/ui/view/commentview';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';

import './importantcomment.css';

export default class ImportantCommentView extends CommentView {
    constructor( ...args ) {
        super( ...args );

        // Bind `isImportant` value to comment model custom attributes.
        this.bind( 'isImportant' ).to( this._model, 'attributes', attributes => !!attributes.isImportant );
    }

    getTemplate() {
        // Use the original method to get the default template.
        // The default template definition structure is described in the comment view API.
        const templateDefinition = super.getTemplate();

        // If `isImportant` is `true`, add the `ck-comment--important` class to the template.
        templateDefinition.children[ 0 ].attributes.class.push( this.bindTemplate.if( 'isImportant', 'ck-comment--important' ) );

        // Add the new button next to other comment buttons (edit and remove).
        templateDefinition.children[ 0 ].children[ 1 ].children[ 1 ].children.unshift( this._createImportantButtonView() );

        return templateDefinition;
    }

    _createImportantButtonView() {
        // Create a new button view.
        const button = new ButtonView( this.locale );

        // Create an icon for the button view.
        const starIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M15.22 18.36c.18-.02.35-.1.46-.25a.6.6 0 00.11-.5l-1.12-5.32 4.12-3.66a.6.6 0 00.18-.65.63.63 0 00-.54-.42l-5.54-.6L10.58 2a.64.64 0 00-.58-.37.64.64 0 00-.58.37l-2.3 4.94-5.55.6a.63.63 0 00-.54.43.6.6 0 00.18.65l4.12 3.66-1.12 5.32c-.05.24.04.49.25.63.2.14.47.16.68.04L10 15.59l4.86 2.69c.1.06.23.09.36.08zm-.96-1.83l-3.95-2.19a.65.65 0 00-.62 0l-3.95 2.19.91-4.33a.6.6 0 00-.2-.58L3.1 8.64l4.51-.5a.64.64 0 00.51-.36L10 3.76l1.88 4.02c.09.2.28.34.5.36l4.52.5-3.35 2.98a.6.6 0 00-.2.58l.91 4.33z"/></svg>';

        // Use the localization service.
        // The feature will be translatable.
        const t = this.locale.t;

        // Set the label and the icon for the button.
        button.set( {
            icon: starIcon,
            isToggleable: true,
            tooltip: t( 'Important' ),
            withText: true
        } );

        // Add a class to the button to style it.
        button.extendTemplate( {
            attributes: {
                class: 'ck-button-important'
            }
        } );

        // The button should be enabled if the comment model is not in read-only mode.
        // The same setting is used for other comment buttons.
        button.bind( 'isEnabled' ).to( this._model, 'isReadOnly', value => !value );

        // The button should be hidden if the comment is not editable
        // (this is true when the current user is not the comment author).
        // The same setting is used for other comment buttons.
        button.bind( 'isVisible' ).to( this._model, 'isEditable' );

        // When the button is clicked, change the comment state in the comment model.
        // The `attributes.isImportant` value will be available together with other comment data.
        button.on( 'execute', () => {
            this._model.setAttribute( 'isImportant', !this._model.attributes.isImportant );
        } );

        return button;
    }
}

# Enabling the custom view

The custom view will be enabled in the build file, as shown earlier.

// build.js

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 Comments from '@ckeditor/ckeditor5-track-changes/src/comments';
import TrackChanges from '@ckeditor/ckeditor5-track-changes/src/trackchanges';

import ImportantCommentView from './importantcommentview';

class Editor extends ClassicEditorBase {}

Editor.builtinPlugins = [ Essentials, Paragraph, Bold, Italic, Comments, TrackChanges ];

Editor.defaultConfig = {
    language: 'en',
    comments: {
        CommentView: ImportantCommentView,
        editorConfig: {
            plugins: [ Essentials, Paragraph, Bold, Italic ]
        }
    }
};

// Export the editor build.
export default Editor;

# HTML structure and initialization

The example below uses the “load & save” type of integration.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>CKEditor 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: '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',

            // 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(),
                            attributes: {
                                isImportant: true
                            }
                        },
                        {
                            commentId: 'comment-2',
                            authorId: 'user-2',
                            content: '<p>Why not?</p>',
                            createdAt: new Date()
                        }
                    ]
                }
            ],

            // Initial editor 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 is not that far from 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;
                }

                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: [ 'bold', 'italic', '|', 'comment', 'trackChanges' ]
                    }
                } )
                .then( editor => {
                    // After the editor is initialized, add an action to be performed after the 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
                        } );

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

# Live demo