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

<textarea id="suggestededits">
  <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 Suggested Edits demo!</h2>

  <p style="text-align: center;">Try out the Suggested Edits feature by typing in the editor and then clicking the Review Changes button 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>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>Don’t forget to check out <a href="http://www.plupload.com" target="_blank">Plupload</a>, the 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 tinymceElement = document.querySelector('textarea#suggested-edits');
const model = tinymceElement.getAttribute('suggestededits-model');

tinymce.init({
  selector: 'textarea#suggested-edits',
  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',
  user_id: 'michaelcook',
  fetch_users: (userIds) => Promise.all(userIds
    .map((userId) =>
      fetch(`/users/${userId}`) // Fetch user data from the server
      .then((response) => response.json())
      .catch(() => ({ id: userId })) // Still return a valid user object even if the fetch fails
  )),
  content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', 
  suggestededits_model: model,
  suggestededits_access: 'full',
  suggestededits_content: 'html'
});
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 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 is accessible via either the suggestededits toolbar button or menu button within 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.

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 applied as a suggested 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 can be used to view and reviewing edits made by multiple authors. The available actions in this view depends on the suggestededits_access option.

The view contains a few controls to manage the review process:

  • Show edits: Toggles whether suggested edits are shown. When hidden, the view shows what the document will look like if the review is completed.

  • Complete review: Ends the review, applying resolved suggestions to the document. Unresolved suggestions remain in the document for future review.

  • Cancel: Ignores any resolved suggestions and makes no change to the document. Feedback given on suggestions will be retained.

Document

The current editor document is displayed in a sandboxed iframe, with each suggested edit highlighted as they appear in the document. The following color coding is used to indicate the type of change:

  • Green: Added content.

  • Blue: Modified attributes or formatting (e.g. bold, italic, etc.).

  • Red: Removed content.

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.

Each suggested edit is listed as a card in the sidebar and color coded by the type of change, along with the user who made the suggestion, when the edit was made, and any feedback provided on that suggestion. When selected, each suggestion can be handled in the following ways:

  • 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.

  • Revert: Reverts the current "Accept" or "Reject" resolution on the suggestion.

  • Provide feedback: Opens a text area for users to provide feedback on the suggestion.

Feedback is shown in chronological order beneath the card details when the 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.

At the top of the sidebar there is a dropdown menu to apply review actions in bulk to all suggested edits:

  • Accept all.

  • Reject all.

  • Revert all.

Finishing a review

When completing a review, 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 is canceled, 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 suggestededits to the plugins option in the editor configuration;

  • add suggestededits to the toolbar option 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

