Report an issue

guideIntegrating revision history with your application

This guide describes integrating the revision history feature as a standalone plugin.

The real-time collaboration integration is currently under development.

The revision history plugin provides an API for creating and managing named revisions of the document. To save and load the revisions from your database you need to provide a proper integration.

This guide will discuss two ways to integrate CKEditor 5 with revision history plugin:

The adapter integration is the recommended one for two reasons:

  • it gives you a better control over the data,
  • it is more efficient and provides a better user experience, as the revisions’ data is loaded on demand rather than upfront before the editor is initialized.

# Before you start

Comments is a commercial plugin and a license key is needed to authenticate. If you do not have one, please contact us. Let us know if you have any feedback or questions!

If you already have a valid license, please log into your user dashboard to access the plugin settings.

If you have more than one licence for CKSource products (comments, track changes, revision history or pagination), you may use any key of those generated in the dashboard.

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 revision history plugin, what data structure do the revisions use and what the plugin API looks like.

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

# Prepare a custom build

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

You can also easily create your own CKEditor 5 build with the revision history feature included using the CKEditor5 online builder.

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

npm install

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

npm install --save-dev @ckeditor/ckeditor5-revision-history

To make the revision history work, you need to import revision history 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 Table from '@ckeditor/ckeditor5-table/src/table';

import RevisionHistory from '@ckeditor/ckeditor5-revision-history/src/revisionhistory';

export default class ClassicEditor extends ClassicEditorBase {}

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

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

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

npm run build

Read more about this topic in the installing plugins guide.

# Core setup

The examples below implement the wide sidebar display mode to display comments and suggestions.

When you have the revision history package included in your custom build, prepare an HTML structure for the sidebar. After that you can enable the revision history plugin.

To browse through the saved revisions, the feature uses an additional editor instance on top of the original editor instance. To enable that, you will need to provide an HTML structure for both the original editor and the revision history editor. The HTML elements for the revision history editor structure should be hidden by default.

