Integrating revision history with your application
The revision history plugin provides an API that lets you create and manage named revisions of your document. To save and access revisions in your database, you first need to integrate this feature.
This guide describes integrating the revision history feature as a standalone (asynchronous) plugin.
If you are using the real-time collaboration feature, refer to the Real-time collaboration features integration guide.
# Integration methods
This guide will discuss two ways to integrate CKEditor 5 with the 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 approach for two reasons:
- It gives you 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
Complementary to this guide, we provide ready-to-use samples available for download. You may use the samples as an example or as a starting point for your own integration.
# Preparing a custom editor setup
To use the revision history plugin, you need to prepare a custom editor setup with the asynchronous version of the revision history feature included.
The easiest way to do that is by using the Builder. Pick a preset and start customizing your editor.
The Builder allows you to pick your preferred distribution method and framework. For this guide, we will use the “Vanilla JS” option with “npm” and a simple setup based on the “Classic Editor (basic)” preset, with the revision history feature enabled.
In the “Features” section of the Builder (2nd step), make sure to:
- enable the “Collaboration → Revision History” feature,
- turn off the “real-time” toggle next to the “Collaboration” group.
For better demo experience, we suggest to enable additional features - entire “Text Formatting” group and “Images → Block Images” feature.
Once you finish the setup, the Builder will provide you with the necessary HTML, CSS, and JavaScript code snippets. We will use those code snippets in the next step.
# Setting up a sample project
Once we have a custom editor setup we need a simple JavaScript project to run it. For this, we recommend cloning the basic project template from our repository:
npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project
cd sample-project
npm install
Then, install the necessary dependencies:
npm install ckeditor5
npm install ckeditor5-premium-features
This project template uses Vite under the hood and contains 3 source files that we will use: index.html
, style.css
, and main.js
.
It is now the time to use our custom editor setup. Go to the “Installation” section of the Builder and copy the generated code snippets to those 3 files.
# Activating the feature
To use this premium feature, you need to activate it with a license key. Refer to the License key and activation guide for details.
After you have successfully obtained the license key open the index.js
file and update the your-license-key
string with your license key.
# Building the project
Finally, build the project by running:
npm run dev
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 data. You will learn how to add data to the comments plugin later in this guide.
Let’s now dive deeper into the structure of this setup.
# Basic setup’s anatomy
Let’s now go through the key fragments of this basic setup.
# HTML structure
The HTML and CSS structure of the page creates two main containers:
<div class="editor-container__editor">
is the main container used by the editor.<div class="revision-history">
is the main container used by revision history.<div class="revision-history__editor">
is the container used by revision history that shows selected revision content.<div class="revision-history__sidebar">
is the container used by revision history sidebar that holds revision list.
# JavaScript
The main.js
file sets up the editor instance:
- Adds the
revisionHistory
button to the editor toolbar. - Loads all necessary editor plugins (including the
RevisionHistory
plugin). - Sets the
licenseKey
configuration option. - Sets the
revisionHistory
configuration option to point to containers mentioned above. - Defines the templates for the
RevisionHistoryIntegration
andUsersIntegrations
plugins that we will use in the next steps of this tutorial.
# Revision history API
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.
# Next steps
We have set up a simple JavaScript project that runs a basic CKEditor instance with the asynchronous version of the Revision history feature. It does not yet handle loading or saving data, though. The next two sections cover the two available integration methods.
# A simple “load and save” integration
In this solution, you load the user and revision data during the editor initialization. You save the revision data after you finish working with the editor (for example, when you submit the form containing the WYSIWYG editor).
# Loading the data
After you include the revision history plugin in the editor, you need to create a plugin that will initialize users and existing revisions.
You should load the revisions data during the plugin initialization step, that is, using the init()
method of the integration plugin that you will provide (as in the example below).
First, store the users and the revisions data into a variable that will be available for your plugin.
// Revisions data will be available under a global variable `revisions`.
const revisions = [
{
"id": "initial",
"name": "Initial revision",
"creatorId": "user-1",
"authorsIds": [ "user-1" ],
"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": "2024-05-27T13:22:59.077Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 1
},
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "user-1",
"authorsIds": [ "user-1" ],
"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:user-1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:user-1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:user-1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:user-1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:user-1: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:user-1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:user-1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1: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:user-1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:user-1: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:user-1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:user-1:3"]],"children":[]}]}]'
}
},
"createdAt": "2024-05-27T13:23:52.553Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 20
},
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "user-1",
"authorsIds": [ "user-1" ],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:user-1: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:user-1: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:user-1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:user-1: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": "2024-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 20,
"toVersion": 24
},
// An 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": "2024-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 24,
"toVersion": 24
}
];
Then, modify RevisionHistoryIntegration
plugin to read the data from revisions
array and load it to editor using RevisionsRepository
API.
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.
You can also refer to an example shown in the adapter integration section.
class RevisionHistoryIntegration extends Plugin {
static get pluginName() {
return 'RevisionHistoryIntegration';
}
static get requires() {
return [ 'RevisionHistory' ];
}
init() {
const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );
for ( const revisionData of revisions ) {
revisionHistory.addRevisionData( revisionData );
}
}
}
Since revision data will be used, editorConfig.initialData
is no longer needed and you can remove it as described in Editor initial data and revision data.
# Saving the data
You must keep the document data and revisions data in sync for the feature to work correctly.
You must 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 the get-data
ID, for example, <button id="get-data">Get editor data</button>
.
ClassicEditor
.create(document.querySelector('#editor'), editorConfig)
.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 save it as a string in your database. Then, parse the value from JSON when loading revisions.
The integration is now ready to use with your rich text editor.
# Demo
Console:
// Use the `Save revisions` button to see the result...
# Adapter integration
Adapter integration uses an adapter object – provided by you – to immediately save revisions data in your data store.
This is the recommended way of integrating revision history with your application as it lets you handle the client-server communication more securely. For example, you can check user permissions, validate sent data, or update the data with information obtained on the server side.
Additionally, revisions may include a 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. This improves the overall user experience.
It is important to load the revisions data during the plugin initialization step, that is, using the init()
method of the integration plugin that you will provide (as in the example below).
This sample does not contain the comments and track change adapters. Check the comments integration guide and track changes integration guide to learn how to build a complete solution. Also, these snippets define the same list of users. Make sure to deduplicate this code and define the list of users once to avoid errors.
# Saving document data and revisions data
Before you move to the actual implementation remember that you should always keep the document data and revisions data in sync. This means that whenever you save one, you should save the other as well. This is natural for the “load & save” integration but for adapter integration, you need to keep that in mind. A mismatch in the data will result in the feature not working correctly.
Additionally, the 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 you load the document data.
# 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. Thanks to this, you do not have to worry that the editor will be destroyed before the adapter action has finished.
Now you are ready to create the adapter. Let’s start with mocking revisions data. In real-life scenario this will be the data stored in your data storage.
// Revisions data will be available under a global variable `revisions`.
const revisions = [
{
"id": "initial",
"name": "Initial revision",
"creatorId": "user-1",
"authorsIds": [ "user-1" ],
"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": "2024-05-27T13:22:59.077Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 1
},
{
"id": "e6f80e6be6ee6057fd5a449ab13fba25d",
"name": "Updated with the actual data",
"creatorId": "user-1",
"authorsIds": [ "user-1" ],
"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:user-1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:user-1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:user-1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:user-1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:user-1: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:user-1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:user-1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1: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:user-1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:user-1: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:user-1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:user-1:3"]],"children":[]}]}]'
}
},
"createdAt": "2024-05-27T13:23:52.553Z",
"attributes": {},
"fromVersion": 1,
"toVersion": 20
},
{
"id": "e6590c50ccbc86acacb7d27231ad32064",
"name": "Inserted logo",
"creatorId": "user-1",
"authorsIds": [ "user-1" ],
"diffData": {
"main": {
"insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:user-1: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:user-1: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:user-1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:user-1: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": "2024-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 20,
"toVersion": 24
},
// An 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": "2024-05-27T13:26:39.252Z",
"attributes": {},
"fromVersion": 24,
"toVersion": 24
}
];
Next, create adapter plugin based on RevisionHistoryIntegration
plugin template to fetch revisions data asynchronously.
// A plugin that introduces the adapter.
class RevisionHistoryIntegration extends Plugin {
static get pluginName() {
return 'RevisionHistoryIntegration';
}
static get requires() {
return [ 'RevisionHistory' ];
}
async init() {
const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );
revisionHistory.adapter = {
getRevision: ( { revisionId } ) => {
return this._findRevision( revisionId );
},
updateRevisions: revisionsData => {
const documentData = this.editor.getData();
// This should be an asynchronous request to your database
// that saves `revisionsData` and `documentData`.
//
// The 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.
const revisionsData = await this._fetchRevisionsData();
for ( const revisionData of revisionsData ) {
revisionHistory.addRevisionData( revisionData );
}
}
async _findRevision( revisionId ) {
// Get the revision data based on its ID.
// This should be an asynchronous request to your database.
return Promise.resolve( revisions.find( revision => revision.id === revisionId ) );
}
async _fetchRevisionsData() {
// Get a list of all revisions.
// This should be an asynchronous call to your database.
//
// Note that the revision list should not contain the `diffData` property.
// The `diffData` property may be big and will be fetched on demand by `adapter.getRevision()`.
return Promise.resolve(revisions.map(revision => ({ ...revision, diffData: undefined })));
}
}
Since revision data will be used, editorConfig.initialData
is no longer needed and you can remove it as described in Editor initial data and revision data.
It is recommended to stringify the revisions’ attributes
property value to JSON and 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.
# Demo
Revision history adapter actions console:
// Create new version to see the result...
# Learn more
# Pending actions
Revision history uses the pending actions feature. Pending actions are added when the revisions data is being updated through the revision history adapter, to prevent closing the editor before the update finishes.
# Editor initial data and revision data
When the revision history feature is used, it is crucial to keep the revisions state in synchronization with the document data. Because of that, if there are any revisions saved for a document, the editor initial data will be discarded. The data saved with the most recent revision will be used instead.
Since the editor’s initial data is discarded, you can lower the data load by setting the document’s initial data to empty. This applies to documents for which at least one revision was created.
If the editor’s initial data was set and it was different from the revision data, a warning will be logged in the console.
# 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.
This section describes how to integrate revision history with the autosave plugin. This way, you can frequently save your document and revision data to keep them in sync.
Although this guide provides ready-to-use snippets, we encourage you to also read the How revisions are saved and updated section to get a better understanding of this subject.
The autosave callback is called with the autosave plugin enabled and revision history opened.
The integration differs a bit whether you use the 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 the 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. 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 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 big revision, containing a lot of changes. To prevent that, autosave integration could create new revisions based on your custom strategy.
For example, you may decide to save the current revision (unsaved changes) after a 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 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 the "current revision" with the new document changes.
// If there are any new changes, the `createdAt` property will change its value.
// Do not 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 the 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 have 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, that is, changes that have not been saved in earlier revisions. It is always at the 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.
The current revision is not updated automatically when the document changes. The update can be done using the revisions feature API. It is enough to update the current revision when you need to save it, for example, in the autosave callback. The update is also triggered when the revision history view is opened. In this case, either the autosave callback is called, or the current revision is updated (if the 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 the "current revision", that is, the revision containing unsaved changes.
revisionTrackerPlugin.update();
// Creates a new revision that will contain all the unsaved changes.
// See the API reference to learn more.
revisionTrackerPlugin.saveRevision();
revisionTrackerPlugin.saveRevision( { name: 'My revision' } );
# Revision history samples
Please visit the ckeditor5-collaboration-samples
GitHub repository to find several sample integrations of the revision history feature.
Every day, we work hard to keep our documentation complete. Have you spotted outdated information? Is something missing? Please report it via our issue tracker.
With the release of version 42.0.0, we have rewritten much of our documentation to reflect the new import paths and features. We appreciate your feedback to help us ensure its accuracy and completeness.