Revision History Plugin
This plugin is only available as a paid add-on to a TinyMCE subscription. |
This feature is only available for TinyMCE 7.0 and later. |
The Revision History plugin offers users the ability to view document changes over time and restore previous revisions effortlessly.
Interactive example
-
TinyMCE
-
HTML
-
JS
-
Edit on CodePen
<textarea id="revisionhistory">
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE Revision History demo!</h2>
<p style="text-align: center;">Try out the Revision History feature by typing in the editor and then clicking the Revision History icon in the toolbar.</p>
<p style="text-align: center;">And visit the <a href="https://www.tiny.cloud/pricing">pricing page</a> to learn more about our Premium plugins.</p>
<h2>A simple table to play with</h2>
<table style="border-collapse: collapse; width: 100%;" border="1">
<thead>
<tr style="text-align: left;">
<th>Product</th>
<th>Cost</th>
<th>Really?</th>
</tr>
</thead>
<tbody>
<tr>
<td>TinyMCE Cloud</td>
<td>Get started for free</td>
<td><strong>Yes!</strong></td>
</tr>
<tr>
<td>Plupload</td>
<td>Free</td>
<td><strong>Yes!</strong></td>
</tr>
</tbody>
</table>
<h2>Got questions or need help?</h2>
<ul>
<li>Our <a class="mceNonEditable" href="https://www.tiny.cloud/docs/tinymce/7/">documentation</a> is a great resource for learning how to configure TinyMCE.</li>
<li>Have a specific question? Try the <a href="https://stackoverflow.com/questions/tagged/tinymce" target="_blank" rel="noopener"><code>tinymce</code> tag at Stack Overflow</a>.</li>
<li>We also offer enterprise grade support as part of <a href="https://www.tiny.cloud/pricing">TinyMCE premium subscriptions</a>.</li>
</ul>
<h2>Found a bug?</h2>
<p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p>
<h2>Finally…</h2>
<p>Don’t forget to check out our other product <a href="http://plupload.com" target="_blank">Plupload</a>, your ultimate upload solution featuring HTML5 upload support.</p>
<p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.</p>
<p>All the best from the TinyMCE team.</p>
</textarea>
const getRandomDelay = () => {
const minDelay = 500;
const maxDelay = 2000;
return Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay;
};
const lightRevisions = [
{
revisionId: '3',
author: {
id: 'tiny.husky',
name: 'A Tiny Husky',
avatar: '../_images/tiny-husky.jpg',
},
createdAt: '2023-11-25T08:30:21.578Z'
},
{
revisionId: '2',
author: {
id: 'tiny.user',
name: 'A Tiny User',
avatar: '../_images/logos/android-chrome-192x192.png',
},
createdAt: '2023-11-24T22:26:21.578Z',
},
{
revisionId: '1',
author: {
id: 'tiny.user',
name: 'A Tiny User',
avatar: '../_images/logos/android-chrome-192x192.png',
},
createdAt: '2023-11-23T20:26:21.578Z',
},
];
const revisionhistory_fetch = () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(
lightRevisions
.sort((a, b) =>
new Date(a.createdAt) < new Date(b.createdAt) ? -1 : 1
)
.reverse()
);
}, getRandomDelay());
});
const revisions = [
{
revisionId: '3',
createdAt: '2023-11-24T22:26:21.578Z',
author: {
id: 'tiny.husky',
name: 'A Tiny Husky',
avatar: '../_images/tiny-husky.jpg',
},
content: `
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
<h2>A simple table to play with</h2>
<table style="border-collapse: collapse; width: 100%;" border="1">
<thead>
<tr>
<th>Product</th>
<th>Cost</th>
<th>Really?</th>
</tr>
</thead>
<tbody>
<tr>
<td>TinyMCE</td>
<td>Free</td>
<td>YES!</td>
</tr>
<tr>
<td>Plupload</td>
<td>Free</td>
<td>YES!</td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p>
<h2>Finally ...</h2>
<p><s>Don't forget to check out our other product <a href="http://www.plupload.com" target="_blank" rel="noopener">Plupload</a>, your ultimate upload solution featuring HTML5 upload support.</s></p>
<p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.<br>All the best from the TinyMCE team.</p>
`,
},
{
revisionId: '2',
createdAt: '2023-11-25T08:30:21.578Z',
author: {
id: 'tiny.user',
name: 'A Tiny User',
avatar: '../_images/logos/android-chrome-192x192.png',
},
content: `
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
<h2>Got questions or need help?</span></h2>
<ol>
<li>Our <a href="../">documentation</a> is a great resource for learning how to configure TinyMCE.</li>
<li>Have a specific question? Try the <a href="https://stackoverflow.com/questions/tagged/tinymce" target="_blank" rel="noopener"><code>tinymce</code> tag at Stack Overflow</a>.</li>
<li>We also offer enterprise grade support as part of <a href="../../../../pricing">TinyMCE premium plans</a>.</li>
</ol>
<h2>A simple table to play with</h2>
<table style="border-collapse: collapse; width: 100%;" border="1">
<thead>
<tr>
<th>Product</th>
<th>Cost</th>
<th>Really?</th>
</tr>
</thead>
<tbody>
<tr>
<td>TinyMCE</td>
<td>Free</td>
<td>YES!</td>
</tr>
<tr>
<td>Plupload</td>
<td>Free</td>
<td>YES!</td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p>
<h2>Finally ...</h2>
<p>Don't forget to check out our other product <a href="http://www.plupload.com" target="_blank" rel="noopener">Plupload</a>, your ultimate upload solution featuring HTML5 upload support.</p>
<p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.<br>All the best from the TinyMCE team.</p>
`,
},
{
revisionId: '1',
createdAt: '2023-11-29T10:11:21.578Z',
author: {
id: 'tiny.user',
name: 'A Tiny User',
avatar: '../_images/logos/android-chrome-192x192.png',
},
content: `
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE editor demo!</h2>
<h2>Got questions or need help?</h2>
<ul>
<li>Our <a href="../">documentation</a> is a great resource for learning how to configure TinyMCE.</li>
<li>Have a specific question? Try the <a href="https://stackoverflow.com/questions/tagged/tinymce" target="_blank" rel="noopener"><code>tinymce</code> tag at Stack Overflow</a>.</li>
<li>We also offer enterprise grade support as part of <a href="../../../../pricing">TinyMCE premium plans</a>.</li>
</ul>
<h2>A simple table to play with</h2>
<table style="border-collapse: collapse; width: 100%;" border="1">
<thead>
<tr>
<th>Product</th>
<th>Cost</th>
<th>Really?</th>
</tr>
</thead>
<tbody>
<tr>
<td>TinyMCE</td>
<td>Free</td>
<td>YES!</td>
</tr>
<tr>
<td>Plupload</td>
<td>Free</td>
<td>YES!</td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you think you have found a bug please create an issue on the <a href="https://github.com/tinymce/tinymce/issues">GitHub repo</a> to report it to the developers.</p>
<h2>Finally ...</h2>
<p>Don't forget to check out our other product <a href="http://www.plupload.com" target="_blank" rel="noopener">Plupload</a>, your ultimate upload solution featuring HTML5 upload support.</p>
<p>Thanks for supporting TinyMCE! We hope it helps you and your users create great content.<br>All the best from the TinyMCE team.</p>
`,
}
];
const revisionhistory_fetch_revision = (_editor, revision) =>
new Promise((resolve, reject) => {
setTimeout(() => {
const newRevision = revisions.find((r) => r.revisionId === revision.revisionId);
if (newRevision === undefined) {
reject(`Revision ${revision.revisionId} is not found`);
} else {
resolve(newRevision);
}
}, getRandomDelay());
});
tinymce.init({
selector: 'textarea#revisionhistory',
height: 800,
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
revisionhistory_fetch,
revisionhistory_fetch_revision,
revisionhistory_author: {
id: 'john.doe',
name: 'John Doe'
},
revisionhistory_display_author: true
});
This feature is only supported when TinyMCE is run in classic mode. It is not supported in inline mode. For more information on the differences between the editing modes, see Classic editing mode. |
How it works
The Revision History view is accessible via either the revisionhistory
toolbar button or menu button within the View
menu.
The key components are:
-
In the Revision History view header, there are two buttons.
-
Restore this version
: Sets the selected revision’s content to the editor and closes the view. Note: the button is disabled for theinitial
anddraft
revisions. -
Close
: Closes the Revision History view.
-
-
The readonly diff view presents the changes between the selected revision and its immediate predecessor, clearly highlighting for easy recognition. The changes are also color-coded for clarity:
-
Red: Removed content.
-
Green: New content.
-
Yellow: Content being modified. Modifications to HTML content implies attributes or formatting (e.g. bold, italic, etc.).
-
-
The revisions sidebar displays all available document revisions. When a revision is selected, the diff view is updated accordingly.
The default highlighting colors can be customized using the revisionhistory_css_url option.
|
The Revision History plugin processes commented HTML as valid content but disregards it during the revision comparison process. Revisions containing only commented content appear as empty in the view. |
Basic setup
To setup the Revision History plugin in the editor:
-
add
revisionhistory
to theplugins
option in the editor configuration; -
add
revisionhistory
to thetoolbar
option in the editor configuration; -
add
revisionhistory_fetch
option to the editor configuration;
For example:
tinymce.init({
selector: 'textarea', // change this value according to your HTML
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
revisionhistory_fetch: () => Promise.resolve([]), // Replace this with an API request to get saved revisions
});
Understanding revision types
The Revision History plugin offers three revision types:
-
Initial: This revision is generated during the TinyMCE
Loaded
event, capturing the editor’s initial content. -
Draft: Generated upon opening the Revision History, this revision reflects the editor’s current content. When included, it becomes the
latest revision
in the revisions list. -
Saved: These revisions are fetched from the client’s storage when opening the Revision History view, using the
revisionhistory_fetch
andrevisionhistory_fetch_revision
options.
When working with a document that has no saved revisions, the Revision History plugin typically maintains two revisions: initial
and draft
. If no changes have been made to the content since initialization, the initial
revision is not displayed, as it is identical to the draft
revision.
For documents with saved revisions, the initial
revision is disregarded, assuming it already exists among the saved revisions. To include the initial content, add it as a revision using the revisionhistory_fetch
option.
Data structure
Revision
The revision is an Object
that contains the following fields:
Field | Type | Required? | Description |
---|---|---|---|
|
|
required |
The unique string ID of the revision. |
|
|
required |
A UTC datetime string in ISO-8061 format. |
|
|
optional |
HTML string of the revision content. Empty string is considered as valid content. |
|
Author |
optional |
The author of the revision. |
Author
The author is an Object
that represents the author or creator of a revision. It contains the following fields:
Field | Type | Required? | Description |
---|---|---|---|
|
|
required |
The unique string ID of the author. |
|
|
optional |
The name of the revision author. If not provided, the default value is |
|
|
optional |
The URL of the author’s avatar image. If not provided or invalid, the Revision History will use a generated avatar using the author’s initials. |
Options
The following configuration options affect the behavior of the Revision History plugin.
revisionhistory_fetch
The revisionhistory_fetch
function retrieves saved revisions and is called when the user opens the Revision History view.
The revisionhistory_fetch function is required for the Revision History plugin to function.
|
It expects an asynchronous function that returns a Promise which resolves to an array of revisions.
The Revision History plugin does not sort results before displaying them, allowing integrators to sort the results according to their preference. Tiny recommends to sort the results in reverse chronological order, as this makes it easier for users to identify revisions by date. |
Type: Function
(Returns a Promise)
Input parameters: None
Return data: Array
of Revision Objects
Example of revisionhistory_fetch
response
const revisions = [
{
revisionId: '3',
createdAt: '2023-11-29T10:11:21.578Z',
content: `
<h2>Welcome to TinyMCE Docs!</h2>
<p>Here is some content that is <strong>bold</strong> and <em>italic</em>.</p>
`,
},
{
revisionId: '2',
createdAt: '2023-11-25T08:30:21.578Z',
content: `
<p>Welcome to TinyMCE Docs!</p>
<p>Here is some content that is bold and italic.</p>
`,
},
{
revisionId: '1',
createdAt: '2023-11-24T22:26:21.578Z',
content: `
<p>Welcome to Tinymce!</p>
`,
}
];
Example: Using revisionhistory_fetch
tinymce.init({
selector: 'textarea', // change this value according to your HTML
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
revisionhistory_fetch: () =>
fetch('<API URL>') // Update the URL and response handling code according to your API
.then((response) => response.json())
.then((data) => data)
.catch((error) => console.log('Failed to get revisions\n' + error))
});
revisionhistory_fetch_revision
When a revision is selected, the plugin uses this option to update the selected or closest revision that has no content. The function is expected to return a Promise that resolves with an updated revision.
Type: Function
(Returns a Promise)
Input parameters:
Field | Type | Required? | Description |
---|---|---|---|
|
|
required |
The current editor instance (useful if there are multiple editors on the page). |
|
Revision |
required |
The targeted revision. |
Return data: Revision Object
Example: Using revisionhistory_fetch_revision
const lightRevisions = [
{
revisionId: '3',
createdAt: '2023-11-29T10:11:21.578Z',
},
{
revisionId: '2',
createdAt: '2023-11-25T08:30:21.578Z',
},
{
revisionId: '1',
createdAt: '2023-11-24T22:26:21.578Z',
}
];
const revisions = [
{
revisionId: '3',
createdAt: '2023-11-29T10:11:21.578Z',
content: `
<h2>Welcome to TinyMCE Docs!</h2>
<p>Here is some content that is <strong>bold</strong> and <em>italic</em>.</p>
`,
},
{
revisionId: '2',
createdAt: '2023-11-25T08:30:21.578Z',
content: `
<p>Welcome to TinyMCE Docs!</p>
<p>Here is some content that is bold and italic.</p>
`,
},
{
revisionId: '1',
createdAt: '2023-11-24T22:26:21.578Z',
content: `
<p>Welcome to Tinymce!</p>
`,
}
];
tinymce.init({
selector: 'textarea', // change this value according to your HTML
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
revisionhistory_fetch: () => Promise.resolve(lightRevisions),
revisionhistory_fetch_revision: (_editor, revision) => new Promise((resolve) => {
let newRevision = null;
for (let i = 0; i < revisions.length; i++) {
const temp = revisions[i];
if (temp.revisionId === revision.revisionId) {
newRevision = temp;
break;
}
}
resolve(newRevision);
})
});
revisionhistory_author
This option configures the author for the initial
and draft
revisions.
Type: Author Object
Example: using revisionhistory_author
tinymce.init({
selector: 'textarea', // Change this value according to your HTML
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
revisionhistory_fetch: () => Promise.resolve([]),
revisionhistory_display_author: true,
revisionhistory_author: {
id: 'john.doe',
name: 'John Doe',
avatar: 'https://example.com/avatar.jpg'
}
});
revisionhistory_display_author
This option configures the display of the revision’s author. When set to true
, the author’s name appears in each revision within the Revision History sidebar. If the author’s name is not provided, it defaults to Anonymous
.
Type: Boolean
Default value: false
revisionhistory_css_url
This option sets the location where a CSS file containing the styles for change annotations should be loaded from. It can be used along with revisionhistory_diff_classes
to override the default CSS classes.
Ensure that the file includes all CSS classes expected by revisionhistory_diff_classes .
|
Type: String
Default value: '${pluginUrl}/css/revisionhistory.css'
Example: using revisionhistory_css_url
tinymce.init({
selector: 'textarea', // change this value according to your HTML
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
revisionhistory_fetch: () => Promise.resolve([]), // Required option for the plugin - replace with actual API request
revisionhistory_css_url: './revisionhistory.css'
});
revisionhistory_diff_classes
This option configures the CSS classes being applied to the change annotations. This option can be used in combination with the revisionhistory_css_url
to provide custom styles to the change annotations.
The CSS class names must exist in the revisionhistory_css_url .
|
Type: Object
Default value: addition
, removal
, modification
{
addition: 'tox-revisionhistory__annotation--added',
removal: 'tox-revisionhistory__annotation--removed',
modification: 'tox-revisionhistory__annotation--modified'
}
Example: using revisionhistory_diff_classes
tinymce.init({
selector: 'textarea', // change this value according to your HTML
plugins: 'revisionhistory',
toolbar: 'revisionhistory',
revisionhistory_fetch: () => Promise.resolve([]), // Required option for the plugin - replace with actual API request
revisionhistory_css_url: './revisionhistory.css',
revisionhistory_diff_classes: {
addition: 'added',
removal: 'removed',
modification: 'modified'
}
});
Toolbar buttons
The Revision History plugin provides the following toolbar buttons:
Toolbar button identifier | Description |
---|---|
|
Opens the Revision History view. |
These toolbar buttons can be added to the editor using:
-
The
toolbar
configuration option. -
The
quickbars_insert_toolbar
configuration option.
Menu items
The Revision History plugin provides the following menu items:
Menu item identifier | Default Menu Location | Description |
---|---|---|
|
View |
Opens the Revision History view. |
These menu items can be added to the editor using:
-
The
menu
configuration option. -
The
contextmenu
configuration option.
Commands
The Revision History plugin provides the following TinyMCE commands.
Command | Description |
---|---|
revisionHistory |
Toggle the Revision History view |
tinymce.activeEditor.execCommand('revisionHistory');
Events
The Revision History plugin provides the following events.
The following events are provided by the Revision History plugin.
Name | Data | Description |
---|---|---|
VersionRestored |
|
Fired when a version is restored. |
APIs
The Revision History plugin provide the following APIs.
interface PluginAPI {
diff: (originalHTML: string, changedHTML: string) => string
}
diff
Returns a HTML string with annotations indicating the changes in originalHTML
relative to changedHTML
. The styling of annotations can be customized via revisionhistory_diff_classes
option.
This feature is only available for TinyMCE 7.1 and later. |
const input1 = `<p>The banner is <span style="color: rgb(224, 62, 45);">red</span></p>`;
const input2 = `<p>The flag is <span style="color: rgb(22, 145, 121);">red</span></p>`;
tinymce.activeEditor.plugins.revisionhistory.diff(input1, input2);
/*
Expected output:
<p>The
<del class="tox-revisionhistory__annotation--removed">banner</del><ins class="tox-revisionhistory__annotation--added">flag</ins>
is
<ins class="tox-revisionhistory__annotation--modified"><span style="color: rgb(22, 145, 121);">red</span></ins>
</p>
*/