To have a proper HTML structure:

  • Add the layout for the document editor and the revision history editor.
  • Add the sidebar and revisionHistory configurations.
  • Add the revisionHistory 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 with revision history</title>
    <style type="text/css">
    .editor-container {
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        position: relative;
        width: 1260px;
    }

    .ck.ck-editor {
        max-width: 800px;
    }

    #revision-viewer-container {
        display: none;
    }

    .editor-container > .ck-editor {
        position: relative;
        width: 950px;
    }

    .editor-container .ck-editor__top .ck-toolbar {
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .editor-container .ck-editor__editable_inline {
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .sidebar-container {
        position: relative;
        width: 600px;
        overflow: hidden;
        background: var(--ck-color-toolbar-background);
        border: 1px solid var(--ck-color-toolbar-border);
        margin-left: -1px;
    }
    </style>
</head>
<body>

<div class="editors-holder">
    <!-- The original editor !-->
    <div id="editor-container">
        <div class="editor-container">
            <div id="editor"></div>
            <div class="sidebar-container" id="sidebar-container"></div>
        </div>
    </div>

    <!-- Structure for the revision viewer editor !-->
    <div id="revision-viewer-container">
        <div class="editor-container">
            <div id="revision-viewer-editor"></div>
            <div class="sidebar-container" id="revision-viewer-sidebar"></div>
        </div>
    </div>
</div>
<script src="../build/ckeditor.js"></script>
<script>
    ClassicEditor.create( document.querySelector( '#editor' ), {
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar-container' )
        },
        revisionHistory: {
            editorContainer: document.querySelector( '#editor-container' ),
            viewerContainer: document.querySelector( '#revision-viewer-container' ),
            viewerEditorElement: document.querySelector( '#revision-viewer-editor' ),
            viewerSidebarContainer: document.querySelector( '#revision-viewer-sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'revisionHistory' ]
        }
    } )
    .catch( error => console.error( error ) );
</script>

</body>
</html>

When you open the sample in the browser, you should see the WYSIWYG editor with the revision history plugin. However, it still does not load or save any revision data and the user is not defined. You will learn how to add those later in this guide.

# A simple “load and save” integration

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

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

# Loading the data

When the revision history plugin is already included in the editor, you need to create a plugin which will initialize users and existing revisions.

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

If your application needs to request the revisions data from the server asynchronously, you can create a plugin that will fetch the data from the database instead of putting the data in the HTML source. In such a 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 = {
    // Initial editor data
    data: `<figure class="image">
        <img src="../../assets/img/revision-history-demo.png">
    </figure>
    <h1>PUBLISHING AGREEMENT</h1>
    <h3>Introduction</h3>
    <p>This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”.</p>
    <h3>Grant of Rights</h3>
    <p>The Author grants the Publisher full right and title to the following, in perpetuity:</p>
    <ul>
        <li>To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future.</li>
        <li>To create or devise modified, abridged, or derivative works based on the works listed.</li>
        <li>To allow others to use the listed works at their discretion, without providing additional compensation to the Author.</li>
    </ul>
    <p>These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future.</p>
    <p>Any rights not granted to the Publisher above remain with the Author.</p>
    <p>The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.</p>`,

    // User data.
    user: {
        id: 'u1',
        name: 'Joe Doe',
        // Note that the avatar is optional.
        avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
    },

    // Initial revisions data
    revisions: [
        {
            "id": "e232bd41cc1d6ff75d3b8f100fc9a070b",
            "name": "Initial revision - template",
            "creatorId": "u1",
            "authorsIds": [ "u1" ],
            "data": {
                "main": {
                    "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]',
                    "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]'
                }
            },
            "createdAt": "2021-05-27T13:22:59.077Z",
            "attributes": {},
            "isLocked": true
        },
        {
            "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
            "name": "Updated with the actual data",
            "creatorId": "u1",
            "authorsIds": [ "u1" ],
            "data": {
                "main": {
                    "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:u1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:u1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:u1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:u1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:u1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                    "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:u1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:u1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:u1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:u1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:u1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:u1:3"]],"children":[]}]}]'
                }
            },
            "createdAt": "2021-05-27T13:23:52.553Z",
            "attributes": {},
            "isLocked": true
        },
        {
            "id": "e6590c50ccbc86acacb7d27231ad32064",
            "name": "Inserted logo",
            "creatorId": "u1",
            "authorsIds": [ "u1" ],
            "data": {
                "main": {
                    "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","../../assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                    "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:u1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
                }
            },
            "createdAt": "2021-05-27T13:26:39.252Z",
            "attributes": {},
            "isLocked": true
        }
    ]
};

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

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

    static get pluginName() {
        return 'RevisionHistoryIntegration';
    }

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

    init() {
        const revisionsRepositoryPlugin = this.editor.plugins.get( 'RevisionsRepository' );
        const revisions = appData.revisions || [];

        // Load revisions data
        for ( const revision of revisions ) {
            const newRevision = revisionsRepositoryPlugin.createRevision( revision );
            revisionsRepositoryPlugin.addRevision( newRevision );
        }
    }
}

// Initialize users here
class UsersInit {
    constructor( editor ) {
        this.editor = editor;
    }

    static get pluginName() {
        return 'UsersInit';
    }

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

    init() {
        const users = this.editor.plugins.get( 'Users' );

        users.addUser( appData.user );
        users.defineMe( appData.user.id );
    }
}

// In order to load comments and track changes,
// you should also create the comments and track changes integrations.

# Saving the data

Document data and revisions data have to be kept in sync for the feature to work correctly.

It is important that you always save revisions data and document data together.

To save the revisions data you need to get it from the RevisionsRepository plugin first. To do this, use the getRevisions() method.

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

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.data,
        extraPlugins: [ UsersInit, RevisionHistoryIntegration ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar-container' )
        },
        revisionHistory: {
            editorContainer: document.querySelector( '#editor-container' ),
            viewerContainer: document.querySelector( '#revision-viewer-container' ),
            viewerEditorElement: document.querySelector( '#revision-viewer-editor' ),
            viewerSidebarContainer: document.querySelector( '#revision-viewer-sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'revisionHistory' ]
        }
    } )
    .then( editor => {
        // After the editor is initialized, add an action to be performed after a button is clicked.
        document.querySelector( '#get-data' ).addEventListener( 'click', () => {
            const revisionsRepositoryPlugin = editor.plugins.get( 'RevisionsRepository' );

            // Get the data on demand.
            const editorData = editor.data.get();
            const revisionsData = revisionsRepositoryPlugin.getRevisions();

            // Now, use `editorData` and `revisionsData` to save the data in your application.
            console.log( editorData );
            console.log( revisionsData );
        } );
    } )
    .catch( error => console.error( error ) );

