Configuring the Comments plugin in callback mode

Callback mode is the default mode for the Comments plugin. In the callback mode, callback functions are required to save user comments on a server. The Comments functions (create, reply, edit, delete comment, delete all conversations, resolve, lookup, and fetch conversations) are configured differently depending upon the server-side storage configuration.

How the Comments plugin works in callback mode

All options accept functions incorporating done and fail callbacks as parameters. The function return type is not important, but all functions must call exactly one of these two callbacks: fail or done.

  • The fail callback takes either a string or a JavaScript Error type.

  • The done callback takes an argument specific to each function.

Most functions (create, reply, and edit) require an id identifying the current author. This can be provided directly within the callbacks or dynamically via the tinycomments_fetch_author_info option.

Current author: By default, the Comments plugin does not know the name of the current user. To display the correct author information when creating a new conversation, integrators can now use the tinycomments_fetch_author_info option to supply author details dynamically through a callback.

During the initial editor load, the Comments uses tinycomments_fetch callback to retrieve the existing conversations in the document. If not configured, the Comments will fallback to tinycomments_lookup.

When a user adds a comment or a reply, the Comments plugin uses the tinycomments_lookup callback to retrieve the selected conversation.

Interactive example

  • TinyMCE

  • HTML

  • JS

  • Edit on CodePen

<textarea id="comments-callback">
  <h2>Welcome to Tiny Comments!</h2>
  <p>Please try out this demo of our Tiny Comments premium plugin.</p>
  <ol>
    <li>Highlight the content you want to comment on.</li>
    <li>Click the <em>Add Comment</em> icon in the toolbar.</li>
    <li>Type your comment into the text field at the bottom of the Comment sidebar.</li>
    <li>Click <strong>Comment</strong>.</li>
  </ol>
  <p>Your comment is <span class="mce-annotation tox-comment" data-mce-annotation-uid="mce-conversation_420304606321716900864126" data-mce-annotation="tinycomments">then</span> attached to the text, <span class="mce-annotation tox-comment" data-mce-annotation-uid="mce-conversation_19679600221621399703915" data-mce-annotation="tinycomments">exactly like this!</span></p>
  <p>If you want to take Tiny Comments for a test drive in your own environment, Tiny Comments is one of the premium plugins you can try for free for 14 days by signing up for a Tiny account. Make sure to check out our documentation as well.</p>
  <h2>A simple table to play with</h2>
  <table style="border-collapse: collapse; width: 100%;" border="1">
    <thead>
      <tr>
        <th>Product</th>
        <th>Value</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><a href="https://www.tiny.cloud/">Tiny Cloud</a></td>
        <td>The easiest and most reliable way to integrate powerful rich text editing into your application.</td>
      </tr>
      <tr>
        <td><a href="https://www.tiny.cloud/drive/">Tiny Drive</a></td>
        <td>Image and file management for TinyMCE in the cloud.</td>
      </tr>
    </tbody>
  </table>
  <p>Thanks for supporting TinyMCE! We hope it helps your users create great content.</p>
</textarea>
/********************************
 *   Demo-specific configuration  *
 ********************************/

const userDb = {
  'michaelcook': {
    id: 'michaelcook',
    name: 'Michael Cook',
    fullName: 'Michael Cook',
    description: 'Product Owner',
    image: "../_images/avatars/michaelcook.png"
  },
  'kalebwilson': {
    id: 'kalebwilson',
    name: 'Kaleb Wilson',
    fullName: 'Kaleb Wilson',
    description: 'Marketing Director',
    image: "../_images/avatars/kalebwilson.png"
  }
};

const currentUid = 'kalebwilson';
const adminUid = 'michaelcook';

const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString();
const anhourago = new Date(now.getTime() - 60 * 60 * 1000).toISOString();

const fillAuthorInfo = (id, fullName, image) => ({
  author: id,
  authorName: fullName,
  authorAvatar: image,
});

