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, and lookup) are configured differently depending upon the server-side storage configuration.
Interactive example
The following example uses a simulated server (provided by Polly.js) which has been hidden from the example javascript to keep the example code concise. The interactions between TinyMCE and Polly.js are visible in the browser console.
-
TinyMCE
-
HTML
-
JS
<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 then attached to the text, exactly like this!</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>
/********************************
* Tiny Comments functions *
* (must call "done" or "fail") *
********************************/
const tinycomments_create = (req, done, fail) => {
const { content, createdAt } = req;
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((req2) => {
const conversationUid = req2.conversationUid;
done({ conversationUid });
})
.catch((e) => {
fail(e);
});
};
const tinycomments_reply = (req, done, fail) => {
const { conversationUid, content, createdAt } = req;
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((req2) => {
const commentUid = req2.commentUid;
done({ commentUid });
})
.catch((e) => {
fail(e);
});
};
const tinycomments_edit_comment = (req, done, fail) => {
const { conversationUid, commentUid, content, modifiedAt } = req;
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((req2) => {
const canEdit = req2.canEdit;
done({ canEdit });
})
.catch((e) => {
fail(e);
});
};
const tinycomments_delete = (req, done, fail) => {
const conversationUid = req.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...'));
}
});
};
const tinycomments_delete_all = (_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...'));
}
});
};
const tinycomments_delete_comment = (req, done, fail) => {
const { conversationUid, commentUid } = req;
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...'));
}
});
};
const tinycomments_lookup = ({ 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) => ({
...comment,
content: comment.content,
authorName: getUser(comment.author)?.displayName,
})),
},
};
};
lookup()
.then((data) => {
console.log('Lookup success ' + conversationUid, data);
done(data);
})
.catch((err) => {
console.error('Lookup failure ' + conversationUid, err);
fail(err);
});
};
tinymce.init({
selector: 'textarea#comments-callback',
height: 800,
plugins: 'code tinycomments help lists',
toolbar:
'undo redo | blocks | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | addcomment showcomments | help',
menubar: 'file edit view insert format tc',
menu: {
tc: {
title: 'Comments',
items: 'addcomment showcomments deleteallconversations',
},
},
tinycomments_create,
tinycomments_reply,
tinycomments_edit_comment,
tinycomments_delete,
tinycomments_delete_all,
tinycomments_delete_comment,
tinycomments_lookup,
/* The following setup callback opens the comments sidebar when the editor loads */
setup: (editor) => {
editor.on('SkinLoaded', () => {
editor.execCommand('ToggleSidebar', false, 'showcomments');
});
},
});
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 (create, reply, and edit) functions require an id
identifying the current author.
- Current author
-
The Comments plugin does not know the name of the current user. Determining the current user and storing the comment related to that user, has to be configured by the developer.
After a user comments (triggering tinycomments_create
for the first comment, or tinycomments_reply
for subsequent comments), the Comments plugin requests the updated conversation using tinycomments_lookup
, which should now contain the additional comment with the proper author.
Options
Required options
When using callback mode, the Comments plugin requires callback functions for the following options:
The tinycomments_resolve
option is optional.
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_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.
The done
callback should accept the following object:
{
commentUid: string // the value of the new comment uid
}
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: commentUid });
})
.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_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_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 conversation the reply is targeting.
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_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_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_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_lookup
The Comments plugin uses the tinycomments_lookup
function to retrieve an 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. Thelookup
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: The author avatar feature is only available in TinyMCE 6.1 or higher and if provided:
|
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
});
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,
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 colours.
Refer to the documentation for building a skin using this repo.
For more information on configuring TinyMCE formats, refer to the formats section.