It is recommended to stringify the revisions’ attributes property value to JSON and to save it as a string in your database. Then parse the value from JSON when loading revisions.

# Full implementation

Below you can find the final implementation.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>CKEditor 5 with revision history</title>
    <style type="text/css">
    .editor-container {
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        position: relative;
        width: 1260px;
    }

    #revision-viewer-container {
        display: none;
    }

    .editor-container > .ck-editor {
        position: relative;
        width: 950px;
    }

    .editor-container .ck-editor__top .ck-toolbar {
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .editor-container .ck-editor__editable_inline {
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .sidebar-container {
        position: relative;
        width: 600px;
        overflow: hidden;
        background: var(--ck-color-toolbar-background);
        border: 1px solid var(--ck-color-toolbar-border);
        margin-left: -1px;
    }
    </style>
</head>
<body>
<button id="get-data">Get editor data</button>
<div class="editors-holder">
    <!-- The original editor !-->
    <div id="editor-container">
        <div class="editor-container">
            <div id="editor"></div>
            <div class="sidebar-container" id="sidebar-container"></div>
        </div>
    </div>

    <!-- Structure for the revision viewer editor !-->
    <div id="revision-viewer-container">
        <div class="editor-container">
            <div id="revision-viewer-editor"></div>
            <div class="sidebar-container" id="revision-viewer-sidebar"></div>
        </div>
    </div>
</div>
<script src="../build/ckeditor.js"></script>
<script>
    // Application data will be available under a global variable `appData`.
    const appData = {
        // Initial editor data
        data: `<figure class="image">
            <img src="../../assets/img/revision-history-demo.png">
        </figure>
        <h1>PUBLISHING AGREEMENT</h1>
        <h3>Introduction</h3>
        <p>This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”.</p>
        <h3>Grant of Rights</h3>
        <p>The Author grants the Publisher full right and title to the following, in perpetuity:</p>
        <ul>
            <li>To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future.</li>
            <li>To create or devise modified, abridged, or derivative works based on the works listed.</li>
            <li>To allow others to use the listed works at their discretion, without providing additional compensation to the Author.</li>
        </ul>
        <p>These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future.</p>
        <p>Any rights not granted to the Publisher above remain with the Author.</p>
        <p>The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.</p>`,

        // User data.
        user: {
            id: 'u1',
            name: 'Joe Doe',
            // Note that the avatar is optional.
            avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
        },

        // Initial revisions data
        revisions: [
            {
                "id": "e232bd41cc1d6ff75d3b8f100fc9a070b",
                "name": "Initial revision - template",
                "creatorId": "u1",
                "authorsIds": [ "u1" ],
                "data": {
                    "main": {
                        "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]',
                        "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]'
                    }
                },
                "createdAt": "2021-05-27T13:22:59.077Z",
                "attributes": {},
                "isLocked": true
            },
            {
                "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
                "name": "Updated with the actual data",
                "creatorId": "u1",
                "authorsIds": [ "u1" ],
                "data": {
                    "main": {
                        "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:u1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:u1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:u1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:u1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:u1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                        "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:u1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:u1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:u1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:u1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:u1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:u1:3"]],"children":[]}]}]'
                    }
                },
                "createdAt": "2021-05-27T13:23:52.553Z",
                "attributes": {},
                "isLocked": true
            },
            {
                "id": "e6590c50ccbc86acacb7d27231ad32064",
                "name": "Inserted logo",
                "creatorId": "u1",
                "authorsIds": [ "u1" ],
                "data": {
                    "main": {
                        "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","../../assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                        "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:u1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
                    }
                },
                "createdAt": "2021-05-27T13:26:39.252Z",
                "attributes": {},
                "isLocked": true
            }
        ]
    };

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

        static get pluginName() {
            return 'RevisionHistoryIntegration';
        }

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

        init() {
            const revisionsRepositoryPlugin = this.editor.plugins.get( 'RevisionsRepository' );
            const revisions = appData.revisions || [];

            // Load revisions data
            for ( const revision of revisions ) {
                const newRevision = revisionsRepositoryPlugin.createRevision( revision );
                revisionsRepositoryPlugin.addRevision( newRevision );
            }
        }
    }

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

        static get pluginName() {
            return 'UsersInit';
        }

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

        init() {
            const users = this.editor.plugins.get( 'Users' );

            users.addUser( appData.user );
            users.defineMe( appData.user.id );
        }
    }

    // In order to load comments and track changes,
    // you should also create the comments and track changes integrations.

    ClassicEditor
        .create( document.querySelector( '#editor' ), {
            initialData: appData.data,
            extraPlugins: [ UsersInit, RevisionHistoryIntegration ],
            licenseKey: 'your-license-key',
            sidebar: {
                container: document.querySelector( '#sidebar-container' )
            },
            revisionHistory: {
                editorContainer: document.querySelector( '#editor-container' ),
                viewerContainer: document.querySelector( '#revision-viewer-container' ),
                viewerEditorElement: document.querySelector( '#revision-viewer-editor' ),
                viewerSidebarContainer: document.querySelector( '#revision-viewer-sidebar' )
            },
            toolbar: {
                items: [ 'bold', 'italic', '|', 'revisionHistory' ]
            }
        } )
        .then( editor => {
            // After the editor is initialized, add an action to be performed after a button is clicked.
            document.querySelector( '#get-data' ).addEventListener( 'click', () => {

                const revisionsRepositoryPlugin = editor.plugins.get( 'RevisionsRepository' );

                // Get the data on demand.
                const editorData = editor.data.get();
                const revisionsData = revisionsRepositoryPlugin.getRevisions();

                // Now, use `editorData` and `revisionsData` to save the data in your application.
                console.log( editorData );
                console.log( revisionsData );
            } );
        } )
        .catch( error => console.error( error ) );