const getAuthorInfo = (uid) => {
  const user = userDb[uid];
  if (user) {
    return fillAuthorInfo(user.id, user.fullName, user.image);
  }
  return {
    author: uid,
    authorName: uid,
  };
};

const conversationDb = {
  'mce-conversation_19679600221621399703915': {
    uid: 'mce-conversation_19679600221621399703915',
    comments: [{
      uid: 'mce-conversation_19679600221621399703915',
      ...getAuthorInfo(currentUid),
      content: `What do you think about this?`,
      createdAt: yesterday,
      modifiedAt: yesterday
    }, {
      uid: 'mce-conversation_19679600221621399703917',
      ...getAuthorInfo(adminUid),
      content: `I think this is a great idea!`,
      createdAt: anhourago,
      modifiedAt: anhourago,
    }]
  },
  'mce-conversation_420304606321716900864126': {
    uid: 'mce-conversation_420304606321716900864126',
    comments: [{
      uid: 'mce-conversation_420304606321716900864126',
      ...getAuthorInfo(adminUid),
      content: `Please revise this sentence, exclamation points are unprofessional!`,
      createdAt: yesterday,
      modifiedAt: anhourago
    }]
  }
};

const fakeDelay = 200;
const randomString = () => crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14);

const resolvedConversationDb = {};

/********************************
 *   Tiny Comments functions    *
 * (must call "done" or "fail") *
 ********************************/

const tinycomments_create = (req, done, fail) => {
  if (req.content === 'fail') {
    fail(new Error('Something has gone wrong...'));
  } else {
    const uid = 'annotation-' + randomString();
    conversationDb[uid] = {
      uid,
      comments: [{
        uid,
        ...getAuthorInfo(currentUid),
        content: req.content,
        createdAt: req.createdAt,
        modifiedAt: req.createdAt
      }]
    };
    setTimeout(() => done({ conversationUid: uid }), fakeDelay);
  }
};

const tinycomments_reply = (req, done) => {
  const replyUid = 'annotation-' + randomString();
  conversationDb[req.conversationUid].comments.push({
    uid: replyUid,
    ...getAuthorInfo(currentUid),
    content: req.content,
    createdAt: req.createdAt,
    modifiedAt: req.createdAt
  });
  setTimeout(() => done({ commentUid: replyUid }), fakeDelay);
};

const tinycomments_delete = (req, done) => {
  if (currentUid === adminUid) { // Replace wth your own logic, e.g. check if user created the conversation
    delete conversationDb[req.conversationUid];
    setTimeout(() => done({ canDelete: true }), fakeDelay);
  } else {
    setTimeout(() => done({ canDelete: false, reason: 'Must be admin user' }), fakeDelay);
  }
};

const tinycomments_resolve = (req, done) => {
  const conversation = conversationDb[req.conversationUid];
  if (currentUid === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges
    delete conversationDb[req.conversationUid];
    setTimeout(() => done({ canResolve: true }), fakeDelay);
  } else {
    setTimeout(() => done({ canResolve: false, reason: 'Must be conversation author' }), fakeDelay);
  }
};

const tinycomments_delete_comment = (req, done) => {
  const oldcomments = conversationDb[req.conversationUid].comments;
  let reason = 'Comment not found';

  const newcomments = oldcomments.filter((comment) => {
    if (comment.uid === req.commentUid) { // Found the comment to delete
      if (currentUid === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges
        return false; // Remove the comment
      } else {
        reason = 'Not authorised to delete this comment'; // Update reason
      }
    }
    return true; // Keep the comment
  });
  if (newcomments.length === oldcomments.length) {
    setTimeout(() => done({ canDelete: false, reason }), fakeDelay);
  } else {
    conversationDb[req.conversationUid].comments = newcomments;
    setTimeout(() => done({ canDelete: true }), fakeDelay);
  }
};

