Suggested Edits Plugin
| This plugin is only available as a paid add-on to a TinyMCE subscription. |
The Suggested Edits plugin allows multiple users to collaborate on a document. The review window shows which user suggested which edits, whether they added, removed, modified, or replaced any content, and allows users to provide feedback on those suggestions or give a final review by accepting or rejecting them.
Interactive example
-
TinyMCE
-
HTML
-
JS
-
Edit on CodePen
<textarea id="suggestededits" suggestededits-model='{"history":{"2":[{"id":1,"uid":"james-wilson","timestamp":1752576936000,"feedback":"Nice improvement!"}]},"version":2,"metadata":{"5":{"source":"ai"}},"contents":[{"type":"p","children":[{"type":"img","attrs":{"style":"display: block; margin-left: auto; margin-right: auto;","title":"Tiny Logo","src":"https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png","alt":"TinyMCE Logo","width":"128","height":"128"}}]},{"type":"h2","attrs":{"style":"text-align: center;"},"children":[{"text":"Welcome to the TinyMCE Suggested Edits "},{"text":"interactive ","opData":{"id":1,"type":"insert","uid":"alex-thompson","timestamp":1752015064000}},{"text":"demo!"}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"Try out the Suggested Edits feature"},{"text":" by typing in the editor and t","opData":{"id":2,"type":"remove","uid":"alex-thompson","timestamp":1752415064000}},{"text":": type in the editor, apply formatting or delete some content. T","opData":{"id":2,"type":"insert","uid":"alex-thompson","timestamp":1752415064000}},{"text":"hen"},{"text":",","opData":{"id":3,"type":"insert","uid":"alex-thompson","timestamp":1752515064000}},{"text":" click"},{"text":"ing","opData":{"id":4,"type":"remove","uid":"alex-thompson","timestamp":1752515064000}},{"text":" the Review edits button in the toolbar"},{"text":" to see your changes","opData":{"id":5,"type":"insert","uid":"kai-nakamura","timestamp":1752615064000}},{"text":"."}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"And visit the "},{"text":"pricing page","opData":{"id":6,"type":"modify","uid":"kai-nakamura","timestamp":1752615064000},"format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}}],"oldFormat":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}},"em"]},{"text":" to learn more about our Premium plugins."}]},{"type":"h2","children":[{"text":"A simple table to play with"}]},{"type":"table","attrs":{"style":"border-collapse: collapse; width: 100%;","border":"1"},"children":[{"type":"thead","children":[{"type":"tr","attrs":{"style":"text-align: left;"},"children":[{"type":"th","children":[{"text":"Product"}]},{"type":"th","children":[{"text":"Cost"}]},{"type":"th","children":[{"text":"Really?"}]}]}]},{"type":"tbody","children":[{"type":"tr","children":[{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"TinyMCE Cloud"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Get started for free"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Yes!","format":["strong"]}]}]}]}]},{"type":"h2","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"children":[{"text":"Found a bug?"}]},{"type":"p","children":[{"text":" ","opData":{"id":7,"type":"remove","uid":"mia-andersson","timestamp":1752576331000}},{"text":"If you believe you have found a bug please create an issue on the ","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}},{"text":"GitHub repo","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"format":[{"type":"a","attrs":{"href":"https://github.com/tinymce/tinymce/issues"}}]},{"text":" to report it to the developers.","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}}]},{"type":"h2","children":[{"text":"Finally…"}]},{"type":"p","children":[{"text":"Need file uploads in your app? Consider using "},{"text":"Uploadcare","format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/docs/tinymce/latest/uploadcare/","target":"_blank","rel":"noopener noreferrer"}}]},{"text":" with TinyMCE for a fast, modern upload experience."}]},{"type":"p","children":[{"text":"Thanks for supporting TinyMCE. We hope it helps you and your users create great content."}]},{"type":"p","children":[{"text":"All the best from the TinyMCE team."}]}]}'>
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE Suggested Edits interactive demo!</h2>
<p style="text-align: center;">Try out the Suggested Edits feature: type in the editor, apply formatting or delete some content. Then, click the Review edits button in the toolbar to see your changes.</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 style="text-align: center;">TinyMCE Cloud</td>
<td style="text-align: center;">Get started for free</td>
<td style="text-align: center;"><strong>Yes!</strong></td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you believe 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>Need file uploads in your app? Consider using <a href="https://www.tiny.cloud/docs/tinymce/latest/uploadcare/" target="_blank" rel="noopener noreferrer">Uploadcare</a> with TinyMCE for a fast, modern upload experience.</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 API_URL = 'https://demouserdirectory.tiny.cloud/v1/users';
const tinymceElement = document.querySelector('textarea#suggestededits');
const model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits',
height: 500,
plugins: 'suggestededits advlist anchor autolink code charmap emoticons fullscreen help image link lists media preview searchreplace table',
toolbar: 'undo redo | suggestededits suggestededits-tracking | styles fontsizeinput | bold italic | align bullist numlist | table link image | code',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
suggestededits_model: model,
suggestededits_access: 'full',
suggestededits_content: 'html',
view_show: 'suggestededits',
user_id: 'kai-nakamura',
fetch_users: (userIds) => Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId }))))
});
| 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. |
|
For information on bundling this plugin with module bundlers, see Bundling TinyMCE - Overview. |
How it works
The Suggested Edits plugin keeps track of every edit made to the document by the current user and stores this metadata in an internal model of the document. These suggestions can then be reviewed in the Review edits view, where each edit is highlighted in the document, and where users can accept, reject, or provide feedback. The Review edits view opens from either the suggestededits toolbar button or the suggestededits menu item in the View menu.
The model
The Suggested Edits model is a JSON object representing the document along with all unreviewed edits and feedback. The model must be kept in sync with the editor content and loaded into the editor at the same time as the content.
The structure of the model is not documented and should not be relied upon. Models saved by earlier versions of the plugin continue to load and are upgraded automatically.
The model can be retrieved from the plugin using the getModel API, saved externally alongside the document, and loaded into the editor with the suggestededits_model option. This ensures that the document and the model are in sync and every user’s contributions are tracked correctly. If the model and content are out of sync when the editor loads, the difference between them will be included in the next edit by the current user. If a model is not provided in the editor configuration or is set to undefined, the plugin will generate a new model from the initial content.
Alternatively the suggestededits_content option allows the model to generate the editor content, in which case the two do not need to be kept in sync.
Reviewing edits
The Review edits view displays the edits made by multiple authors for review. The available actions in this view depend on the suggestededits_access option.
While the Review edits view is open, the editor menu bar, toolbar, and status bar remain visible, the editor content is read-only, and the editor can still be resized. Most toolbar buttons and menu items are disabled while the view is open. Items that work in any editor mode, such as Fullscreen, Preview, and Print, remain available. For details, see Built-in contexts. Opening a sidebar closes the Review edits view. Opening and closing the Review edits view keeps the current scroll position in the document.
Header
The header at the top of the sidebar shows the Review edits title and two buttons that end the review:
-
Save & Exit: Ends the review, applying resolved suggestions to the document. Unresolved suggestions remain in the document for future review. This button is disabled for users without
fullaccess. -
Exit review: Ends the review without applying resolved suggestions, and makes no change to the document. Feedback given on suggestions will be retained.
The Exit review button displays only an icon. The text Exit review appears as a tooltip and as the accessible label of the button.
Footer
The footer below the document switches between two views:
-
Diff mode: Shows the suggested edits with the color coding described in Document. While unresolved suggestions remain, Diff mode dims unchanged content so the unresolved suggestions stand out. Diff mode is the default view.
-
Preview: Shows how the document reads with the suggested edits applied, taking accepted and rejected suggestions into account. Added and modified content is underlined, and removed content is hidden unless the suggestion is selected.
Document
The Review edits view displays the current editor document in an iframe, with each suggested edit highlighted where it appears in the document.
In Diff mode, the following color coding is used to indicate the type of change:
-
Green: Added content.
-
Blue: Modified attributes or formatting, such as bold or italic.
-
Red: Removed content, shown with a strikethrough.
In Diff mode, removed images, videos, and embedded frames are also marked with a red X. Line break markers appear only where a line break was added or removed.
Replaced content is represented as both added and removed content, and indicates that some content was removed and replaced with new content in a single edit. When a suggestion is selected, whether in the document or in the sidebar, the corresponding highlighted suggestion in the document will be outlined in blue and scrolled into view. In Diff mode, accepted and rejected suggestions show their result without highlighting, except while the suggestion is selected. Selecting anywhere outside a card or a suggestion clears the selection.
Sidebar
Each suggested edit is listed as a card in the sidebar, in the order the edits appear in the document. The card header shows the user who made the suggestion and when the edit was made. Below the header, the card describes the edit and quotes the changed text, for example: Replaced “the” with “them”. Long text is shortened with an ellipsis.
Edits made with the TinyMCE AI plugin are recorded as AI-assisted. An AI badge appears on their cards by default. Set suggestededits_ai_attribution to false to hide the badge.
For users with full access, pointing to a card or selecting it shows the following buttons in the card header:
-
Accept: Resolves the suggestion, applying the edit to the document when the review is completed.
-
Reject: Resolves the suggestion, turning back the edit to the original state.
After a suggestion is accepted or rejected, the card header shows a Revert button with an icon that indicates the decision. Selecting Revert removes the decision and returns the suggestion to an unresolved state.
When an unresolved suggestion is selected, users with feedback or full access can select Provide feedback to open a text area and provide feedback on the suggestion.
Feedback is shown in chronological order beneath the card details when an unresolved card is selected. Feedback allows users to discuss suggestions before resolving them. The feedback author can edit or delete their own feedback, with the appropriate permissions in the suggestededits_access option.
Bulk actions
Users with full access can use the Bulk actions menu at the top of the sidebar to resolve several suggestions at once:
-
Accept all: Accepts every unresolved suggestion. Suggestions that are already accepted or rejected keep their decision. While some suggestions are resolved and others remain unresolved, this item is labeled Accept remaining.
-
Reject all: Rejects every unresolved suggestion. Suggestions that are already accepted or rejected keep their decision. While some suggestions are resolved and others remain unresolved, this item is labeled Reject remaining.
-
Revert all: Returns every accepted or rejected suggestion to an unresolved state.
The accept and reject items are unavailable when no unresolved suggestions remain. Revert all is unavailable until at least one suggestion is resolved.
Keyboard navigation
The Review edits view supports the following keys:
-
Tab and Shift+Tab: Move focus between the buttons in the sidebar header, the Bulk actions menu, and the cards.
-
Up Arrow and Down Arrow: Move between cards and select the focused suggestion.
-
Enter: Moves focus into the content of the focused card.
-
Escape: Moves focus to the Exit review button. When Exit review already has focus, Escape ends the review.
Finishing a review
When a review is completed with Save & Exit, resolved suggestions will be applied to the document and will no longer be tracked in the model as a suggestion. Feedback on resolved suggestions is discarded. Any "accepted" edits will remain in the document, and any "rejected" edits will be reverted to the state before the suggestion was made.
Review actions will apply the following to the document:
When added content is:
-
Accepted: The content will remain in the document.
-
Rejected: The content will be removed from the document.
When modified content is:
-
Accepted: The content will retain the current formats and attributes.
-
Rejected: The content will revert to modified formats and attributes to match the content state before the edit was made.
When removed content is:
-
Accepted: The content will be removed from the document.
-
Rejected: The removed content will be restored to the document.
If a review ends with Exit review, no resolved suggestions will be applied to the document. All suggestions, including any feedback provided during that review session, will remain stored in the model.
Initial setup
To setup the Suggested Edits plugin in the editor:
-
add
suggestededitsto thepluginsoption in the editor configuration; -
add
suggestededitsto thetoolbaroption in the editor configuration;
For example:
tinymce.init({
selector: 'textarea#suggestededits', // change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
});
This configuration adds Suggested Edits to the editor toolbar, enabling access to the plugin features. To fully utilize the plugin, additional configuration options must be provided.
Options
The following configuration options affect the behavior of the Suggested Edits plugin.
suggestededits_model
The suggestededits_model option loads an existing model into the Suggested Edits plugin. This model contains all current suggested edits and is used to maintain continuity across sessions. If a model is not provided, the plugin generates a new model from the initial editor content.
Type: Object
suggestededits_modelconst tinymceElement = document.querySelector('textarea#suggestededits');
const model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits', // Change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
suggestededits_model: model // Load the saved model into the editor
});
suggestededits_content
The suggestededits_content option controls whether the content is loaded from the editor or the suggestededits_model model, allowing you to define the source of truth for the document. In either case, if a model is not provided, the plugin will generate a new model from the initial editor content.
When set to:
-
'html': the editor loads content normally, and the plugin synchronises the model with the content. This simplifies the configuration of Suggested Edits in an existing integration, by attaching the Suggested Edits model as an additional field alongside the editor content. -
'model': the editor uses thesuggestededits_modeloption to generate the initial content, ignoring any pre-existing content in the editor. With this configuration, you only need to store and provide the model, simplifying the integration of the Suggested Edits plugin.
You are responsible for saving the model and providing it on the next load using the suggestededits_model option.
|
Type: String
Possible Values: 'html', 'model'
Default value: 'html'
suggestededits_contentconst tinymceElement = document.querySelector('textarea#suggestededits');
const model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits', // Change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
suggestededits_model: model, // Load the saved model into the editor
suggestededits_content: 'model' // Set to 'model' if you want to load the initial content from the `suggestededits_model` option
});
suggestededits_access
The suggestededits_access option determines the level of access a user has to the Review edits view. This setting is crucial for controlling who can accept or reject suggestions, add feedback, or view suggestions in read-only mode. The suggestededits_access option does not control access to the editor content, however when used in conjunction with the readonly option it allows for fine-grained control over user permissions.
When set to:
-
'full': The user has full access to the Review edits view, with permission to accept or reject suggestions, use the Bulk actions menu, and complete the review with Save & Exit. -
'feedback': The user has access to the Review edits view, with permission to add feedback to suggestions. The Save & Exit button is disabled. -
'read': The user has read-only access to the Review edits view. The Save & Exit button is disabled. -
'none': The user has no access to the Review edits view. Thesuggestededitstoolbar button and menu item are unavailable.
Type: String
Possible Values: 'full', 'feedback', 'read', 'none'
Default Value: 'full'
suggestededits_access: 'feedback'tinymce.init({
selector: 'textarea#suggestededits', // Change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
suggestededits_access: 'feedback', // Change this value to set the Review edits view permissions
readonly: false // Set to true to restrict editing
});
-
TinyMCE
-
HTML
-
JS
-
Edit on CodePen
<textarea id="suggestededits-access-feedback" suggestededits-model='{"history":{"2":[{"id":1,"uid":"james-wilson","timestamp":1752576936000,"feedback":"Nice improvement!"}]},"version":1,"contents":[{"type":"p","children":[{"type":"img","attrs":{"style":"display: block; margin-left: auto; margin-right: auto;","title":"Tiny Logo","src":"https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png","alt":"TinyMCE Logo","width":"128","height":"128"}}]},{"type":"h2","attrs":{"style":"text-align: center;"},"children":[{"text":"Welcome to the TinyMCE Suggested Edits "},{"text":"interactive ","opData":{"id":1,"type":"insert","uid":"alex-thompson","timestamp":1752015064000}},{"text":"demo!"}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"Try out the Suggested Edits feature"},{"text":" by typing in the editor and t","opData":{"id":2,"type":"remove","uid":"alex-thompson","timestamp":1752415064000}},{"text":": type in the editor, apply formatting or delete some content. T","opData":{"id":2,"type":"insert","uid":"alex-thompson","timestamp":1752415064000}},{"text":"hen"},{"text":",","opData":{"id":3,"type":"insert","uid":"alex-thompson","timestamp":1752515064000}},{"text":" click"},{"text":"ing","opData":{"id":4,"type":"remove","uid":"alex-thompson","timestamp":1752515064000}},{"text":" the Review edits button in the toolbar"},{"text":" to see your changes","opData":{"id":5,"type":"insert","uid":"kai-nakamura","timestamp":1752615064000}},{"text":"."}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"And visit the "},{"text":"pricing page","opData":{"id":6,"type":"modify","uid":"kai-nakamura","timestamp":1752615064000},"format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}}],"oldFormat":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}},"em"]},{"text":" to learn more about our Premium plugins."}]},{"type":"h2","children":[{"text":"A simple table to play with"}]},{"type":"table","attrs":{"style":"border-collapse: collapse; width: 100%;","border":"1"},"children":[{"type":"thead","children":[{"type":"tr","attrs":{"style":"text-align: left;"},"children":[{"type":"th","children":[{"text":"Product"}]},{"type":"th","children":[{"text":"Cost"}]},{"type":"th","children":[{"text":"Really?"}]}]}]},{"type":"tbody","children":[{"type":"tr","children":[{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"TinyMCE Cloud"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Get started for free"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Yes!","format":["strong"]}]}]}]}]},{"type":"h2","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"children":[{"text":"Found a bug?"}]},{"type":"p","children":[{"text":" ","opData":{"id":7,"type":"remove","uid":"mia-andersson","timestamp":1752576331000}},{"text":"If you believe you have found a bug please create an issue on the ","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}},{"text":"GitHub repo","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"format":[{"type":"a","attrs":{"href":"https://github.com/tinymce/tinymce/issues"}}]},{"text":" to report it to the developers.","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}}]},{"type":"h2","children":[{"text":"Finally…"}]},{"type":"p","children":[{"text":"Need file uploads in your app? Consider using "},{"text":"Uploadcare","format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/docs/tinymce/latest/uploadcare/","target":"_blank","rel":"noopener noreferrer"}}]},{"text":" with TinyMCE for a fast, modern upload experience."}]},{"type":"p","children":[{"text":"Thanks for supporting TinyMCE. We hope it helps you and your users create great content."}]},{"type":"p","children":[{"text":"All the best from the TinyMCE team."}]}]}'>
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE Suggested Edits interactive demo!</h2>
<p style="text-align: center;">Try out the Suggested Edits feature: type in the editor, apply formatting or delete some content. Then, click the Review edits button in the toolbar to see your changes.</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 style="text-align: center;">TinyMCE Cloud</td>
<td style="text-align: center;">Get started for free</td>
<td style="text-align: center;"><strong>Yes!</strong></td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you believe 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>Need file uploads in your app? Consider using <a href="https://www.tiny.cloud/docs/tinymce/latest/uploadcare/" target="_blank" rel="noopener noreferrer">Uploadcare</a> with TinyMCE for a fast, modern upload experience.</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 API_URL = 'https://demouserdirectory.tiny.cloud/v1/users';
const tinymceElement = document.querySelector('textarea#suggestededits-access-feedback');
const model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits-access-feedback',
height: 500,
plugins: 'suggestededits advlist anchor autolink code charmap emoticons fullscreen help image link lists media preview searchreplace table',
toolbar: 'undo redo | suggestededits | styles fontsizeinput | bold italic | align bullist numlist | table link image | code',
readonly: false, // Set to true to prevent edits to the content
suggestededits_model: model,
suggestededits_access: 'feedback', // Set this value to restrict the permissions in the Suggested Edits view
suggestededits_content: 'html',
user_id: 'kai-nakamura',
fetch_users: (userIds) => Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId }))))
});
suggestededits_access: 'read'tinymce.init({
selector: 'textarea#suggestededits', // Change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
suggestededits_access: 'read', // Change this value to set the Review edits view permissions
readonly: false // Set to true to restrict editing
});
-
TinyMCE
-
HTML
-
JS
-
Edit on CodePen
<textarea id="suggestededits-access-read" suggestededits-model='{"history":{"2":[{"id":1,"uid":"james-wilson","timestamp":1752576936000,"feedback":"Nice improvement!"}]},"version":1,"contents":[{"type":"p","children":[{"type":"img","attrs":{"style":"display: block; margin-left: auto; margin-right: auto;","title":"Tiny Logo","src":"https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png","alt":"TinyMCE Logo","width":"128","height":"128"}}]},{"type":"h2","attrs":{"style":"text-align: center;"},"children":[{"text":"Welcome to the TinyMCE Suggested Edits "},{"text":"interactive ","opData":{"id":1,"type":"insert","uid":"alex-thompson","timestamp":1752015064000}},{"text":"demo!"}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"Try out the Suggested Edits feature"},{"text":" by typing in the editor and t","opData":{"id":2,"type":"remove","uid":"alex-thompson","timestamp":1752415064000}},{"text":": type in the editor, apply formatting or delete some content. T","opData":{"id":2,"type":"insert","uid":"alex-thompson","timestamp":1752415064000}},{"text":"hen"},{"text":",","opData":{"id":3,"type":"insert","uid":"alex-thompson","timestamp":1752515064000}},{"text":" click"},{"text":"ing","opData":{"id":4,"type":"remove","uid":"alex-thompson","timestamp":1752515064000}},{"text":" the Review edits button in the toolbar"},{"text":" to see your changes","opData":{"id":5,"type":"insert","uid":"kai-nakamura","timestamp":1752615064000}},{"text":"."}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"And visit the "},{"text":"pricing page","opData":{"id":6,"type":"modify","uid":"kai-nakamura","timestamp":1752615064000},"format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}}],"oldFormat":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}},"em"]},{"text":" to learn more about our Premium plugins."}]},{"type":"h2","children":[{"text":"A simple table to play with"}]},{"type":"table","attrs":{"style":"border-collapse: collapse; width: 100%;","border":"1"},"children":[{"type":"thead","children":[{"type":"tr","attrs":{"style":"text-align: left;"},"children":[{"type":"th","children":[{"text":"Product"}]},{"type":"th","children":[{"text":"Cost"}]},{"type":"th","children":[{"text":"Really?"}]}]}]},{"type":"tbody","children":[{"type":"tr","children":[{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"TinyMCE Cloud"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Get started for free"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Yes!","format":["strong"]}]}]}]}]},{"type":"h2","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"children":[{"text":"Found a bug?"}]},{"type":"p","children":[{"text":" ","opData":{"id":7,"type":"remove","uid":"mia-andersson","timestamp":1752576331000}},{"text":"If you believe you have found a bug please create an issue on the ","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}},{"text":"GitHub repo","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"format":[{"type":"a","attrs":{"href":"https://github.com/tinymce/tinymce/issues"}}]},{"text":" to report it to the developers.","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}}]},{"type":"h2","children":[{"text":"Finally…"}]},{"type":"p","children":[{"text":"Need file uploads in your app? Consider using "},{"text":"Uploadcare","format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/docs/tinymce/latest/uploadcare/","target":"_blank","rel":"noopener noreferrer"}}]},{"text":" with TinyMCE for a fast, modern upload experience."}]},{"type":"p","children":[{"text":"Thanks for supporting TinyMCE. We hope it helps you and your users create great content."}]},{"type":"p","children":[{"text":"All the best from the TinyMCE team."}]}]}'>
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE Suggested Edits interactive demo!</h2>
<p style="text-align: center;">Try out the Suggested Edits feature: type in the editor, apply formatting or delete some content. Then, click the Review edits button in the toolbar to see your changes.</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 style="text-align: center;">TinyMCE Cloud</td>
<td style="text-align: center;">Get started for free</td>
<td style="text-align: center;"><strong>Yes!</strong></td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you believe 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>Need file uploads in your app? Consider using <a href="https://www.tiny.cloud/docs/tinymce/latest/uploadcare/" target="_blank" rel="noopener noreferrer">Uploadcare</a> with TinyMCE for a fast, modern upload experience.</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 API_URL = 'https://demouserdirectory.tiny.cloud/v1/users';
const tinymceElement = document.querySelector('textarea#suggestededits-access-read');
const model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits-access-read',
height: 500,
plugins: 'suggestededits advlist anchor autolink code charmap emoticons fullscreen help image link lists media preview searchreplace table',
toolbar: 'undo redo | suggestededits | styles fontsizeinput | bold italic | align bullist numlist | table link image | code',
readonly: false, // Set to true to prevent edits to the content
suggestededits_model: model,
suggestededits_access: 'read', // Set this value to restrict the permissions in the Suggested Edits view
suggestededits_content: 'html',
user_id: 'kai-nakamura',
fetch_users: (userIds) => Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId }))))
});
suggestededits_auto_approve
The suggestededits_auto_approve option controls whether edits made during the session by the current user are automatically approved without requiring manual review. With this option enabled any edits by the current user, as set by the user_id option, as well as any previous suggestions made by the current user, will be instantly applied, bypassing the review process. This is useful for scenarios where reviews aren’t necessary, such as when creating an initial draft or when the user is a trusted editor.
When set to:
-
false: All edits require manual approval before being applied. -
true: All new edits are automatically approved and applied.
In TinyMCE 8.7, this option controls the default state of the suggestededits-tracking toolbar button and menu item. When set to false, the suggestededits-tracking button will be active by default and edits will appear in the review. When set to true, the button will be inactive by default, meaning edits will not appear in the review. Toggling the button or menu item will override the default state set by this option for the current session.
|
Type: Boolean
Default Value: false
suggestededits_auto_approve: truetinymce.init({
selector: 'textarea#suggestededits', // Change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits suggestededits-tracking',
suggestededits_auto_approve: true, // Change this value to auto-approve suggested edits
});
-
TinyMCE
-
HTML
-
JS
-
Edit on CodePen
<textarea id="suggestededits-auto-approve" suggestededits-model='{"history":{"2":[{"id":1,"uid":"james-wilson","timestamp":1752576936000,"feedback":"Nice improvement!"}]},"version":1,"contents":[{"type":"p","children":[{"type":"img","attrs":{"style":"display: block; margin-left: auto; margin-right: auto;","title":"Tiny Logo","src":"https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png","alt":"TinyMCE Logo","width":"128","height":"128"}}]},{"type":"h2","attrs":{"style":"text-align: center;"},"children":[{"text":"Welcome to the TinyMCE Suggested Edits "},{"text":"interactive ","opData":{"id":1,"type":"insert","uid":"alex-thompson","timestamp":1752015064000}},{"text":"demo!"}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"Try out the Suggested Edits feature"},{"text":" by typing in the editor and t","opData":{"id":2,"type":"remove","uid":"alex-thompson","timestamp":1752415064000}},{"text":": type in the editor, apply formatting or delete some content. T","opData":{"id":2,"type":"insert","uid":"alex-thompson","timestamp":1752415064000}},{"text":"hen"},{"text":",","opData":{"id":3,"type":"insert","uid":"alex-thompson","timestamp":1752515064000}},{"text":" click"},{"text":"ing","opData":{"id":4,"type":"remove","uid":"alex-thompson","timestamp":1752515064000}},{"text":" the Review edits button in the toolbar"},{"text":" to see your changes","opData":{"id":5,"type":"insert","uid":"kai-nakamura","timestamp":1752615064000}},{"text":"."}]},{"type":"p","attrs":{"style":"text-align: center;"},"children":[{"text":"And visit the "},{"text":"pricing page","opData":{"id":6,"type":"modify","uid":"kai-nakamura","timestamp":1752615064000},"format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}}],"oldFormat":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/pricing"}},"em"]},{"text":" to learn more about our Premium plugins."}]},{"type":"h2","children":[{"text":"A simple table to play with"}]},{"type":"table","attrs":{"style":"border-collapse: collapse; width: 100%;","border":"1"},"children":[{"type":"thead","children":[{"type":"tr","attrs":{"style":"text-align: left;"},"children":[{"type":"th","children":[{"text":"Product"}]},{"type":"th","children":[{"text":"Cost"}]},{"type":"th","children":[{"text":"Really?"}]}]}]},{"type":"tbody","children":[{"type":"tr","children":[{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"TinyMCE Cloud"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Get started for free"}]},{"type":"td","attrs":{"style":"text-align: center;"},"children":[{"text":"Yes!","format":["strong"]}]}]}]}]},{"type":"h2","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"children":[{"text":"Found a bug?"}]},{"type":"p","children":[{"text":" ","opData":{"id":7,"type":"remove","uid":"mia-andersson","timestamp":1752576331000}},{"text":"If you believe you have found a bug please create an issue on the ","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}},{"text":"GitHub repo","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000},"format":[{"type":"a","attrs":{"href":"https://github.com/tinymce/tinymce/issues"}}]},{"text":" to report it to the developers.","opData":{"id":7,"type":"insert","uid":"mia-andersson","timestamp":1752576331000}}]},{"type":"h2","children":[{"text":"Finally…"}]},{"type":"p","children":[{"text":"Need file uploads in your app? Consider using "},{"text":"Uploadcare","format":[{"type":"a","attrs":{"href":"https://www.tiny.cloud/docs/tinymce/latest/uploadcare/","target":"_blank","rel":"noopener noreferrer"}}]},{"text":" with TinyMCE for a fast, modern upload experience."}]},{"type":"p","children":[{"text":"Thanks for supporting TinyMCE. We hope it helps you and your users create great content."}]},{"type":"p","children":[{"text":"All the best from the TinyMCE team."}]}]}'>
<p><img style="display: block; margin-left: auto; margin-right: auto;" title="Tiny Logo" src="https://www.tiny.cloud/docs/tinymce/latest/_images/logos/android-chrome-256x256.png" alt="TinyMCE Logo" width="128" height="128"></p>
<h2 style="text-align: center;">Welcome to the TinyMCE Suggested Edits interactive demo!</h2>
<p style="text-align: center;">Try out the Suggested Edits feature: type in the editor, apply formatting or delete some content. Then, click the Review edits button in the toolbar to see your changes.</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 style="text-align: center;">TinyMCE Cloud</td>
<td style="text-align: center;">Get started for free</td>
<td style="text-align: center;"><strong>Yes!</strong></td>
</tr>
</tbody>
</table>
<h2>Found a bug?</h2>
<p>If you believe 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>Need file uploads in your app? Consider using <a href="https://www.tiny.cloud/docs/tinymce/latest/uploadcare/" target="_blank" rel="noopener noreferrer">Uploadcare</a> with TinyMCE for a fast, modern upload experience.</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 API_URL = 'https://demouserdirectory.tiny.cloud/v1/users';
const tinymceElement = document.querySelector('textarea#suggestededits-auto-approve');
const model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits-auto-approve',
height: 500,
plugins: 'suggestededits advlist anchor autolink code charmap emoticons fullscreen help image link lists media preview searchreplace table',
toolbar: 'undo redo | suggestededits suggestededits-tracking | styles fontsizeinput | bold italic | align bullist numlist | table link image | code',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
suggestededits_model: model,
suggestededits_access: 'full',
suggestededits_content: 'html',
suggestededits_auto_approve: true,
user_id: 'kai-nakamura',
fetch_users: (userIds) => Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId }))))
});
suggestededits_ai_attribution
This option controls the display of the AI badge on suggestion cards. An AI badge appears on the cards of edits made with the TinyMCE AI plugin by default. Set to false to hide the badge.
| AI-assisted suggestions are always recorded in the model. This option controls only whether the AI badge is displayed. |
Type: Boolean
Default value: true
view_show
The view_show option can be used to show the Suggested Edits view when the editor is loaded.
tinymce.init({
selector: 'textarea', // change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
view_show: 'suggestededits',
// ... other Suggested Edits options (e.g. suggestededits_model, user_id, fetch_users)
});
user_id
This option sets the unique identifier for the current user in the editor. It is used in the the UserLookup API.
Type: String
Default value: 'Anonymous'
user_id optiontinymce.init({
selector: 'textarea', // Change this value according to your HTML
user_id: 'james-wilson' // replace this with a unique string to identify the user
});
fetch_users
A required callback function that fetches user data. This function is called with an array of user IDs and should return a Promise that resolves to an array of user objects. The callback is used by the UserLookup API. If the returned array does not include all requested user IDs, promises for the missing users will be rejected with a "User {id} not found" error.
Type: Function
Parameters:
- ids (Array<string>): An array of user IDs to fetch.
Returns:
- Promise<Array<Object>>: A promise that resolves to an array of user objects.
fetch_users optionconst API_URL = 'https://demouserdirectory.tiny.cloud/v1/users';
tinymce.init({
selector: 'textarea',
user_id: 'james-wilson',
fetch_users: (userIds) => Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId })))),
});
const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users';
tinymce.init({
selector: 'textarea',
user_id: 'james-wilson',
fetch_users: async (userIds) => {
const users = await Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId }))));
return userIds.map(
(userId) =>
users.find((user) => user.id === userId)
|| ({ id: userId }) // Still returns a valid user even if it wasn't returned from the server
);
},
});
Both the user_id and fetch_users options are required to configure the User Lookup API. This API is used in the Suggested Edits plugin to provide user information for each user who has made a change, allowing other users to see who made which suggestion.
-
The current user ID should be set with the
user_idoption. The user ID should be a unique string that identifies the user, such as a username or an email address. -
The
fetch_usersoption is required to provide the name and avatar for users who have made suggestions. This option can be configured to fetch data from a backend service. Thefetch_usersfunction is given an array of user IDs and should return a promise, which resolves to an array containing data for the requested user IDs.
Toolbar buttons
The Suggested Edits plugin provides the following toolbar buttons:
| Toolbar button identifier | Description |
|---|---|
|
Opens the Review edits view. |
|
Opens the Review edits view (this button uses text instead of an icon). |
|
Toggles the tracking of suggested edits. |
These toolbar buttons can be added to the editor using:
-
The
toolbarconfiguration option. -
The
quickbars_insert_toolbarconfiguration option.
Menu items
The Suggested Edits plugin provides the following menu items:
| Menu item identifier | Default Menu Location | Description |
|---|---|---|
|
View |
Opens the Review edits view. |
|
Not Applicable |
Toggles the tracking of suggested edits. |
These menu items can be added to the editor using:
-
The
menuconfiguration option. -
The
contextmenuconfiguration option.
Commands
The Suggested Edits plugin provides the following TinyMCE commands.
| Command | Description |
|---|---|
suggestededits |
Opens the Review edits view, or closes it without applying resolved suggestions if it is already open. The view opens only when there are suggestions to review and the user has at least |
suggestededitsToggleTracking |
Toggles the tracking of suggested edits. |
tinymce.activeEditor.execCommand('suggestededits');
tinymce.activeEditor.execCommand('suggestededitsToggleTracking');
tinymce.activeEditor.queryCommandState('suggestededitsToggleTracking'); // Returns true if tracking of suggested edits is enabled, otherwise false.
Events
The Suggested Edits plugin provides the following events.
The following events are provided by the Suggested Edits plugin.
| Name | Data | Description |
|---|---|---|
SuggestedEditsBeginReview |
N/A |
The Review edits view has opened. |
SuggestedEditsReviewComplete |
N/A |
A review in the Review edits view has been completed with Save & Exit. |
SuggestedEditsReviewCancelled |
N/A |
A review in the Review edits view ended without applying resolved suggestions. This event fires when the user selects Exit review, when the review closes through the |
SuggestedEditsHasChangesUpdate |
|
The Suggested Edits model is updated. The |
SuggestedEditsTrackingToggle |
|
The tracking state of suggested edits has changed. The |
APIs
The Suggested Edits plugin provides the following APIs.
| Name | Arguments | Description |
|---|---|---|
getModel |
N/A |
Returns a JSON object representing the current model of the document. |
setModel |
|
Sets the current model of the document. |
resetModel |
N/A |
Generates a model from the current content and sets it as the current model, clearing all suggestions. |
hasChanges |
N/A |
Returns a boolean value indicating whether the document contains any suggested edits. |
// Get the current model of the document
tinymce.activeEditor.plugins.suggestededits.getModel();
// Set current model of the document
tinymce.activeEditor.plugins.suggestededits.setModel(model);
// Reset the model to the current content, clearing all suggestions
tinymce.activeEditor.plugins.suggestededits.resetModel();
// Check if document contains changes to be reviewed
tinymce.activeEditor.plugins.suggestededits.hasChanges();
getModel ExampleThis example demonstrates how to save the current document and model to ensure they are saved synchronously. The current model is retrieved using the getModel API.
const tinymceElement = document.querySelector('textarea#suggestededits');
const suggestededits_model = JSON.parse(tinymceElement.getAttribute('suggestededits-model'));
tinymce.init({
selector: 'textarea#suggestededits', // change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits save',
suggestededits_model,
setup: (editor) => {
editor.on('AddUndo Undo Redo SuggestedEditsBeginReview SuggestedEditsReviewComplete SuggestedEditsReviewCancelled', () => {
const content = editor.getContent();
const model = editor.plugins.suggestededits.getModel();
tinymceElement.value = content;
tinymceElement.setAttribute('suggestededits-model', JSON.stringify(model));
});
}
});
setModel ExampleThis example demonstrates how to set the model and the document in the editor, in this case fetching them from a server. The setModel method sets the current model of the document and the editor content, as generated from that model.
tinymce.init({
selector: 'textarea#suggestededits', // change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
setup: (editor) => {
editor.on('init', () => {
// Fetch and set the suggested edits model
fetch(`/api/models/${documentId}`)
.then((response) => response.json())
.then((model) => {
editor.plugins.suggestededits.setModel(model);
})
.catch((error) => console.error(`Error fetching model for ${documentId}:`, error));
});
}
});
resetModel ExampleThis example demonstrates how to reset the model to the current content of the editor, clearing all suggestions. The resetModel method generates a new model from the current content and sets it as the current model.
tinymce.init({
selector: 'textarea#suggestededits', // change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits clearsuggestions',
setup: (editor) => {
editor.ui.registry.addButton('clearsuggestions', {
text: 'Clear Suggestions',
onAction: () => {
// Reset the model to the current content, clearing all suggestions
editor.plugins.suggestededits.resetModel();
editor.notificationManager.open({
text: 'All suggestions have been cleared.',
type: 'info',
timeout: 5000,
});
}
});
}
});
hasChanges ExampleThis example demonstrates how to check if there are any changes in the document that need to be reviewed before saving. The hasChanges method is used to determine if there are any unreviewed edits.
tinymce.init({
selector: 'textarea#suggestededits', // change this value according to your HTML
plugins: 'suggestededits',
toolbar: 'suggestededits',
setup: (editor) => {
editor.ui.registry.addButton('save', {
text: 'Save',
onAction: () => {
// Get the current model of the document
const hasChanges = editor.plugins.suggestededits.hasChanges();
if (hasChanges) {
editor.notificationManager.open({
text: 'There are changes to be reviewed.',
type: 'warning',
timeout: 5000,
});
} else {
// Insert save logic here
}
}
});
}
});