</script>

</body>
</html>

# Demo

Console:

// Use the `Save revisions` button to see the result...

# Adapter integration

An adapter integration uses an adapter object — provided by you — to immediately save revisions data in your data store.

This is a recommended way of integrating revision history with your application as 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.

Additionally, revisions may include significant amount of data. Loading multiple revisions of a big document may adversely impact the loading time of your application. When using an adapter, the revision data is loaded on demand, when needed, which improves the overall user experience.

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

# Implementation

First, define the adapter using the RevisionHistory#adapter property. The adapter methods allow you to load and save changes in your database.

Each change in revisions is performed immediately on the UI side. 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 the adapter is saving the revision’s 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 = {
    // Initial editor data
    data: `<figure class="image">
        <img src="../../assets/img/revision-history-demo.png">
    </figure>
    <h1>PUBLISHING AGREEMENT</h1>
    <h3>Introduction</h3>
    <p>This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”.</p>
    <h3>Grant of Rights</h3>
    <p>The Author grants the Publisher full right and title to the following, in perpetuity:</p>
    <ul>
        <li>To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future.</li>
        <li>To create or devise modified, abridged, or derivative works based on the works listed.</li>
        <li>To allow others to use the listed works at their discretion, without providing additional compensation to the Author.</li>
    </ul>
    <p>These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future.</p>
    <p>Any rights not granted to the Publisher above remain with the Author.</p>
    <p>The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.</p>`,

    // User data.
    user: {
        id: 'u1',
        name: 'Joe Doe',
        // Note that the avatar is optional.
        avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
    }
}

// Plugin that introduces the adapter.
class RevisionHistoryAdapter {
    constructor( editor ) {
        this.editor = editor;
    }

    static get pluginName() {
        return 'RevisionHistoryAdapter';
    }

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

    init() {
        const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );

        revisionHistory.adapter = {
            getRevisions: ( { channelId } ) => {
                // Get all revisions for the document with id `channelId`.
                // This should be an asynchronous request to your database.
                // Do not dump your data here -- this is only for testing purposes.
                return Promise.resolve( [
                    {
                        "id": "e232bd41cc1d6ff75d3b8f100fc9a070b",
                        "name": "Initial revision - template",
                        "creatorId": "u1",
                        "authorsIds": [ "u1" ],
                        "createdAt": "2021-05-27T13:22:59.077Z",
                        "attributes": {},
                        "isLocked": true
                    },
                    {
                        "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
                        "name": "Updated with the actual data",
                        "creatorId": "u1",
                        "authorsIds": [ "u1" ],
                        "createdAt": "2021-05-27T13:23:52.553Z",
                        "attributes": {},
                        "isLocked": true
                    },
                    {
                        "id": "e6590c50ccbc86acacb7d27231ad32064",
                        "name": "Inserted logo",
                        "creatorId": "u1",
                        "authorsIds": [ "u1" ],
                        "createdAt": "2021-05-27T13:26:39.252Z",
                        "attributes": {},
                        "isLocked": true
                    }
                ] );
            },
            getRevision: ( { revisionId } ) => {
                // Get revision data, based on its id.
                // This should be an asynchronous request to your database.
                // Do not dump your revisions data here -- this is only for testing purposes.
                switch ( revisionId ) {
                    case 'e232bd41cc1d6ff75d3b8f100fc9a070b':
                        return Promise.resolve(
                            {
                                "id": "e232bd41cc1d6ff75d3b8f100fc9a070b",
                                "name": "Initial revision - template",
                                "creatorId": "u1",
                                "authorsIds": [ "u1" ],
                                "data": {
                                    "main": {
                                        "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]',
                                        "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]'
                                    }
                                },
                                "createdAt": "2021-05-27T13:22:59.077Z",
                                "attributes": {},
                                "isLocked": true
                            }
                        );
                    case 'e6f80e6be6ee6057fd5a449ab13fba25d':
                        return Promise.resolve(
                            {
                                "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
                                "name": "Updated with the actual data",
                                "creatorId": "u1",
                                "authorsIds": [ "u1" ],
                                "data": {
                                    "main": {
                                        "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:u1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:u1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:u1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:u1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:u1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                                        "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:u1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:u1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:u1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:u1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:u1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:u1:3"]],"children":[]}]}]'
                                    }
                                },
                                "createdAt": "2021-05-27T13:23:52.553Z",
                                "attributes": {},
                                "isLocked": true
                            }
                        );
                    case 'e6590c50ccbc86acacb7d27231ad32064':
                        return Promise.resolve(
                            {
                                "id": "e6590c50ccbc86acacb7d27231ad32064",
                                "name": "Inserted logo",
                                "creatorId": "u1",
                                "authorsIds": [ "u1" ],
                                "data": {
                                    "main": {
                                        "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","../../assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                                        "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:u1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
                                    }
                                },
                                "createdAt": "2021-05-27T13:26:39.252Z",
                                "attributes": {},
                                "isLocked": true
                            }
                        );
                }
            },
            updateRevision: revisionData => {
                let documentData = null;

                if ( revisionData.data ) {
                    // Changes saved in the revision are updated.
                    // Keep the document data in sync and save it as well.
                    documentData = this.editor.getData();
                }

                // This should be an asynchronous request to your database
                // which saves the updated revision data (`revisionData`) and current document data (if needed).
                return Promise.resolve();
            },
            addRevision: revisionData => {
                const documentData = this.editor.getData();

                // This should be an asynchronous request to your database
                // which saves the added revision data (`revisionData`) and current document data.
                return Promise.resolve();
            }
        };
    }
}

// Initialize the users here
class UsersInit {
    constructor( editor ) {
        this.editor = editor;
    }

    static get pluginName() {
        return 'UsersInit';
    }

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

    init() {
        const users = this.editor.plugins.get( 'Users' );

        users.addUser( appData.user );
        users.defineMe( appData.user.id );
    }
}

// In order to load comments and track changes,
// you should also integrate the comments and track changes adapters.

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        initialData: appData.data,
        extraPlugins: [ UsersInit, RevisionHistoryAdapter ],
        licenseKey: 'your-license-key',
        sidebar: {
            container: document.querySelector( '#sidebar-container' )
        },
        revisionHistory: {
            editorContainer: document.querySelector( '#editor-container' ),
            viewerContainer: document.querySelector( '#revision-viewer-container' ),
            viewerEditorElement: document.querySelector( '#revision-viewer-editor' ),
            viewerSidebarContainer: document.querySelector( '#revision-viewer-sidebar' )
        },
        toolbar: {
            items: [ 'bold', 'italic', '|', 'revisionHistory' ]
        }
    } )
    .catch( error => console.error( error ) );

It is recommended to stringify the revisions’ attributes property value to JSON and to save it as a string in your database. Then parse the value from JSON when loading revisions.

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