const tinycomments_edit_comment = (req, done) => {
  const oldcomments = conversationDb[req.conversationUid].comments;
  let reason = 'Comment not found';
  let canEdit = false;

  const newcomments = oldcomments.map((comment) => {
    if (comment.uid === req.commentUid) { // Found the comment to delete
      if (currentUid === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges
        canEdit = true; // User can edit the comment
        return { ...comment, content: req.content, modifiedAt: new Date().toISOString() }; // Update the comment
      } else {
        reason = 'Not authorised to edit this comment'; // Update reason
      }
    }
    return comment; // Keep the comment
  });

  if (canEdit) {
    conversationDb[req.conversationUid].comments = newcomments;
    setTimeout(() => done({ canEdit }), fakeDelay);
  } else {
    setTimeout(() => done({ canEdit, reason }), fakeDelay);
  }
};

const tinycomments_delete_all = (req, done) => {
  const conversation = conversationDb[req.conversationUid];
  if (currentUid === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges
    delete conversationDb[req.conversationUid];
    setTimeout(() => done({ canDelete: true }), fakeDelay);
  } else {
    setTimeout(() => done({ canDelete: false, reason: 'Must be conversation author' }), fakeDelay);
  }
};

const tinycomments_lookup = (req, done) => {
  setTimeout(() => {
    done({
      conversation: {
        uid: conversationDb[req.conversationUid].uid,
        comments: [...conversationDb[req.conversationUid].comments]
      }
    });
  }, fakeDelay);
};

const tinycomments_fetch = (conversationUids, done) => {
  const fetchedConversations = {};
  conversationUids.forEach((uid) => {
    const conversation = conversationDb[uid];
    if (conversation) {
      fetchedConversations[uid] = {...conversation};
    }
  });
  setTimeout(() => done({ conversations: fetchedConversations }), fakeDelay);
};

// Read the above `getAuthorInfo` function to see how this could be implemented
const tinycomments_fetch_author_info = (done) => done(getAuthorInfo(currentUid));

tinymce.init({
  selector: 'textarea#comments-callback',
  license_key: 'gpl',
  height: 800,
  toolbar: 'addcomment showcomments code | bold italic underline',
  menubar: 'file edit view insert format tools tc help',
  menu: {
    tc: {
      title: 'TinyComments',
      items: 'addcomment showcomments deleteallconversations'
    }
  },
  plugins: ['tinycomments', 'help', 'code', 'quickbars', 'link', 'lists', 'image'],
  quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments',
  quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions',
  sidebar_show: 'showcomments',
  tinycomments_mode: 'callback',
  tinycomments_create,
  tinycomments_reply,
  tinycomments_delete,
  tinycomments_resolve,
  tinycomments_delete_all,
  tinycomments_lookup,
  tinycomments_delete_comment,
  tinycomments_edit_comment,
  tinycomments_fetch,
  tinycomments_fetch_author_info,
});

Options

Required options

When using callback mode, the Comments plugin requires callback functions for the following options:

tinycomments_create

The Comments plugin uses the tinycomments_create function to create a comment.

The tinycomments_create function saves the comment as a new conversation and returns a unique conversation ID via the done callback. If an unrecoverable error occurs, it should indicate this with the fail callback.

The tinycomments_create function is given a request (req) object as the first parameter, which has these fields:

  • content: The content of the comment to create.

  • createdAt: The date the comment was created.

The done callback should accept the following object:

{
  conversationUid: string, // the new conversation uid
  // Optional error callback which will be run if the new conversation could not be created
  onError: (err) => { ... },
  // Optional success callback which will be run when the new conversation is successfully created
  onSuccess: (uid) => { ... }
}
For example:
const create_comment = (ref, done, fail) => {
  const { content, createdAt } = ref;

  fetch('https://api.example/conversations/', {
    method: 'POST',
    body: JSON.stringify({ content: content, createdAt: createdAt }),
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
  })
    .then((response) => {
      if (!response.ok) {
        throw new Error('Failed to create comment');
      }
      return response.json();
    })
    .then((ref2) => {
      let conversationUid = ref2.conversationUid;
      done({ conversationUid: conversationUid });
    })
    .catch((e) => {
      fail(e);
    });
}