Example: using suggestededits_model
await fetch(`/models/${documentId}`)
  .then((response) => response.json())
  .then((model) => {
    tinymce.init({
      selector: 'textarea',  // 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 the suggestededits_model option 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'

Example: using suggestededits_content
tinymce.init({
  selector: 'textarea',  // Change this value according to your HTML
  plugins: 'suggestededits',
  toolbar: 'suggestededits',
  suggestededits_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 Suggested 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 Suggested Edits view, with permission to accept or reject suggestions.

  • 'feedback': The user has access to the Suggested Edits view, with permission to add feedback to suggestions.

  • 'read': The user has read-only access to the Suggested Edits view.

  • 'none': The user has no access to the Suggested Edits view.

Type: String

Possible Values: 'full', 'feedback', 'read', 'none'

Default Value: 'full'

Example: suggestededits_access: 'feedback'
tinymce.init({
  selector: 'textarea',  // Change this value according to your HTML
  plugins: 'suggestededits',
  toolbar: 'suggestededits',
  suggestededits_access: 'feedback', // Change this value to set the {pluginname} view permissions
  readonly: false // Set to true to restrict editing
});
  • TinyMCE

  • HTML

  • JS

<textarea id="suggestededits-access-feedback">
  <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 Suggested Edits demo!</h2>

  <p style="text-align: center;">Try out the Suggested Edits feature by typing in the editor and then clicking the Review Changes button 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>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>Don’t forget to check out <a href="http://www.plupload.com" target="_blank">Plupload</a>, the 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 tinymceElement = document.querySelector('textarea#suggested-edits');
const model = tinymceElement.getAttribute('suggestededits-model');

tinymce.init({
  selector: 'textarea#suggested-edits',
  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',
  user_id: 'michaelcook',
  fetch_users: (userIds) => Promise.all(userIds
    .map((userId) =>
      fetch(`/users/${userId}`) // Fetch user data from the server
      .then((response) => response.json())
      .catch(() => ({ id: userId })) // Still return a valid user object even if the fetch fails
  )),
  content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
  readonly: false, // Set to true to prevent edits to the content
  suggestededits_access: 'feedback', // Set this value to restrict the permissions in the Suggested Edits view
  suggestededits_content: 'html',
  suggestededits_model: model
});
Example: suggestededits_access: 'read'
tinymce.init({
  selector: 'textarea',  // Change this value according to your HTML
  plugins: 'suggestededits',
  toolbar: 'suggestededits',
  suggestededits_access: 'read', // Change this value to set the {pluginname} view permissions
  readonly: false // Set to true to restrict editing
});
  • TinyMCE

  • HTML

  • JS

<textarea id="suggestededits-access-read">
  <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 Suggested Edits demo!</h2>

  <p style="text-align: center;">Try out the Suggested Edits feature by typing in the editor and then clicking the Review Changes button 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>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>Don’t forget to check out <a href="http://www.plupload.com" target="_blank">Plupload</a>, the 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 tinymceElement = document.querySelector('textarea#suggested-edits');
const model = tinymceElement.getAttribute('suggestededits-model');

tinymce.init({
  selector: 'textarea#suggested-edits',
  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',
  user_id: 'michaelcook',
  fetch_users: (userIds) => Promise.all(userIds
    .map((userId) =>
      fetch(`/users/${userId}`) // Fetch user data from the server
      .then((response) => response.json())
      .catch(() => ({ id: userId })) // Still return a valid user object even if the fetch fails
  )),
  content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }',
  readonly: false, // Set to true to prevent edits to the content
  suggestededits_access: 'read', // Set this value to restrict the permissions in the Suggested Edits view
  suggestededits_content: 'html',
  suggestededits_model: model
});

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'

Example: using user_id option
tinymce.init({
  selector: 'textarea',  // Change this value according to your HTML
  user_id: 'alextaylor' // 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.

Example: using fetch_users option
tinymce.init({
  selector: '#editor',
  user_id: 'alextaylor',
  fetch_users: (userIds) => Promise.all(userIds
    .map((userId) =>
      fetch(`/users/${userId}`) // Fetch user data from the server
      .then((response) => response.json())
      .catch(() => ({ id: userId })) // Still return a valid user object even if the fetch fails
  )),
});
Example: returning user array with validation
tinymce.init({
    selector: '#editor',
    user_id: 'alextaylor',
    fetch_users: async (userIds) => {
        const users = await fetch('/users', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ userIds })
        })
        .then((response) => response.json())
        .catch(() =>
            userIds.map((userId) =>
                ({ id: userId }) // Still returns valid users even if the fetch fails
            )
        );

        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_id option. The user ID should be a unique string that identifies the user, such as a username or an email address.

  • The fetch_users option 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. The fetch_users function 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

suggestededits

Opens the Review edits view.

suggestededits-label

Opens the Review edits view (this button uses text instead of an icon).

These toolbar buttons can be added to the editor using:

The Suggested Edits plugin provides the following menu items:

Menu item identifier Default Menu Location Description

suggestededits

View

Opens the Suggested Edits view.

These menu items can be added to the editor using:

Commands

The Suggested Edits plugin provides the following TinyMCE commands.

Command Description

suggestededits

Toggles the Suggested Edits view.

Example
tinymce.activeEditor.execCommand('suggestededits');

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 Suggested Edits view has opened.

SuggestedEditsReviewComplete

N/A

A review in the Suggested Edits view has been completed.

SuggestedEditsReviewCancelled

N/A

A review in the Suggested Edits view was cancelled.

SuggestedEditsHasChangesUpdate

hasChanges

The Suggested Edits model is updated. The hasChanges data is a boolean value indicating whether there are suggestions to review.

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

Object

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.

Examples
// 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 Example

This example demonstrates how to submit the current document and model to a server, to ensure they are saved synchronously. The current model is retrieved using the getModel API.

tinymce.init({
  selector: 'textarea#suggestededits',  // change this value according to your HTML
  plugins: 'suggestededits',
  toolbar: 'suggestededits save',
  suggestededits_model,
  setup: (editor) => {
    editor.ui.registry.addButton('save', {
      text: 'Save',
      onAction: () => {
        // Get the current content of the editor
        const content = editor.getContent();

        // Get the current model of the document
        const model = editor.plugins.suggestededits.getModel();

        fetch(`/api/documents/${documentId}`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: content,
        })
        .then((response) => response.json())
        .then((data) => console.log('Document saved:', data))
        .catch((error) => console.error('Error saving document:', error));

        // Save the model to the server
        fetch(`/api/models/${documentId}`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(model),
        })
        .then((response) => response.json())
        .then((data) => console.log('Model saved:', data))
        .catch((error) => console.error('Error saving model:', error));
      }
    });
  }
});
setModel Example

This example demonstrates how to set the model and the document in the editor, after 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 Example

This 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 Example

This 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
        }
      }
    });
  }
});