# Full implementation

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>CKEditor 5 with revision history</title>
    <style type="text/css">
    .editor-container {
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        position: relative;
        width: 1260px;
    }

    #revision-viewer-container {
        display: none;
    }

    .editor-container > .ck-editor {
        position: relative;
        width: 950px;
    }

    .editor-container .ck-editor__top .ck-toolbar {
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .editor-container .ck-editor__editable_inline {
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .sidebar-container {
        position: relative;
        width: 600px;
        overflow: hidden;
        background: var(--ck-color-toolbar-background);
        border: 1px solid var(--ck-color-toolbar-border);
        margin-left: -1px;
    }
    </style>
</head>
<body>

<div class="editors-holder">
    <!-- The original editor !-->
    <div id="editor-container">
        <div class="editor-container">
            <div id="editor"></div>
            <div class="sidebar-container" id="sidebar-container"></div>
        </div>
    </div>

    <!-- Structure for the revision viewer editor !-->
    <div id="revision-viewer-container">
        <div class="editor-container">
            <div id="revision-viewer-editor"></div>
            <div class="sidebar-container" id="revision-viewer-sidebar"></div>
        </div>
    </div>
</div>
<script src="../build/ckeditor.js"></script>
<script>
    // Application data will be available under a global variable `appData`.
    const appData = {
        // Initial editor data
        data: `<figure class="image">
            <img src="../../assets/img/revision-history-demo.png">
        </figure>
        <h1>PUBLISHING AGREEMENT</h1>
        <h3>Introduction</h3>
        <p>This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”.</p>
        <h3>Grant of Rights</h3>
        <p>The Author grants the Publisher full right and title to the following, in perpetuity:</p>
        <ul>
            <li>To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future.</li>
            <li>To create or devise modified, abridged, or derivative works based on the works listed.</li>
            <li>To allow others to use the listed works at their discretion, without providing additional compensation to the Author.</li>
        </ul>
        <p>These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future.</p>
        <p>Any rights not granted to the Publisher above remain with the Author.</p>
        <p>The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.</p>`,

        // User data.
        user: {
            id: 'u1',
            name: 'Joe Doe',
            // Note that the avatar is optional.
            avatar: 'https://randomuser.me/api/portraits/thumb/men/26.jpg'
        }
    }

    // Plugin that introduces the adapter.
    class RevisionHistoryAdapter {
        constructor( editor ) {
            this.editor = editor;
        }

        static get pluginName() {
            return 'RevisionHistoryAdapter';
        }

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

        init() {
            const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );

            revisionHistory.adapter = {
                getRevisions: ( { channelId } ) => {
                    // Get all revisions for the document with id `channelId`.
                    // This should be an asynchronous request to your database.
                    // Do not dump your data here -- this is only for testing purposes.
                    return Promise.resolve( [
                        {
                            "id": "e232bd41cc1d6ff75d3b8f100fc9a070b",
                            "name": "Initial revision - template",
                            "creatorId": "u1",
                            "authorsIds": [ "u1" ],
                            "createdAt": "2021-05-27T13:22:59.077Z",
                            "attributes": {},
                            "isLocked": true
                        },
                        {
                            "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
                            "name": "Updated with the actual data",
                            "creatorId": "u1",
                            "authorsIds": [ "u1" ],
                            "createdAt": "2021-05-27T13:23:52.553Z",
                            "attributes": {},
                            "isLocked": true
                        },
                        {
                            "id": "e6590c50ccbc86acacb7d27231ad32064",
                            "name": "Inserted logo",
                            "creatorId": "u1",
                            "authorsIds": [ "u1" ],
                            "createdAt": "2021-05-27T13:26:39.252Z",
                            "attributes": {},
                            "isLocked": true
                        }
                    ] );
                },
                getRevision: ( { revisionId } ) => {
                    // Get revision data, based on its id.
                    // This should be an asynchronous request to your database.
                    // Do not dump your revisions data here -- this is only for testing purposes.
                    switch ( revisionId ) {
                        case 'e232bd41cc1d6ff75d3b8f100fc9a070b':
                            return Promise.resolve(
                                    {
                                        "id": "e232bd41cc1d6ff75d3b8f100fc9a070b",
                                        "name": "Initial revision - template",
                                        "creatorId": "u1",
                                        "authorsIds": [ "u1" ],
                                        "data": {
                                            "main": {
                                                "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]',
                                                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]'
                                            }
                                        },
                                        "createdAt": "2021-05-27T13:22:59.077Z",
                                        "attributes": {},
                                        "isLocked": true
                                    }
                            );
                        case 'e6f80e6be6ee6057fd5a449ab13fba25d':
                            return Promise.resolve(
                                    {
                                        "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
                                        "name": "Updated with the actual data",
                                        "creatorId": "u1",
                                        "authorsIds": [ "u1" ],
                                        "data": {
                                            "main": {
                                                "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:u1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:u1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:u1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:u1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:u1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                                                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:u1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:u1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:u1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:u1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:u1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:u1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:u1:3"]],"children":[]}]}]'
                                            }
                                        },
                                        "createdAt": "2021-05-27T13:23:52.553Z",
                                        "attributes": {},
                                        "isLocked": true
                                    }
                            );
                        case 'e6590c50ccbc86acacb7d27231ad32064':
                            return Promise.resolve(
                                    {
                                        "id": "e6590c50ccbc86acacb7d27231ad32064",
                                        "name": "Inserted logo",
                                        "creatorId": "u1",
                                        "authorsIds": [ "u1" ],
                                        "data": {
                                            "main": {
                                                "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","../../assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                                                "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:u1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:u1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
                                            }
                                        },
                                        "createdAt": "2021-05-27T13:26:39.252Z",
                                        "attributes": {},
                                        "isLocked": true
                                    }
                            );
                    }
                },
                updateRevision: revisionData => {
                    let documentData = null;

                    if ( revisionData.data ) {
                        // Changes saved in the revision are updated.
                        // Keep the document data in sync and save it as well.
                        documentData = this.editor.getData();
                    }

                    // This should be an asynchronous request to your database
                    // which saves the updated revision data (`revisionData`) and current document data (if needed).
                    return Promise.resolve();
                },
                addRevision: revisionData => {
                    const documentData = this.editor.getData();

                    // This should be an asynchronous request to your database
                    // which saves the added revision data (`revisionData`) and current document data.
                    return Promise.resolve();
                }
            };
        }
    }

    // Initialize the users here
    class UsersInit {
        constructor( editor ) {
            this.editor = editor;
        }

        static get pluginName() {
            return 'UsersInit';
        }

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

        init() {
            const users = this.editor.plugins.get( 'Users' );

            users.addUser( appData.user );
            users.defineMe( appData.user.id );
        }
    }

    // In order to load comments and track changes,
    // you should also integrate the comments and track changes adapters.

    ClassicEditor
        .create( document.querySelector( '#editor' ), {
            initialData: appData.data,
            extraPlugins: [ UsersInit, RevisionHistoryAdapter ],
            licenseKey: 'your-license-key',
            sidebar: {
                container: document.querySelector( '#sidebar-container' )
            },
            revisionHistory: {
                editorContainer: document.querySelector( '#editor-container' ),
                viewerContainer: document.querySelector( '#revision-viewer-container' ),
                viewerEditorElement: document.querySelector( '#revision-viewer-editor' ),
                viewerSidebarContainer: document.querySelector( '#revision-viewer-sidebar' )
            },
            toolbar: {
                items: [ 'bold', 'italic', '|', 'revisionHistory' ]
            }
        } )
        .catch( error => console.error( error ) );