tinymce.init({
  selector: 'textarea', // change this value according to your HTML
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment, // Add the callback to TinyMCE
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Optional callback
});

tinycomments_reply

The Comments plugin uses the tinycomments_reply function to reply to a comment.

The tinycomments_reply function saves the comment as a reply to an existing conversation and returns via the done callback once successful. Unrecoverable errors are communicated to TinyMCE by calling the fail callback instead.

The tinycomments_reply function is given a request (req) object as the first parameter, which has these fields:

  • conversationUid: The uid of the conversation the reply is targeting.

  • content: The content of the comment to create.

  • createdAt: The date the comment was created (ISO 8601 date string) format.

The done callback should accept the following object:

{
  commentUid: string, // the new comment uid
  author: string, // the id of the current author
  authorName: string // the name of the current author
}
For example:
const reply_comment = (ref, done, fail) => {
  const { conversationUid, content, createdAt } = ref;

  fetch(`https://api.example/conversations/${conversationUid}`, {
    method: 'POST',
    body: JSON.stringify({ content: content, createdAt: createdAt }),
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
  })
    .then((response) => {
      if (!response.ok) {
        throw new Error('Failed to reply to comment');
      }
      return response.json();
    })
    .then((ref2) => {
      let commentUid = ref2.commentUid;
      done({
        commentUid: replyUid,
        author: currentUser.id,
        authorName: currentUser.fullName
      });
    })
    .catch((e) => {
      fail(e);
    });
}

tinymce.init({
  selector: 'textarea', // change this value according to your HTML
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment, // Add the callback to TinyMCE
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Optional callback
});

tinycomments_edit_comment

The Comments plugin uses the tinycomments_edit_comment function to edit a comment.

The tinycomments_edit_comment function allows updating or changing existing comments and returns via the done callback once successful. Unrecoverable errors are communicated to TinyMCE by calling the fail callback instead.

The tinycomments_edit_comment function is given a request (req) object as the first parameter, which has these fields:

  • conversationUid: The uid of the conversation the reply is targeting.

  • commentUid: The uid of the comment to edit (it can be the same as conversationUid if editing the first comment in a conversation).

  • content: The content of the comment to create.

  • modifiedAt: The date the comment was modified.

The done callback should accept the following object:

{
  canEdit: boolean, // whether or not the Edit succeeded
  reason: string? // an optional string explaining why the edit was not allowed (if canEdit is false)
}
For example:
const edit_comment = (ref, done, fail) => {
  const { conversationUid, commentUid, content, modifiedAt } = ref;

  fetch(
    `https://api.example/conversations/${conversationUid}/${commentUid}`,
    {
      method: 'PUT',
      body: JSON.stringify({ content: content, modifiedAt: modifiedAt }),
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
    }
  )
    .then((response) => {
      if (!response.ok) {
        throw new Error('Failed to edit comment');
      }
      return response.json();
    })
    .then((ref2) => {
      let canEdit = ref2.canEdit;
      done({ canEdit: canEdit });
    })
    .catch((e) => {
      fail(e);
    });
}

tinymce.init({
  selector: 'textarea', // change this value according to your HTML
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment, // Add the callback to TinyMCE
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Optional callback
});

tinycomments_delete_comment

The tinycomments_delete_comment function should asynchronously return a flag indicating whether the comment or comment thread was removed using the done callback. Unrecoverable errors are communicated to TinyMCE by calling the fail callback instead.

The tinycomments_delete_comment function is given a request (req) object as the first parameter, which has these fields:

  • conversationUid: The uid of the conversation the reply is targeting.

  • commentUid: The uid of the comment to delete (cannot be the same as conversationUid).

The done callback should accept the following object:

{
  canDelete: boolean, // whether or not an individual comment can be deleted
  reason: string? // an optional reason explaining why the delete was not allowed (if canDelete is false)
}
Failure to delete due to permissions or business rules is indicated by "false", while unexpected errors should be indicated using the "fail" callback.
For example:
const delete_comment = (ref, done, fail) => {
  const { conversationUid, commentUid } = ref;

  fetch(
    `https://api.example/conversations/${conversationUid}/${commentUid}`,
    {
      method: 'DELETE',
    }
  ).then((response) => {
    if (response.ok) {
      done({ canDelete: true });
    } else if (response.status === 403) {
      done({ canDelete: false });
    } else {
      fail(new Error('Something has gone wrong...'));
    }
  });
}

tinymce.init({
  selector: 'textarea', // change this value according to your HTML
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread, // Add the callback to TinyMCE
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Optional callback
});

tinycomments_delete

The tinycomments_delete function should asynchronously return a flag indicating whether the comment thread was removed using the done callback. Unrecoverable errors are communicated to TinyMCE by calling the fail callback instead.

The tinycomments_delete function is passed a (req) object as the first parameter, which contains the following key-value pair:

  • conversationUid: The uid of the conversation the reply is targeting.

The done callback should accept the following object:

{
  canDelete: boolean // whether or not the conversation can be deleted
  reason: string? // an optional string explaining why the delete was not allowed (if canDelete is false)
}
Failure to delete due to permissions or business rules is indicated by "false", while unexpected errors should be indicated using the "fail" callback.

For example:

const delete_comment_thread = (ref, done, fail) => {
  const conversationUid = ref.conversationUid;
  fetch(`https://api.example/conversations/${conversationUid}`, {
    method: 'DELETE',
  }).then((response) => {
    if (response.ok) {
      done({ canDelete: true });
    } else if (response.status === 403) {
      done({ canDelete: false });
    } else {
      fail(new Error('Something has gone wrong...'));
    }
  });
}

tinymce.init({
  selector: '#editor',
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread, // Add the callback to TinyMCE
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Optional callback
});

tinycomments_delete_all

The tinycomments_delete_all function should asynchronously return a flag indicating whether all the comment threads were removed using the done callback. Unrecoverable errors are communicated to TinyMCE by calling the fail callback instead.

The tinycomments_delete_all function is given a request (req) object as the first parameter with no fields.

The done callback should accept the following object:

{
  canDelete: boolean, // whether or not all conversations can be deleted
  reason: string? // an optional string explaining why the deleteAll was not allowed (if canDelete is false)
}
Failure to delete due to permissions or business rules should be indicated by canDelete: false, while unexpected errors should be indicated using the fail callback.
For example:
const delete_all_comment_threads = (_req, done, fail) => {
  fetch('https://api.example/conversations', {
    method: 'DELETE',
  }).then((response) => {
    if (response.ok) {
      done({ canDelete: true });
    } else if (response.status === 403) {
      done({ canDelete: false });
    } else {
      fail(new Error('Something has gone wrong...'));
    }
  });
}

tinymce.init({
  selector: 'textarea', // change this value according to your HTML
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads, // Add the callback to TinyMCE
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Optional callback
});

tinycomments_lookup

The Comments plugin uses the tinycomments_lookup function to retrieve one existing conversation using a conversation’s unique ID.

The Display names configuration must be considered for the tinycomments_lookup function:

Display names

The Comments plugin uses a simple string for the display name. For the lookup function, Comments expects each comment to contain the author’s display name, not a user ID, as Comments does not know the user identities. The lookup function should be implemented considering this and resolve user identifiers to an appropriate display name.

The conventional conversation object structure that should be returned via the done callback is as follows:

The tinycomments_lookup function is passed a (req) object as the first parameter, which contains the following key-value pair:

conversationUid

The uid of the conversation the reply is targeting.

The done callback should accept the following object:

{
 conversation: {
   uid: string, // the uid of the conversation,
   comments: [
    {
      author: string, // author of first comment
      authorName: string, // optional - Display name to use instead of author. Defaults to using `author` if not specified
      authorAvatar: string, // optional - URL to the author's avatar. If not provided an automated avatar will be generated
      createdAt: date, // when the first comment was created
      content: string, // content of first comment
      modifiedAt: date, // when the first comment was created/last updated
      uid: string // the uid of the first comment in the conversation
    },
    {
      author: string, // author of second comment
      authorName: string, // optional - Display name to use instead of author. Defaults to using `author` if not specified
      authorAvatar: string, // optional - URL to the author's avatar. If not provided an automated avatar will be generated
      createdAt: date, // when the second comment was created
      content: string, // content of second comment
      modifiedAt: date, // when the second comment was created/last updated
      uid: string // the uid of the second comment in the conversation
    }
  ]
 }
}

The dates should use ISO 8601 format. This can be generated in JavaScript with: new Date().toISOString().

The author avatar feature is only available in TinyMCE 6.1 or higher and if provided:

  • will be scaled to a 36px diameter circle; and

  • can be any filetype able to be wrapped in an <img> element.

For example:
const lookup_comment = ({ conversationUid }, done, fail) => {
  const lookup = async () => {
    const convResp = await fetch(
      `https://api.example/conversations${conversationUid}`
    );
    if (!convResp.ok) {
      throw new Error('Failed to get conversation');
    }
    const comments = await convResp.json();
    const usersResp = await fetch('https://api.example/users/');
    if (!usersResp.ok) {
      throw new Error('Failed to get users');
    }
    const { users } = await usersResp.json();
    const getUser = (userId) => users.find((u) => u.id === userId);
    return {
      conversation: {
        uid: conversationUid,
        comments: comments.map((comment) => {
          const user = getUser(comment.author);
          return {
            ...comment,
            content: comment.content,
            authorName: user?.displayName,
            authorAvatar: user?.avatarUrl
          };
        })
      }
    };
  };

  lookup()
    .then((data) => {
      console.log(`Lookup success ${conversationUid}`, data);
      done(data);
    })
    .catch((err) => {
      console.error(`Lookup failure ${conversationUid}`, err);
      fail(err);
    });
};

tinymce.init({
  selector: '#editor',
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment, // Add the callback to TinyMCE
  tinycomments_fetch: fetch_comments // Optional callback
});

Optional options

tinycomments_resolve

This option adds a Resolve Conversation item to the dropdown menu of the first comment in a conversation.

The tinycomments_resolve function should asynchronously return a flag indicating whether the comment thread was resolved using the done callback. Unrecoverable errors are communicated to TinyMCE by calling the fail callback instead.

The tinycomments_resolve function is passed a (req) object as the first parameter, which contains the following key-value pair:

  • conversationUid: The uid of the targeting conversation

The done callback should accept the following object:

{
  canResolve: boolean // whether or not the conversation can be resolved
  reason?: string // an optional string explaining why resolving was not allowed (if canResolve is false)
}
Failure to resolve due to permissions or business rules should be indicated by canResolve: false, while unexpected errors should be indicated using the fail callback.
For example:
const resolve_comment_thread = (ref, done, fail) => {
  const conversationUid = ref.conversationUid;
  fetch(`https://api.example/conversations/${conversationUid}`, {
    method: 'PUT',
  }).then((response) => {
    if (response.ok) {
      done({ canResolve: true });
    } else if (response.status === 403) {
      done({ canResolve: false });
    } else {
      fail(new Error('Something has gone wrong...'));
    }
  });
}

tinymce.init({
  selector: 'textarea', // change this value according to your HTML
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_resolve: resolve_comment_thread, // Add the callback to TinyMCE
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments
});

tinycomments_fetch

The Comments plugin uses the tinycomments_fetch callback function retrieves multiple conversations based on their unique identifiers.

The conventional conversation object structure that should be returned via the done callback is as follows:

The tinycomments_fetch function is passed a (req) object as the first parameter, which contains the following key-value pair:

  • conversationUids: An array of strings representing the unique identifiers of the conversations to fetch.

The done callback should accept the following object:

