Integrating revision history with your application
This guide describes integrating the revision history feature as a standalone plugin.
If you are using the real-time collaboration feature, refer to the Real-time collaboration features integration guide.
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:
- A simple “load and save” integration using the
RevisionHistory
plugin API. - An adapter integration which saves the revision data immediately in the database.
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
Revision history 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 Autoformat from '@ckeditor/ckeditor5-autoformat/src/autoformat';
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',
// Provide the configuration for the comments feature.
comments: {
editorConfig: {
// The list of plugins that will be included in the comments editors.
extraPlugins: [ Bold, Italic, Autoformat ]
}
}
};
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 editor sidebar and revision history. 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 set up the feature, you need to:
- Add the HTML layout for the document editor and the revision history editor.
- Add the
sidebar
andrevisionHistory
configurations. - Add the
revisionHistory
dropdown to thetoolbar
. - 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.
Also, see an example in the adapter integration below.
// Application data will be available under a global variable `appData`.
const appData = {
// Initial editor data
data: `<figure class="image">
<img src="https://ckeditor.com/docs/ckeditor5/latest/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": "initial",
"name": "Initial revision",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 1
},
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 20
},
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/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": {},
"fromVersion": 20,
"toVersion": 24
},
// Empty current revision:
{
"id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
"name": null,
"creatorId": null,
"authorsIds": [],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"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 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":[],"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": {},
"fromVersion": 24,
"toVersion": 24
}
]
};
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 revisionHistory = this.editor.plugins.get( 'RevisionHistory' );
for ( const revisionData of appData.revisions ) {
revisionHistory.addRevisionData( revisionData );
}
}
}
// 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 (remember to update your HTML structure to contain a button with get-data
id, e.g. <button id="get-data">Get editor data</button>
).
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 revisionHistory = editor.plugins.get( 'RevisionHistory' );
// Get the document data and the revisions data (in JSON format, so it is easier to save).
const editorData = editor.data.get();
const revisionsData = revisionHistory.getRevisions( { toJSON: true } );
// Now, use `editorData` and `revisionsData` to save the data in your application.
//
// Note: it is a good idea to verify the revision `creatorId` parameter when saving
// a revision in the database, however, do not overwrite the value if it was set to `null`!
//
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="https://ckeditor.com/docs/ckeditor5/latest/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": "initial",
"name": "Initial revision",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 1
},
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 20
},
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/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": {},
"fromVersion": 20,
"toVersion": 24
},
// Empty current revision:
{
"id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
"name": null,
"creatorId": null,
"authorsIds": [],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"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 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":[],"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": {},
"fromVersion": 24,
"toVersion": 24
}
]
};
class RevisionHistoryIntegration {
constructor( editor ) {
this.editor = editor;
}
static get pluginName() {
return 'RevisionHistoryIntegration';
}
static get requires() {
return [ 'RevisionHistory' ];
}
init() {
const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );
for ( const revisionData of appData.revisions ) {
revisionHistory.addRevisionData( revisionData );
}
}
}
// 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.
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 revisionHistory = editor.plugins.get( 'RevisionHistory' );
// Get the document data and the revisions data (in JSON format, so it is easier to save).
const editorData = editor.data.get();
const revisionsData = revisionHistory.getRevisions( { toJSON: true } );
// Now, use `editorData` and `revisionsData` to save the data in your application.
//
// Note: it is a good idea to verify the revision `creatorId` parameter when saving
// a revision in the database, however, do not overwrite the value if it was set to `null`!
//
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.
# Saving document data and revisions data
Before we move to the actual implementation it is important to note that document data and revisions data should be always kept in sync. This means that whenever one is saved, the other should be saved as well. This is natural for “load & save” integration but for adapter integration, we need to keep that in mind. Mismatch in the data will result in the feature not working correctly.
Additionally, document data should not be further post-processed after it is saved. To be precise, it should not be changed in a way that would result in a different model, after the document data is loaded.
# Implementation
First, define the adapter using the RevisionHistory#adapter
property. The adapter methods allow you to load and save changes in your database. Read the API reference for RevisionHistoryAdapter
carefully to make sure that you integrate the feature with your application correctly.
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="https://ckeditor.com/docs/ckeditor5/latest/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'
},
// The ID of the current user.
userId: 'u1'
}
// Plugin that introduces the adapter.
class RevisionHistoryAdapter {
constructor( editor ) {
this.editor = editor;
}
static get pluginName() {
return 'RevisionHistoryAdapter';
}
static get requires() {
return [ 'RevisionHistory' ];
}
async init() {
const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );
revisionHistory.adapter = {
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 'initial':
return Promise.resolve(
{
"id": "initial",
"name": "Initial revision",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 1
}
);
case 'e6f80e6be6ee6057fd5a449ab13fba25d':
return Promise.resolve(
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 20
}
);
case 'e6590c50ccbc86acacb7d27231ad32064':
return Promise.resolve(
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/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": {},
"fromVersion": 20,
"toVersion": 24
}
);
case 'egh91t5jccbi894cacxx7dz7t36aj3k021':
return Promise.resolve(
{
"id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
"name": null,
"creatorId": null,
"authorsIds": [],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"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 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":[],"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": {},
"fromVersion": 24,
"toVersion": 24
}
);
}
},
updateRevisions: revisionsData => {
const documentData = this.editor.getData();
// This should be an asynchronous request to your database
// that saves `revisionsData` and `documentData`.
//
// Document data should be saved each time a revision is saved.
//
// `revisionsData` is an array with objects,
// where each object contains updated and new revisions.
//
// See the API reference for `RevisionHistoryAdapter` to learn
// how to correctly integrate the feature with your application.
//
return Promise.resolve();
}
};
// Add the revisions data for existing revisions.
// You can either dump the revisions data straight in the source code, or
// you can fetch the data asynchronously from your database (as this example shows).
//
// Note that the revisions data does not contain `diffData` property.
// `diffData` property may be big and will be fetched on demand by `adapter.getRevision()`.
//
const revisionsData = await this._fetchRevisionsData();
for ( const revisionData of revisionsData ) {
revisionHistory.addRevisionData( revisionData );
}
}
async _fetchRevisionsData() {
// Make an asynchronous call to your database.
return Promise.resolve(
[
{
"id": "initial",
"name": "Initial revision",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"createdAt": "2021-05-27T13:22:59.077Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 1
},
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"createdAt": "2021-05-27T13:23:52.553Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 20
},
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"createdAt": "2021-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 20,
"toVersion": 24
},
// Empty current revision:
{
"id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
"name": null,
"creatorId": null,
"authorsIds": [],
"createdAt": "2021-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 24,
"toVersion": 24
}
]
);
}
}
// 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.userId );
}
}
// 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="https://ckeditor.com/docs/ckeditor5/latest/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'
},
// The ID of the current user.
userId: 'u1'
}
// Plugin that introduces the adapter.
class RevisionHistoryAdapter {
constructor( editor ) {
this.editor = editor;
}
static get pluginName() {
return 'RevisionHistoryAdapter';
}
static get requires() {
return [ 'RevisionHistory' ];
}
async init() {
const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );
revisionHistory.adapter = {
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 'initial':
return Promise.resolve(
{
"id": "initial",
"name": "Initial revision",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 1
}
);
case 'e6f80e6be6ee6057fd5a449ab13fba25d':
return Promise.resolve(
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"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": {},
"fromVersion": 1,
"toVersion": 20
}
);
case 'e6590c50ccbc86acacb7d27231ad32064':
return Promise.resolve(
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:u1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/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": {},
"fromVersion": 20,
"toVersion": 24
}
);
case 'egh91t5jccbi894cacxx7dz7t36aj3k021':
return Promise.resolve(
{
"id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
"name": null,
"creatorId": null,
"authorsIds": [],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.com/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"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 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":[],"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": {},
"fromVersion": 24,
"toVersion": 24
}
);
}
},
updateRevisions: revisionsData => {
const documentData = this.editor.getData();
// This should be an asynchronous request to your database
// that saves `revisionsData` and `documentData`.
//
// Document data should be saved each time a revision is saved.
//
// `revisionsData` is an array with objects,
// where each object contains updated and new revisions.
//
// See the API reference for `RevisionHistoryAdapter` to learn
// how to correctly integrate the feature with your application.
//
return Promise.resolve();
}
};
// Add the revisions data for existing revisions.
// You can either dump the revisions data straight in the source code, or
// you can fetch the data asynchronously from your database (as this example shows).
//
// Note that the revisions data does not contain `diffData` property.
// `diffData` property may be big and will be fetched on demand by `adapter.getRevision()`.
//
const revisionsData = await this._fetchRevisionsData();
for ( const revisionData of revisionsData ) {
revisionHistory.addRevisionData( revisionData );
}
}
async _fetchRevisionsData() {
// Make an asynchronous call to your database.
return Promise.resolve(
[
{
"id": "initial",
"name": "Initial revision",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"createdAt": "2021-05-27T13:22:59.077Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 1
},
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"createdAt": "2021-05-27T13:23:52.553Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 20
},
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "u1",
"authorsIds": [ "u1" ],
"createdAt": "2021-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 20,
"toVersion": 24
},
// Empty current revision:
{
"id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
"name": null,
"creatorId": null,
"authorsIds": [],
"createdAt": "2021-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 24,
"toVersion": 24
}
]
);
}
}
// 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.userId );
}
}
// 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...
# Learn more
# Pending actions
Revision history uses pending actions feature. Pending actions are added when revisions data is being updated through the revision history adapter, to prevent closing the editor before the update finishes.
# Autosave integration
If you are using the real-time collaboration feature, refer to the Autosave for revision history section in the Real-time collaboration features integration guide.
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 additionally called when the 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: async editor => {
const revisionTracker = editor.plugins.get( 'RevisionTracker' );
await revisionTracker.update();
const revisionData = revisionTracker.currentRevision.toJSON();
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, revisionData );
}
}
# 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 both revision data and the document data, all that needs to be done in the autosave integration is to update the revision:
autosave: {
save: editor => {
const revisionTracker = editor.plugins.get( 'RevisionTracker' );
return revisionTracker.update();
}
}
# Advanced autosave strategies
Presented integrations will simply keep on updating the same revision until the user explicitly saves or names the current revision or closes the editor.
This may result in creating a very big revision, containing a lot of changes. To prevent that, autosave integration could create new revisions basing on your custom strategy.
For example, you may decide to save the current revision (unsaved changes) after chosen number of autosave callbacks since the last saved revision:
// Create a new plugin that will handle the autosave logic.
class RevisionHistoryAutosaveIntegration extends Plugin {
init() {
this._saveAfter = 100; // Create a new revision after 100 saves.
this._autosaveCount = 1; // Current autosave counter.
this._lastCreatedAt = null; // Revision `createdAt` value, when the revision was last time autosaved.
}
async autosave() {
const revisionTracker = this.editor.plugins.get( 'RevisionTracker' );
const currentRevision = revisionTracker.currentRevision;
if ( currentRevision.createdAt > this._lastCreatedAt ) {
// Revision was saved or updated in the meantime by a different source (not autosave).
// Reset the counter.
this._autosaveCount = 1;
}
if ( this._autosaveCount === this._saveAfter ) {
// We reached the count, save all changes as a new revision. Reset the counter.
await revisionTracker.saveRevision();
this._autosaveCount = 1;
this._lastCreatedAt = currentRevision.createdAt;
} else {
// Try updating "current revision" with the new document changes.
// If there are any new changes, `createdAt` property will change it's value.
// Don't raise the counter, if the revision has not been updated!
await revisionTracker.update();
if ( currentRevision.createdAt > this._lastCreatedAt ) {
this._autosaveCount++;
this._lastCreatedAt = currentRevision.createdAt;
}
}
return true;
}
}
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 saving strategy that would include time since the last saved revision, number of operations, or multiple variables used together to decide if a new revision should be saved.
# How revisions are updated and saved
Understanding how and when revisions are updated and saved is important when it comes to writing custom code that integrates with revision history, including autosave integration.
There are always at least two revisions available for a document: the initial revision and the current revision. If the document is new and no revisions has been created for it yet, the two revisions are created when the editor is initialized.
The initial revision contains the editor data from when the document was initialized for the first time. It can be empty or contain some content. The initial revision’s id will equal to document id or to 'initial'
if the document id is not specified.
The current revision is a revision that stores all unsaved document changes, i.e. changes that has not been saved in earlier revisions. It is always on top of the revisions list. If a new revision is created it will contain all unsaved changes and will be added below the current revision. Then, the current revision will be empty, until again updated with new, unsaved document changes. An empty current revision is not shown on the revisions list.
Note, that the current revision is not updated automatically when document is changed. The update can be done using the revisions feature API. It is enough to update the current revision only when you need to save it – e.g. in the autosave callback. The update is also triggered when the revision history view is opened. In this case, either autosave callback is called, or the current revision is updated (if autosave plugin is not used).
A new revision can be saved using the revisions feature API. Also, a new revision will be created when:
- A user saves a revision using the dropdown in the editor toolbar.
- A user gives a name to the current revision (in the revision history view).
- Each time the editor is initialized (a new current revision will be created, while the old current revision will become a regular revision).
# Save or update revision using the feature API
const revisionTrackerPlugin = this.editor.plugins.get( 'RevisionTracker' );
// Updates "current revision", i.e. the revision containing unsaved changes.
revisionTrackerPlugin.update();
// Creates a new revision that will contain all the unsaved changes.
// See API reference to learn more.
revisionTrackerPlugin.saveRevision();
revisionTrackerPlugin.saveRevision( { name: 'My revision' } );
Every day, we work hard to keep our documentation complete. Have you spotted an outdated information? Is something missing? Please report it via our issue tracker.