</script>

</body>
</html>

# Demo

Revision history adapter actions console:

// Create new version to see the result...

# Pending actions

Revision history uses pending actions in two ways:

  1. Pending actions are added when revisions data is being updated through the revisions adapter, to prevent closing the editor before the update finishes.
  2. It is impossible to save a revision or open revision history view when there is a pending action (for example, when autosave is processed, or when an image uploads). This behavior may change in the future.

# Autosave integration

Below section describes how to correctly integrate revision history with the autosave plugin. This way you can frequently save both your document data and your revisions data, and keep them in sync.

Although this guide provides ready-to-use snippets, we encourage you to also read How revisions are saved and updated section to get a better understanding of this subject.

If the autosave plugin is enabled, the autosave callback is called when revision history view is opened.

The integration differs a bit whether you use adapter or not.

# Autosave and “load & save” integration

Update the revision and make sure that the updated or created revision is saved together with the editor data:

autosave: {
    save: editor => {
        const revisionTracker = editor.plugins.get( 'RevisionTracker' );
        const revision = revisionTracker.updateRevision();
        const documentData = editor.getData();

        // `saveData()` should save document and revision data in your database
        // and return a `Promise` that resolves when the save is completed.
        return saveData( documentData, revision );
    }
}

# Autosave and adapter integration

Integration when using the adapter is easier, because your revision adapter should save the document data as well, as was already shown in the earlier examples.

Since the adapter already takes care of saving the document data, all that needs to be done in the autosave integration is updating the revision:

autosave: {
    save: editor => {
        editor.plugins.get( 'RevisionTracker' ).updateRevision();

        return Promise.resolve();
    }
}

# Advanced autosave strategies

Presented integrations will simply keep on updating the same revision until the user explicitly saves or names current revision.

This may result in creating a very big revision, containing a lot of changes. To prevent that, autosave integration could lock revisions basing on your custom strategy.

For example, you may decide to lock current revision after chosen number of autosave callbacks since the last locked revision:

// Create a new plugin that will handle the autosave logic.
class RevisionHistoryAutosaveIntegration extends Plugin {
    init() {
        this.lockAfter = 100;
        this.autosaveCount = 0;
    }

    autosave() {
        const revisionTracker = this.editor.plugins.get( 'RevisionTracker' );

        if ( revisionTracker.isLocked ) {
            // Revision could get locked last time the autosave was called, or
            // the user might have saved or named a revision,
            this.autosaveCount = 0;
        }

        const isLocked = this.autosaveCount === this.lockAfter;

        revisionTracker.updateRevision( { isLocked } );
        // Get the editor data and perform call to your database
        // if you are using "load & save" integration.

        this.autosaveCount++;

        return Promise.resolve();
    }
}

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        extraPlugins: [
            // ...
            // Add the new plugin to the editor configuration:
            RevisionHistoryAutosaveIntegration
        ],
        // ...
        // Add autosave configuration -- call the plugin method:
        autosave: {
            save: editor => {
                return editor.plugins.get( RevisionHistoryAutosaveIntegration ).autosave();
            }
        }
    } )
    .catch( error => console.error( error ) );

Similarly, you can implement a locking strategy that would include time since the last locked revision, number of operations, or multiple variables used together to decide if a revision should be locked.

# How revisions are saved and updated

Understanding revision lifecycle is important when it comes to writing custom code that integrates with revision history, including autosave integration.

A revision is updated (or a new revision is created) in following scenarios:

  • A user saves a revision using the dropdown in the toolbar.
  • A user opens revision history view when there are yet unsaved changes in the document.
  • A user names a revision when the revision history view is open, using the sidebar.

You can also use the feature API to update a revision:

const revisionTrackerPlugin = this.editor.plugins.get( 'RevisionTracker' );

revisionTrackerPlugin.updateRevision();

# Unlocked and locked revisions

Before moving forward, it is important to understand differences between unlocked and locked revisions.

As long as the revision is unlocked, it can be updated to include the latest document changes. It can be seen as “work in progress”. Since unlocked revision can be updated, it should be used for autosave purposes, so that frequent saves do not create hundreds of revisions. When a revision is updated with new document changes, its creation date is updated as well. Keep in mind that unlocked revision cannot have a name and only the most recent revision can be unlocked.

On the other hand, locked revisions are considered “complete”. Revision becomes locked when it is explicitly saved by a user (through UI) or when it becomes named. After a revision is locked, new document changes are saved as a new revision.

You can decide whether the current revision should become locked or not by passing isLocked parameter in updateRevision() call:

// Assuming the editor just initialized, a new revision will be created.
// By default (if no parameters are passed), the revision is not locked.
revisionTracker.updateRevision();

// Update current revision with the latest document changes.
revisionTracker.updateRevision();

// Update current revision with the latest document changes and lock it.
revisionTracker.updateRevision( { isLocked: true } );
// Keep in mind that at this point, there is still only one revision.

// Since the most recent revision is locked,
// recent document changes will be saved as a new revision.
revisionTracker.updateRevision();
// At this point, there are two revisions in the editor.

// Update current revision with the lastest document changes.
// Giving a name to a revision automatically locks it.
revisionTracker.updateRevision( { name: 'Revision name' } );
// There are still two revisions in the editor,
// both are locked and the most recent one is named.

Lastly, revisions loaded when the editor is initialized become locked.

# Revision creation and update flow

Complementary, the following chart shows how revisions are created and updated.

Revision creation and update flow chart

The chart assumes that the revisions and document data is saved in a database whenever a revision is created or updated. If you use “load & save” integration without an autosave plugin, you can lose your data due to editor closing unexpectedly at any point.

  • Gray boxes are locked revisions.
  • White boxes are unlocked revisions.
  • Revisions without a name display creation/update date.
  • “Recorded changes” is internal revision history data.
  • “Editor closes unexpectedly” is e.g. power outage.