{
  conversations: {
    [conversationUid: string]: {
      uid: string,
      comments: [
        {
          uid: string,
          author: string,
          authorName?: string,
          authorAvatar?: string,
          content: string,
          createdAt: string, // ISO 8601 date string
          modifiedAt: string // ISO 8601 date string
        },
        // ... more comments
      ]
    },
    // ... more conversations
  }
}
For example:
const tinycomments_fetch = (conversationUids, done, fail) => {
  const requests = conversationUids.map((uid) => fetch(`https://api.example/conversations/${uid}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      }})
      .then((response) => response.json())
      .then((data) => ({
        [uid]: {
          uid: uid,
          comments: data
        }
      }))
    );

  Promise.all(requests)
    .then((data) => {
      console.log('data', data);
      const conversations = data.reduce((conv, d) => ({
          ...conv,
          ...d
        })
      , {});
      console.log(`Fetch success ${conversationUids}`, conversations);
      done({ conversations });
    })
    .catch((err) => {
      console.error(`Fetch failure ${conversationUids}`, err);
      fail('Fetching conversations failed');
    });

tinymce.init({
  selector: '#editor',
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments // Add the callback to TinyMCE
});

tinycomments_fetch_author_info

The Comments plugin uses the tinycomments_fetch_author_info callback function to provide author details for comments displayed in the editor.

This function is called whenever a comment is rendered, and it allows the editor to display the author’s name and avatar based on the current user.

The tinycomments_fetch_author_info function is passed a done callback parameter.

The function must call done with an object containing the following properties:

  • author (string): A unique identifier for the user.

  • authorName (string): The full name to display for the user. (optional)

  • authorAvatar (string): A URL pointing to the user’s avatar image. (optional)

Example of tinycomments_fetch_author_info function
const tinycomments_fetch_author_info = (done) => {
  setTimeout(() => done({
    author: currentUser.id, // Maps to the `author` field in the comment object
    authorName: currentUser.fullName, // Maps to the `authorName` field in the comment object. (optional)
    authorAvatar: currentUser.image, // Maps to the `authorAvatar` field in the comment object. (optional)
  }), fakeDelay);
};
Example of tinycomments_fetch_author_info function with a custom author
const currentUser = {
  id: 'johnsmith',
  name: 'John Smith',
  fullName: 'John Smith',
  description: 'Company Founder',
  image: '{{imagesdir}}/users/johnsmith.png'
};

const tinycomments_fetch_author_info = (done) => {
  fetch('https://example.api.com/currentUser')
    .then(res => res.json())
    .then(currentUser => {
      done({
        author: currentUser.id, // Maps to the `author` field in the comment object
        authorName: currentUser.fullName, // Maps to the `authorName` field in the comment object. (optional)
        authorAvatar: currentUser.image, // Maps to the `authorAvatar` field in the comment object. (optional)
      });
    });
};

tinymce.init({
  selector: '#editor',
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create: create_comment,
  tinycomments_reply: reply_comment,
  tinycomments_edit_comment: edit_comment,
  tinycomments_delete: delete_comment_thread,
  tinycomments_delete_all: delete_all_comment_threads,
  tinycomments_delete_comment: delete_comment,
  tinycomments_lookup: lookup_comment,
  tinycomments_fetch: fetch_comments, // Optional callback
  tinycomments_fetch_author_info: fetch_comments
});

Show the Comments sidebar when TinyMCE loads

The sidebar_show option can be used to show the Comments sidebar when the editor is loaded.

For example:
tinymce.init({
  selector: 'textarea',  // change this value according to your html
  plugins: 'tinycomments',
  tinycomments_mode: 'callback',
  tinycomments_create,
  tinycomments_reply,
  tinycomments_edit_comment,
  tinycomments_delete,
  tinycomments_delete_all,
  tinycomments_delete_comment,
  tinycomments_lookup,
  tinycomments_fetch,
  sidebar_show: 'showcomments'
});

Configuring the commented text and block CSS properties

The highlight styles are now a part of the overall content skin and are changed through customizing the skin.

TinyMCE open source project oxide (default skin), defines the variables used for changing the annotation colors.

Refer to the documentation for building a skin using this repo.

For more information on configuring TinyMCE formats, refer to the formats section.