diff --git a/_data/nav.yml b/_data/nav.yml index aede89b21..6e1aacb0b 100644 --- a/_data/nav.yml +++ b/_data/nav.yml @@ -242,6 +242,13 @@ - url: "casechange" - url: "checklist" - url: "comments" + pages: + - url: "introduction_to_tiny_comments" + - url: "comments_using_comments" + - url: "comments_callback_mode" + - url: "comments_embedded_mode" + - url: "comments_toolbars_menus" + - url: "comments_commands_events_apis" - url: "mediaembed" - url: "export" pages: @@ -723,7 +730,6 @@ - url: "#Deploying an icon pack" - url: "creating-a-plugin" - url: "annotations" - - url: "configuring-comments-callbacks" - url: "yeoman-generator" - url: "creating-custom-notifications" - url: "php-upload-handler" diff --git a/_includes/configuration/contextmenu.md b/_includes/configuration/contextmenu.md index f29e0a0fc..5260e0ea6 100644 --- a/_includes/configuration/contextmenu.md +++ b/_includes/configuration/contextmenu.md @@ -29,6 +29,6 @@ tinymce.init({ }); ``` -{%if page.title != "Context menu" %} +{% if page.title != "Context menu" %} For information on configuring the `contextmenu` option and creating custom context menu items [context menu examples]({{site.baseurl}}/ui-components/contextmenu/). {% endif %} \ No newline at end of file diff --git a/_includes/live-demos/comments-callback/example.js b/_includes/live-demos/comments-callback/example.js new file mode 100644 index 000000000..5b2fe8988 --- /dev/null +++ b/_includes/live-demos/comments-callback/example.js @@ -0,0 +1,214 @@ +/******************************** + * Tiny Comments functions * + * (must call "done" or "fail") * + ********************************/ + +function tinycomments_create(req, done, fail) { + let content = req.content; + let createdAt = req.createdAt; + + 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) => { + let conversationUid = req2.conversationUid; + done({ conversationUid: conversationUid }); + }) + .catch((e) => { + fail(e); + }); +} + +function tinycomments_reply(req, done, fail) { + let conversationUid = req.conversationUid; + let content = req.content; + let createdAt = req.createdAt; + + 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) => { + let commentUid = req2.commentUid; + done({ commentUid: commentUid }); + }) + .catch((e) => { + fail(e); + }); +} + +function tinycomments_edit_comment(req, done, fail) { + let conversationUid = req.conversationUid; + let commentUid = req.commentUid; + let content = req.content; + let modifiedAt = req.modifiedAt; + + 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) => { + let canEdit = req2.canEdit; + done({ canEdit: canEdit }); + }) + .catch((e) => { + fail(e); + }); +} + +function tinycomments_delete(req, done, fail) { + let 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...')); + } + }); +} + +function 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...')); + } + }); +} + +function tinycomments_delete_comment(req, done, fail) { + let conversationUid = req.conversationUid; + let commentUid = req.commentUid; + + 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...')); + } + }); +} + +function tinycomments_lookup({ conversationUid }, done, fail) { + let lookup = async function () { + let convResp = await fetch( + 'https://api.example/conversations/' + conversationUid + ); + if (!convResp.ok) { + throw new Error('Failed to get conversation'); + } + let comments = await convResp.json(); + let usersResp = await fetch('https://api.example/users/'); + if (!usersResp.ok) { + throw new Error('Failed to get users'); + } + let { users } = await usersResp.json(); + let getUser = function (userId) { + return users.find((u) => { + return u.id === userId; + }); + }; + return { + conversation: { + uid: conversationUid, + comments: comments.map((comment) => { + return { + ...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#callback-mode', + height: 800, + plugins: 'paste code tinycomments help lists', + toolbar: + 'undo redo | formatselect | ' + + '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: function (editor) { + editor.on('SkinLoaded', () => { + editor.execCommand('ToggleSidebar', false, 'showcomments'); + }); + }, +}); diff --git a/_includes/live-demos/comments-callback/index.html b/_includes/live-demos/comments-callback/index.html new file mode 100644 index 000000000..50e9ecd42 --- /dev/null +++ b/_includes/live-demos/comments-callback/index.html @@ -0,0 +1,32 @@ + diff --git a/_includes/live-demos/comments-callback/index.js b/_includes/live-demos/comments-callback/index.js new file mode 100644 index 000000000..26d90e10a --- /dev/null +++ b/_includes/live-demos/comments-callback/index.js @@ -0,0 +1,686 @@ +tinymce.ScriptLoader.loadScripts( + [ + '//unpkg.com/@pollyjs/core@5.1.1', + '//unpkg.com/@pollyjs/adapter-fetch@5.1.1', + '//unpkg.com/@pollyjs/persister-local-storage@5.1.1', + ], + () => { + /****************************** + * Mock server implementation * + ******************************/ + + let { Polly } = window['@pollyjs/core']; + let FetchAdapter = window['@pollyjs/adapter-fetch']; + let LocalStoragePersister = window['@pollyjs/persister-local-storage']; + + Polly.register(FetchAdapter); + Polly.register(LocalStoragePersister); + let polly = new Polly('test', { + adapters: ['fetch'], + persister: 'local-storage', + }); + let server = polly.server; + + server.any().on('request', (req) => { + console.log('Server request:', req); + }); + + server.any().on('beforeResponse', (req, res) => { + console.log('Server response:', res); + }); + + /* this would be an admin for the file, they're allowed to do all operations */ + function getOwner() { + return localStorage.getItem('owner') ?? users[0].id; + } + + /* Server knows the author, probably by cookie or JWT token */ + function getAuthor() { + return localStorage.getItem('author') ?? users[0].id; + } + + /* this would be an admin for the file, they're allowed to do all operations */ + function setOwner(user) { + localStorage.setItem('owner', user) ?? users[0].id; + } + + /* Server knows the author, probably by cookie or JWT token */ + function setAuthor(user) { + localStorage.setItem('author', user) ?? users[0].id; + } + + function randomString() { + /* ~62 bits of randomness, so very unlikely to collide for <100K uses */ + return Math.random().toString(36).substring(2, 14); + } + + /* Our server "database" */ + function getDB() { + return JSON.parse(localStorage.getItem('fakedb') ?? '{}'); + } + function setDB(data) { + localStorage.setItem('fakedb', JSON.stringify(data)); + } + + function getConversation(uid) { + let store = getDB(); + console.log('DB get:', uid, store[uid]); + return store[uid]; + } + + function setConversation(uid, conversation) { + let store = getDB(); + console.log('DB set:', uid, store[uid], conversation); + store[uid] = conversation; + setDB(store); + } + + function deleteConversation(uid) { + let store = getDB(); + console.log('DB delete:', uid); + delete store[uid]; + setDB(store); + } + + function deleteAllConversations() { + console.log('DB delete all'); + let store = {}; + setDB(store); + } + + server.host('https://api.example', () => { + /* create new conversation */ + server.post('/conversations/').intercept((req, res) => { + let author = getAuthor(); + let { content, createdAt } = JSON.parse(req.body); + console.log(req.body); + try { + let conversationUid = randomString(); + setConversation(conversationUid, [ + { + author, + createdAt, + modifiedAt: createdAt, + content, + uid: conversationUid /* first comment has same uid as conversation */, + }, + ]); + res.status(201).json({ conversationUid }); + } catch (e) { + console.log('Server error:', e); + res.status(500); + } + }); + + /* add new comment to conversation */ + server.post('/conversations/:conversationUid').intercept((req, res) => { + let author = getAuthor(); + let { content, createdAt } = JSON.parse(req.body); + let conversationUid = req.params.conversationUid; + try { + let conversation = getConversation(conversationUid); + let commentUid = randomString(); + setConversation( + conversationUid, + conversation.concat([ + { + author, + createdAt, + modifiedAt: createdAt, + content, + uid: commentUid, + }, + ]) + ); + res.status(201).json({ commentUid }); + } catch (e) { + console.log('Server error:', e); + res.status(500); + } + }); + + /* edit a comment */ + server + .put('/conversations/:conversationUid/:commentUid') + .intercept((req, res) => { + let author = getAuthor(); + let { content, modifiedAt } = JSON.parse(req.body); + let conversationUid = req.params.conversationUid; + let commentUid = req.params.commentUid; + + try { + let conversation = getConversation(conversationUid); + let commentIndex = conversation.findIndex((comment) => { + return comment.uid === commentUid; + }); + let comment = conversation[commentIndex]; + let canEdit = comment.author === author; + if (canEdit) { + setConversation(conversationUid, [ + ...conversation.slice(0, commentIndex), + { + ...comment, + content, + modifiedAt, + }, + ...conversation.slice(commentIndex + 1), + ]); + } + res.status(201).json({ canEdit }); + } catch (e) { + console.log('Server error:', e); + res.status(500); + } + }); + + /* delete a comment */ + server + .delete('/conversations/:conversationUid/:commentUid') + .intercept((req, res) => { + let author = getAuthor(); + let owner = getOwner(); + let conversationUid = req.params.conversationUid; + let commentUid = req.params.commentUid; + let conversation = getConversation(conversationUid); + if (!conversation) { + res.status(404); + } + let commentIndex = conversation.findIndex((comment) => { + return comment.uid === commentUid; + }); + if (commentIndex === -1) { + res.status(404); + } + if ( + conversation[commentIndex].author === author || + author === owner + ) { + setConversation(conversationUid, [ + ...conversation.slice(0, commentIndex), + ...conversation.slice(commentIndex + 1), + ]); + res.status(204); + } else { + res.status(403); + } + }); + + /* delete a conversation */ + server.delete('/conversations/:conversationUid').intercept((req, res) => { + let author = getAuthor(); + let owner = getOwner(); + let conversationUid = req.params.conversationUid; + let conversation = getConversation(conversationUid); + if (conversation) { + if (conversation[0].author === author || author === owner) { + deleteConversation(conversationUid); + res.status(204); + } else { + res.status(403); + } + } else { + res.status(404); + } + }); + + /* delete all conversations */ + server.delete('/conversations').intercept((req, res) => { + let author = getAuthor(); + let owner = getOwner(); + if (author === owner) { + deleteAllConversations(); + res.status(204); + } else { + res.status(403); + } + }); + + /* lookup a conversation */ + server.get('/conversations/:conversationUid').intercept((req, res) => { + let conversation = getConversation(req.params.conversationUid); + if (conversation) { + res.status(200).json(conversation); + } else { + res.status(404); + } + }); + + /* lookup users */ + server.get('/users/').intercept((req, res) => { + res.status(200).json({ + users, + }); + }); + }); /* server.host */ + + /* Connect using the `connectTo` API */ + polly.connectTo('fetch'); + + /************************************************ + * Fake Users and associated pickers * + * Should be based on sessions and backend data * + ***********************************************/ + + const users = [ + { id: 'alex', displayName: 'Alex' }, + { id: 'jessie', displayName: 'Jessie' }, + { id: 'sam', displayName: 'Sam' }, + ]; + + /* Set initial Owner */ + setOwner(users[2].id); + + /* Set initial Author */ + setAuthor(users[0].id); + + /******************************** + * Tiny Comments functions * + * (must call "done" or "fail") * + ********************************/ + + /** + * Callback for when the operation was successful. + * @template T + * @callback done + * @param {T} data - the data + * @returns {void} + */ + + /** + * Callback for when the operation failed. + * @callback fail + * @param {string|Error} error - the reason for the failure + * @returns {void} + */ + + /** + * The data supplied to create a comment. + * @typedef {Object} TinyCommentsCreateReq + * @property {string} content - comment content + * @property {string} createdAt - ISO creation date + */ + + /** + * The response returned when a comment was created on the server. + * @typedef {Object} TinyCommentsCreateResp + * @property {string} conversationUid - ID of created comment + * @property {?fail} onError - error callback to call when the comment can't be put into the document + * @property {?done} onSuccess - success callback to call when the comment is put into the document + */ + + /** + * Conversation "create" function. Saves the comment as a new conversation, + * and asynchronously returns a conversation unique ID via the "done" + * callback. + * + * @param {TinyCommentsCreateReq} req - the comment to create + * @param {done} done - callback to call when the comment is created on the server + * @param {fail} fail - callback to call when something fails + */ + function tinycomments_create(req, done, fail) { + let content = req.content; + let createdAt = req.createdAt; + + 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) => { + let conversationUid = req2.conversationUid; + done({ conversationUid: conversationUid }); + }) + .catch((e) => { + fail(e); + }); + } + + /** + * + * @typedef {Object} TinyCommentsReplyReq + * @property {string} conversationUid + * @property {string} content + * @property {string} createdAt + */ + + /** + * + * @typedef {Object} TinyCommentsReplyResp + * @property {string} commentUid + */ + + /** + * Conversation "reply" function. Saves the comment as a reply to the + * an existing conversation, and asynchronously returns via the "done" + * callback when finished. + * + * @param {TinyCommentsReplyReq} req - the comment to append + * @param {done} done - callback to call when the comment is created on the server + * @param {fail} fail - callback to call when something fails + */ + function tinycomments_reply(req, done, fail) { + let conversationUid = req.conversationUid; + let content = req.content; + let createdAt = req.createdAt; + + 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) => { + let commentUid = req2.commentUid; + done({ commentUid: commentUid }); + }) + .catch((e) => { + fail(e); + }); + } + /** + * + * @typedef {Object} TinyCommentsEditReq + * @property {string} conversationUid + * @property {string} commentUid + * @property {string} content + * @property {string} modifiedAt + */ + + /** + * + * @typedef {Object} TinyCommentsEditResp + * @property {boolean} canEdit + * @property {?string} reason + */ + + /** + * + * @param {TinyCommentsEditReq} req + * @param {done} done + * @param {fail} fail + */ + function tinycomments_edit_comment(req, done, fail) { + let conversationUid = req.conversationUid; + let commentUid = req.commentUid; + let content = req.content; + let modifiedAt = req.modifiedAt; + + 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) => { + let canEdit = req2.canEdit; + done({ canEdit: canEdit }); + }) + .catch((e) => { + fail(e); + }); + } + + /** + * + * @typedef TinyCommentsDeleteReq + * @property {string} conversationUid + */ + + /** + * + * @typedef TinyCommentsDeleteResp + * @property {boolean} canDelete + * @property {?string} reason + */ + + /** + * Conversation "delete" function. Deletes an entire conversation. + * Returns asynchronously whether the conversation was deleted. + * Failure to delete due to permissions or business rules is indicated + * by `{canDelete: false}`, while unexpected errors should be indicated using the + * "fail" callback. + * @param {TinyCommentsDeleteReq} req + * @param {done} done + * @param {fail} fail + */ + function tinycomments_delete(req, done, fail) { + let 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...')); + } + }); + } + + /** + * + * @typedef TinyCommentsDeleteAllReq + * @type {object} + */ + + /** + * + * @typedef TinyCommentsDeleteAllResp + * @property {boolean} canDelete + * @property {?string} reason + */ + + /** + * All conversations "delete_all" function. Deletes all conversations. + * Returns asynchronously whether all conversations were deleted. + * Failure to delete due to permissions or business rules is indicated + * by `{canDelete: false}`, while unexpected errors should be indicated using the + * "fail" callback. + * @param {TinyCommentsDeleteAllReq} _req - no options + * @param {done} done + * @param {fail} fail + */ + function 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...')); + } + }); + } + + /** + * + * @typedef TinyCommentsDeleteCommentReq + * @property {string} conversationUid + * @property {string} commentUid + */ + + /** + * + * @typedef TinyCommentsDeleteCommentResp + * @property {boolean} canDelete + * @property {?string} reason + */ + + /** + * + * @param {TinyCommentsDeleteCommentReq} req + * @param {done} done + * @param {fail} fail + */ + function tinycomments_delete_comment(req, done, fail) { + let conversationUid = req.conversationUid; + let commentUid = req.commentUid; + + 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...')); + } + }); + } + + /** + * @typedef TinyCommentsLookupReq + * @property {string} conversationUid + */ + + /** + * + * @typedef TinyCommentsLookupRespComment + * @property {string} author + * @property {?string} authorName + * @property {string} createdAt + * @property {string} modifiedAt + * @property {string} content + * @property {string} uid + */ + + /** + * + * @typedef TinyCommentsLookupRespConversation + * @property {string} uid + * @property {TinyCommentsLookupRespComment[]} comments + */ + + /** + * + * @typedef TinyCommentsLookupResp + * @property {TinyCommentsLookupRespConversation} conversation + */ + + /** + * Conversation "lookup" function. Retreives an existing conversation + * via a conversation unique ID. Asynchronously returns the conversation + * via the "done" callback. + * + * @param {TinyCommentsLookupReq} req + * @param {done} done + * @param {fail} fail + */ + function tinycomments_lookup({ conversationUid }, done, fail) { + let lookup = async function () { + let convResp = await fetch( + 'https://api.example/conversations/' + conversationUid + ); + if (!convResp.ok) { + throw new Error('Failed to get conversation'); + } + let comments = await convResp.json(); + let usersResp = await fetch('https://api.example/users/'); + if (!usersResp.ok) { + throw new Error('Failed to get users'); + } + let { users } = await usersResp.json(); + let getUser = function (userId) { + return users.find((u) => { + return u.id === userId; + }); + }; + return { + conversation: { + uid: conversationUid, + comments: comments.map((comment) => { + return { + ...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#callback-mode', + height: 800, + plugins: 'paste code tinycomments help lists', + toolbar: + 'undo redo | formatselect | ' + + '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: function (editor) { + editor.on('SkinLoaded', () => { + editor.execCommand('ToggleSidebar', false, 'showcomments', { + skip_focus: true, + }); + }); + }, + }); + } +); diff --git a/_includes/live-demos/comments-2/index.html b/_includes/live-demos/comments-embedded/index.html similarity index 100% rename from _includes/live-demos/comments-2/index.html rename to _includes/live-demos/comments-embedded/index.html diff --git a/_includes/live-demos/comments-2/index.js b/_includes/live-demos/comments-embedded/index.js similarity index 67% rename from _includes/live-demos/comments-2/index.js rename to _includes/live-demos/comments-embedded/index.js index 7a5d870cb..5d8abe754 100644 --- a/_includes/live-demos/comments-2/index.js +++ b/_includes/live-demos/comments-embedded/index.js @@ -7,7 +7,7 @@ tinymce.init({ menubar: 'file edit view insert format tools tc', menu: { tc: { - title: 'TinyComments', + title: 'Comments', items: 'addcomment showcomments deleteallconversations' } }, @@ -21,5 +21,11 @@ tinymce.init({ canResolve: allowed || currentAuthor === userAllowedToResolve }); }, - content_style: {{site.liveDemoIframeCSSStyles}} + content_style: {{site.liveDemoIframeCSSStyles}}, + /* The following setup callback opens the comments sidebar when the editor loads */ + setup: function (editor) { + editor.on('SkinLoaded', function () { + editor.execCommand("ToggleSidebar", false, "showcomments", { skip_focus: true }); + }) + } }); \ No newline at end of file diff --git a/_includes/live-demos/full-featured/example.js b/_includes/live-demos/full-featured/example.js index 617af20e0..4448983b9 100644 --- a/_includes/live-demos/full-featured/example.js +++ b/_includes/live-demos/full-featured/example.js @@ -12,7 +12,7 @@ tinymce.init({ }, menu: { tc: { - title: 'TinyComments', + title: 'Comments', items: 'addcomment showcomments deleteallconversations' } }, diff --git a/_includes/live-demos/full-featured/index.js b/_includes/live-demos/full-featured/index.js index fc9feb0ce..0392721ce 100644 --- a/_includes/live-demos/full-featured/index.js +++ b/_includes/live-demos/full-featured/index.js @@ -168,7 +168,7 @@ tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/fak }, menu: { tc: { - title: 'TinyComments', + title: 'Comments', items: 'addcomment showcomments deleteallconversations' } }, diff --git a/_includes/live-demos/readonly-demo/index.html b/_includes/live-demos/readonly-demo/index.html index 491f1b98d..16b385f16 100644 --- a/_includes/live-demos/readonly-demo/index.html +++ b/_includes/live-demos/readonly-demo/index.html @@ -23,4 +23,4 @@ - + diff --git a/_includes/live-demos/readonly-demo/style.css b/_includes/live-demos/readonly-demo/style.css new file mode 100644 index 000000000..cb7a9cbf0 --- /dev/null +++ b/_includes/live-demos/readonly-demo/style.css @@ -0,0 +1,28 @@ +.button { + display: inline-block; + min-width: 158px; + height: 45px; + line-height: 45px; + border-radius: 3px; + cursor: pointer; + font-size: 14px; + padding: 0 25px; + text-align: center; +} +.button:active { + transform: translateY(1px); +} + +.button-color { + background-color: {{site.primaryColor}}; +} + +.button-color:hover { + background-color: #1d6abe; +} + +.link-text { + color: #fff; + font-weight: 600; +} + diff --git a/_includes/plugins/comments_embed_fullpage_issues.md b/_includes/plugins/comments_embed_fullpage_issues.md new file mode 100644 index 000000000..ad204b6a8 --- /dev/null +++ b/_includes/plugins/comments_embed_fullpage_issues.md @@ -0,0 +1,9 @@ +## Using Comments embedded mode with the Full Page plugin + +Developers have to be cautious when deciding the order in which the plugins are added in the plugins list. + +Comments can cause an issue if the [Full Page]({{site.baseurl}}/plugins/opensource/fullpage/) plugin `fullpage` appears before Comments plugin `tinycomments` in the plugin list, and `tinycomments` is configured to use `embedded mode`. + +The order that the plugins appear affects the order that the `getContent` hooks are processed in. This creates an issue with `tinycomments` working as expected since the `fullpage` plugin adds outer `` elements before `tinycomments` adds its comment data. This leads to the comment data being in the wrong place. The consequence of this situation is that when a saved document is re-opened, the comment data is lost (but the highlights are still there). + +For a workaround, please ensure that `tinycomments` is listed before `fullpage` in the plugins list. This should result in `tinycomments` working properly. diff --git a/_includes/plugins/comments_highlighting_css.md b/_includes/plugins/comments_highlighting_css.md new file mode 100644 index 000000000..95582f1a0 --- /dev/null +++ b/_includes/plugins/comments_highlighting_css.md @@ -0,0 +1,9 @@ +## Configuring the commented text properties + +The highlight styles are now a part of the overall content skin and are changed through customizing the skin. + +{{site.productname}} open source project [oxide](https://github.com/tinymce/oxide/blob/master/src/less/theme/content/comments/comments.less) (default skin), defines the variables used for changing the annotation colours. + +Refer to the [documentation]({{site.baseurl}}/advanced/creating-a-skin/#creatingaskin) for building a skin using this repo. + +For more information on configuring {{site.productname}} formats, refer to the [formats]({{site.baseurl}}/configure/content-formatting/#formats) section. \ No newline at end of file diff --git a/_includes/plugins/comments_open_sidebar.md b/_includes/plugins/comments_open_sidebar.md new file mode 100644 index 000000000..bec5b7208 --- /dev/null +++ b/_includes/plugins/comments_open_sidebar.md @@ -0,0 +1,45 @@ +## Show the comments sidebar when TinyMCE loads + +To show the comments sidebar when the editor is loaded or to display the sidebar by default, add a callback to open the sidebar once the editor 'skin' is loaded. + +For example: + +{% if page.name == "comments_callback_mode.md" %} +```js +tinymce.init({ + selector: '#editor', + plugins: 'tinycomments', + tinycomments_mode: 'callback', + 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: function (editor) { + editor.on('SkinLoaded', function () { + editor.execCommand("ToggleSidebar", false, "showcomments"); + }) + } +}); +``` +{% else %} +```js +tinymce.init({ + selector: '#editor', + plugins: 'tinycomments', + tinycomments_mode: 'embedded', + tinycomments_author: currentAuthor, + tinycomments_can_resolve: canResolveCommentsCallback, + /* The following setup callback opens the comments sidebar when the editor loads */ + setup: function (editor) { + editor.on('SkinLoaded', function () { + editor.execCommand("ToggleSidebar", false, "showcomments"); + }) + } +}); +``` +{% endif %} \ No newline at end of file diff --git a/advanced/configuring-comments-callbacks.md b/advanced/configuring-comments-callbacks.md index 1efe522dd..297af5949 100644 --- a/advanced/configuring-comments-callbacks.md +++ b/advanced/configuring-comments-callbacks.md @@ -1,40 +1,52 @@ --- layout: default -title: Configuring callbacks for the Comments plugin -title_nav: Configuring callbacks for the Comments plugin -description: Instructions for configuring callbacks for the Comments plugin +title: Configuring the Comments plugin in callback mode +title_nav: Callback mode +description: Information on configuring the Comments plugin in callback mode keywords: comments commenting tinycomments callback --- -**Callback mode** is the default mode for [the Comments plugin]({{site.baseurl}}/plugins/premium/comments/). In the callback mode, the user needs to configure storage to be able to save comments on the server. The Comments functions (create, reply, edit, delete comment, delete all conversations, and lookup) are configured differently depending upon the server-side storage configuration. +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} -### Required settings +**Callback mode** is the default mode for [the Comments plugin]({{site.baseurl}}/plugins/premium/comments/). 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. -The Comments plugin requires the following functions to be defined: +## Interactive example -```js -tinymce.init({ - ... - tinycomments_create: function (req, done, fail) { ... }, - tinycomments_reply: function (req, done, fail) { ... }, - tinycomments_delete: function (req, done, fail) { ... }, - tinycomments_delete_all: function (req, done, fail) { ... }, - tinycomments_delete_comment: function (req, done, fail) { ... }, - tinycomments_lookup: function (req, done, fail) { ... }, - tinycomments_edit_comment: function (req, done, fail) { ... } -}); +The following example uses a simulated server (provided by [Polly.js](https://netflix.github.io/pollyjs/)) 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. -``` +{% include live-demo.html id="comments-callback" %} + +### How the comments plugin works in callback mode -All functions incorporate `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`. +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 configuring the **current author**: +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 user. + +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. + +## Configuration options - callback mode + +### Required options -* **Current author** - the Comments plugin does not know the name of the current user. After a user comments (triggering `tinycomments_create` for the first comment, or `tinycomments_reply` for subsequent comments), Comments requests the updated conversation via `tinycomments_lookup`, which should now contain the additional comment with the proper author. Determining the current user and storing the comment related to that user, has to be configured by the user. +When using callback mode, the Comments plugin requires callback functions for the following options: + +* [`tinycomments_create`](#tinycomments_create) +* [`tinycomments_reply`](#tinycomments_reply) +* [`tinycomments_edit_comment`](#tinycomments_edit_comment) +* [`tinycomments_delete_comment`](#tinycomments_delete_comment) +* [`tinycomments_delete`](#tinycomments_delete) +* [`tinycomments_delete_all`](#tinycomments_delete_all) +* [`tinycomments_lookup`](#tinycomments_lookup) + +The [`tinycomments_resolve`](#tinycomments_resolve) option is _optional_. ### `tinycomments_create` @@ -44,9 +56,11 @@ The `tinycomments_create` function saves the comment as a new conversation and r 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. +`content` +: The content of the comment to create. -* **createdAt**: The date the comment was created. +`createdAt` +: The date the comment was created. The `done` callback should accept the following object: @@ -60,6 +74,50 @@ The `done` callback should accept the following object: } ``` +For example: + +```js +function create_comment(_ref, done, fail) { + var content = _ref.content; + var createdAt = _ref.createdAt; + + fetch('https://api.example/conversations/', { + method: 'POST', + body: JSON.stringify({ content: content, createdAt: createdAt }), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) + .then(function (response) { + if (!response.ok) { + throw new Error('Failed to create comment'); + } + return response.json(); + }) + .then(function (_ref2) { + var conversationUid = _ref2.conversationUid; + done({ conversationUid: conversationUid }); + }) + .catch(function (e) { + fail(e); + }); +} + +tinymce.init({ + selector: '#editor', + 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 conversation `tinycomments_reply` function to reply to a comment. @@ -68,11 +126,14 @@ The `tinycomments_reply` function saves the comment as a reply to an existing co 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. +`conversationUid` +: The uid of the conversation the reply is targeting. -* **content**: The content of the comment to create. +`content` +: The content of the comment to create. -* **createdAt**: The date the comment was created. +`createdAt` +: The date the comment was created. The `done` callback should accept the following object: @@ -82,6 +143,51 @@ The `done` callback should accept the following object: } ``` +For example: + +```js +function reply_comment(_ref, done, fail) { + var conversationUid = _ref.conversationUid; + var content = _ref.content; + var createdAt = _ref.createdAt; + + fetch('https://api.example/conversations/' + conversationUid, { + method: 'POST', + body: JSON.stringify({ content: content, createdAt: createdAt }), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) + .then(function (response) { + if (!response.ok) { + throw new Error('Failed to reply to comment'); + } + return response.json(); + }) + .then(function (_ref2) { + var commentUid = _ref2.commentUid; + done({ commentUid: commentUid }); + }) + .catch(function (e) { + fail(e); + }); +} + +tinymce.init({ + selector: '#editor', + 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 conversation `tinycomments_edit_comment` function to edit a comment. @@ -90,13 +196,17 @@ The `tinycomments_edit_comment` function allows updating or changing original co 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. +`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) +`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. +`content` +: The content of the comment to create. -* **modifiedAt**: The date the comment was modified. +`modifiedAt` +: The date the comment was modified. The `done` callback should accept the following object: @@ -107,24 +217,55 @@ The `done` callback should accept the following object: } ``` -### `tinycomments_delete` - -The `tinycomments_delete` function should asynchronously return a flag indicating whether the comment or comment thread was removed using the `done` callback. Unrecoverable errors are communicated to {{site.productname}} 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: +For example: ```js -{ - 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) +function edit_comment(_ref, done, fail) { + var conversationUid = _ref.conversationUid; + var commentUid = _ref.commentUid; + var content = _ref.content; + var modifiedAt = _ref.modifiedAt; + + 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(function (response) { + if (!response.ok) { + throw new Error('Failed to edit comment'); + } + return response.json(); + }) + .then(function (_ref2) { + var canEdit = _ref2.canEdit; + done({ canEdit: canEdit }); + }) + .catch(function (e) { + fail(e); + }); } + +tinymce.init({ + selector: '#editor', + 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 +}); ``` -> **Note**: Failure to delete due to permissions or business rules is indicated by "false", while unexpected errors should be indicated using the "fail" callback. ### `tinycomments_resolve` @@ -136,7 +277,8 @@ The `tinycomments_resolve` function should asynchronously return a flag indicati 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. +`conversationUid` +: The uid of the conversation the reply is targeting. The `done` callback should accept the following object: @@ -149,56 +291,215 @@ The `done` callback should accept the following object: > **Note**: 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. -### `tinycomments_delete_all` +For example: -The `tinycomments_delete_all` function should asynchronously return a flag indicating whether all the comments in a conversation were removed using the `done` callback. Unrecoverable errors are communicated to {{site.productname}} by calling the `fail` callback instead. +```js +function resolve_comment_thread(_ref, done, fail) { + var conversationUid = _ref.conversationUid; + fetch('https://api.example/conversations/' + conversationUid, { + method: 'PUT', + }).then(function (response) { + if (response.ok) { + done({ canResolve: true }); + } else if (response.status === 403) { + done({ canResolve: false }); + } else { + fail(new Error('Something has gone wrong...')); + } + }); +} -The `tinycomments_delete_all` function is given a request (req) object as the first parameter with no fields. +tinymce.init({ + selector: '#editor', + 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 {{site.productname}} 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: ```js { - 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) + 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) } ``` -> **Note**: 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. +> **Note**: Failure to delete due to permissions or business rules is indicated by "false", while unexpected errors should be indicated using the "fail" callback. -### `tinycomments_delete_comment` +For example: -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 {{site.productname}} by calling the `fail` callback instead. +```js +function delete_comment(_ref, done, fail) { + var conversationUid = _ref.conversationUid; + var commentUid = _ref.commentUid; -The `tinycomments_delete_comment` function is given a request (req) object as the first parameter, which has these fields: + fetch( + 'https://api.example/conversations/' + conversationUid + '/' + commentUid, + { + method: 'DELETE', + } + ).then(function (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` + +The `tinycomments_delete` function should asynchronously return a flag indicating whether the comment or comment thread was removed using the `done` callback. Unrecoverable errors are communicated to {{site.productname}} 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. -* **commentUid**: The uid of the comment to delete (cannot be the same as conversationUid) +`conversationUid` +: The uid of the conversation the reply is targeting. The `done` callback should accept the following object: ```js { - 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) + 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) } ``` > **Note**: 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: + +```js +function delete_comment_thread(_ref, done, fail) { + var conversationUid = _ref.conversationUid; + fetch('https://api.example/conversations/' + conversationUid, { + method: 'DELETE', + }).then(function (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 comments in a conversation were removed using the `done` callback. Unrecoverable errors are communicated to {{site.productname}} 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: + +```js +{ + 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) +} +``` + +> **Note**: 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: + +```js +function delete_all_comment_threads(_req, done, fail) { + fetch('https://api.example/conversations', { + method: 'DELETE', + }).then(function (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, + 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 Conversation `tinycomments_lookup` function to retrieve an existing conversation via a conversation 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. +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. +`conversationUid` +: The uid of the conversation the reply is targeting. The `done` callback should accept the following object: @@ -230,4 +531,66 @@ The `done` callback should accept the following object: > **Note**: The dates should use [ISO 8601 format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString). This can be generated in JavaScript with: `new Date().toISOString()`. -For more information on the Comments commercial feature, visit our [Premium Features]({{ site.baseurl }}/enterprise/tiny-comments/) page. +For example: + +```js +function lookup_comment({ conversationUid }, done, fail) { + var lookup = async function () { + var convResp = await fetch( + 'https://api.example/conversations/' + conversationUid + ); + if (!convResp.ok) { + throw new Error('Failed to get conversation'); + } + var comments = await convResp.json(); + var usersResp = await fetch('https://api.example/users/'); + if (!usersResp.ok) { + throw new Error('Failed to get users'); + } + var { users } = await usersResp.json(); + var getUser = function (userId) { + return users.find(function (u) { + return u.id === userId; + }); + }; + return { + conversation: { + uid: conversationUid, + comments: comments.map(function (comment) { + return { + ...comment, + content: comment.content, + authorName: getUser(comment.author)?.displayName, + }; + }), + }, + }; + }; + lookup() + .then(function (data) { + console.log('Lookup success ' + conversationUid, data); + done(data); + }) + .catch(function (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 +}); +``` + +{% include plugins/comments_open_sidebar.md %} + +{% include plugins/comments_highlighting_css.md %} diff --git a/demo/comments-2.md b/demo/comments-2.md index 14abe18de..91ced279c 100644 --- a/demo/comments-2.md +++ b/demo/comments-2.md @@ -15,4 +15,4 @@ In this example, the features in the Comments plugin are highlighted, including For information on the other Comments plugin configuration options, see: [The Comments plugin]({{site.baseurl}}/plugins/premium/comments/). -{% include live-demo.html id="comments-2" %} \ No newline at end of file +{% include live-demo.html id="comments-embedded" %} \ No newline at end of file diff --git a/demo/pageembed.md b/demo/pageembed.md index 05e8f9321..8a6287136 100644 --- a/demo/pageembed.md +++ b/demo/pageembed.md @@ -7,10 +7,8 @@ keywords: view Page Embed insert iframe controls: toolbar button, menu item --- - ## Interactive example - -This example shows how to use the Page Embed plugin to embed a page in the content in a responsive or exactly sized iframe.For more information on the Page Embed plugin, see the [docs]({{site.baseurl}}/plugins/premium/pageembed/). +This example shows how to use the Page Embed plugin to embed a page in the content in a responsive or exactly sized iframe. For more information on the Page Embed plugin, see the [docs]({{site.baseurl}}/plugins/premium/pageembed/). {% include live-demo.html id="page-embed" %} \ No newline at end of file diff --git a/enterprise/tiny-comments.md b/enterprise/tiny-comments.md index e12caccac..c2a2cf0a9 100644 --- a/enterprise/tiny-comments.md +++ b/enterprise/tiny-comments.md @@ -30,7 +30,7 @@ The Comments plugin allows the user to perform the following functions: ## Try our Comments plugin demo -{% include live-demo.html id="comments-2" %} +{% include live-demo.html id="comments-embedded" %} {% assign pluginname = 'Comments' %} {% assign pluginminimumplan = 'enterprise' %} diff --git a/plugins/premium/comments.md b/plugins/premium/comments.md deleted file mode 100644 index 37f2f8ad0..000000000 --- a/plugins/premium/comments.md +++ /dev/null @@ -1,344 +0,0 @@ ---- -layout: default -title: Comments -title_nav: Comments -description: Tiny Comments provides the ability to add comments to the content and collaborate with other users for content editing. -keywords: comments commenting tinycomments ---- - -{% assign pluginname = "Comments" %} -{% assign plugincode = "comments" %} - -{{site.premiumplugin}} - -The Comments plugin provides the ability to start or join a conversation by adding comments to the content within the {{site.productname}} editor. The Comments plugin is built upon the [Annotations API]({{site.baseurl}}/advanced/annotations/) and uses annotations to create comment threads (conversations). - -## Interactive example - -{% include live-demo.html id="comments-2" %} - -## Using Comments - -### To add a comment - -1. Select the text from the desired location in the editor body. -1. From the navigation menu, choose **Insert**-> **Add Comment** or click on the **Add comment** ![Add comment]({{site.baseurl}}/images/icons/comment-add.svg) toolbar button to add the comment. -1. The Comment dialog box appears in the sidebar of the editor instance. -1. Type the comment in the box displaying "_Say something…_" suggested text. -1. Press **Clear** to delete or **Save** to store the input comment. - -**Result**: The selected text will be highlighted as per the configured options. The following screen with the option for editing, deleting, and replying to the comment, will appear. - -![Delete Conversation]({{site.baseurl}}/images/comments-edit.png) - -Note: The above procedure can be followed for adding multiple comments to the document. - -### Editing a comment - -Follow this procedure to edit a comment. - -1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. -1. Select **Edit** from the menu items. -1. The comment field becomes editable. Make the required changes. -1. Click **Cancel** to discard or **Save** to store the changes. - -### Delete a comment - -Follow this procedure to delete a comment. This option is not available for the first comment in a conversation. - -1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. -1. Select **Delete** from the menu items. -1. The following options appear in the comments sidebar:
-![delete comment]({{site.baseurl}}/images/comments-delete-comment.png) -1. Click **Cancel** to cancel the action or **Delete** to remove the comment from the conversation. - -### Delete conversation - -This option is only available for the first comment in a conversation. Once the comment is saved, follow this procedure to delete a conversation. - -1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. -1. Select **Delete conversation** from the menu items. -1. The following decision dialog box will appear:
-![delete conversation]({{site.baseurl}}/images/comments-delete-conversation.png) -1. Click **Cancel** to cancel the action or **Delete** to remove the conversation. - -**Result**: The conversation and all its subsequent comments will be deleted. - -### Resolve conversation - -{{site.requires_5_8v}} - -> **NOTE**: This feature requires the [`tinycomments_resolve`]({{site.baseurl}}/advanced/configuring-comments-callbacks/#tinycomments_resolve) or [`tinycomments_can_resolve`]({{site.baseurl}}/plugins/premium/comments/#tinycomments_can_resolve) setting to be configured. - -This option is only available for the first comment in a conversation. Once a comment is saved, follow this procedure to resolve a conversation. - -1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. -1. Select **Resolve conversation** from the menu items. -1. The following decision dialog box will appear:
-![resolve conversation]({{site.baseurl}}/images/comments-resolve-conversation.png) -1. Click **Cancel** to cancel the action or **Resolve** to resolve the conversation. - -**Result**: The conversation will be resolved. - -### Show comment - -Follow this procedure to display the comments sidebar: - -1. Place the cursor on the desired text in the editor body: -1. From the navigation menu, choose **View** -> **Show Comment** or click on the **Show Comments**![Comments]({{site.baseurl}}/images/comments-toolbar-button.png) toggle toolbar button to display the comment. - -**Result**: The comments sidebar will appear and display the corresponding conversation for the highlighted text. - -### Delete all conversations - -Follow this procedure to delete all conversations in the document: - -1. From the navigation menu, choose **File** -> **Delete all conversations** option to delete all the comments in a document. -1. The following decision dialog box will appear:
-![Delete all conversations]({{site.baseurl}}/images/comments-delete-conversations.png) -1. Click **Ok** to remove the all the comments or **Cancel** to dismiss the action. - -**Result**: All the comments for the selected document will be deleted. - -## Add the Comments plugin - -To add the Comments plugin to the {{site.productname}} editor, use the following script: - -```js -tinymce.init({ - selector: '#tiny-ui .editor', - plugins: 'paste tinycomments', - tinycomments_mode: 'embedded', - tinycomments_author: 'Author' -}); -``` - -## Modes - -There are two modes available in Comments that provide the ability to save comments. These modes are configured in the Comments settings. - -* **Callback Mode** - This is the default mode in Comments. This mode is used to configure storage and save comments on the user’s server. This option gives the user a choice of configuring the storage settings to either persist comments immediately or save them at the same time as the content. Additional callbacks must be configured to use Comments in callback mode. - -* **Embedded Mode** - This mode allows the user to store the comments within the content. No additional callbacks are required to be configured to use this mode. - -### Configuring Comments callback mode - -Refer to the [configuring callbacks for comments]({{site.baseurl}}/advanced/configuring-comments-callbacks/) section for more information. - -### Configuring Comments embedded mode - -To configure Comments to use the embedded mode use the following script: -```js -tinymce.init({ - selector: '#textarea', - tinycomments_author: 'embedded_journalist', - tinycomments_author_name: 'Embedded Journalist', - tinycomments_mode: 'embedded' - ... -}) -``` - -#### Embedded mode options - -##### `tinycomments_author` - -This option sets the author id to be used when creating or replying to comments. - -**Type:** `String` - -**Default Value:** `'Anon'` - -###### Example: Using `tinycomments_author` - -```js -tinymce.init({ - selector: '#textarea', - tinycomments_author: 'embedded_journalist', - tinycomments_mode: 'embedded' -}) -``` - -##### `tinycomments_author_name` - -> Available in Tiny Comments version 2.1 onwards. - -_Optional_ - This option sets the author's display name to be used when creating or replying to comments. If this option is omitted, then the author id is used instead. - -**Type:** `String` - -###### Example: Using `tinycomments_author_name` - -```js -tinymce.init({ - selector: '#textarea', - tinycomments_author: 'embedded_journalist', - tinycomments_author_name: 'Embedded Journalist', - tinycomments_mode: 'embedded' -}) -``` - -##### `tinycomments_can_delete` - -_Optional_ - This option sets the author permissions for _deleting comment conversations_. If the `tinycomments_can_delete` option is not included, the current author (`tinycomments_author`) cannot delete comment conversations created by other authors. - -**Type:** `Function` - -**Default Function:** - -```js -function (req, done, fail) { - var allowed = req.comments.length > 0 && - req.comments[0].author === ; - done({ - canDelete: allowed - }); -} -``` - -The following example extends the default behavior to allow the author `` to delete other author's comment conversations by adding `|| currentAuthor === ''`. - -###### Example: Using `tinycomments_can_delete` - -```js -var currentAuthor = 'embedded_journalist'; - -tinymce.init({ - selector: '#textarea', - tinycomments_author: currentAuthor, - tinycomments_can_delete: function (req, done, fail) { - var allowed = req.comments.length > 0 && - req.comments[0].author === currentAuthor; - done({ - canDelete: allowed || currentAuthor === '' - }); - } -}); -``` - -##### `tinycomments_can_resolve` - -{{site.requires_5_8v}} - -_Optional_ - This option adds a _Resolve Conversation_ item to the dropdown menu of the first comment in a conversation. This callback sets the author permissions for _resolving comment conversations_. - -**Type:** `Function` - -###### Example: Using `tinycomments_can_resolve` - -```js -var currentAuthor = 'embedded_journalist'; - -tinymce.init({ - selector: '#textarea', - tinycomments_author: currentAuthor, - tinycomments_can_resolve: function (req, done, fail) { - var allowed = req.comments.length > 0 && - req.comments[0].author === currentAuthor; - done({ - canResolve: allowed || currentAuthor === '' - }); - } -}); -``` - -##### `tinycomments_can_delete_comment` - - -_Optional_ - This option sets the author permissions for _deleting comments_. If the `tinycomments_can_delete_comment` option is not included, the current author (`tinycomments_author`) cannot delete comments added by other authors. - -**Type:** `Function` - -**Default Function:** - -```js -function (req, done, fail) { - var allowed = req.comment.author === ; - done({ - canDelete: allowed - }); -} -``` - -The following example extends the default behavior to allow the author `` to delete other author's comments by adding `|| currentAuthor === ''`. - -###### Example: Using `tinycomments_can_delete_comment` - -```js -var currentAuthor = 'embedded_journalist'; - -tinymce.init({ - selector: '#textarea', - tinycomments_author: currentAuthor, - tinycomments_can_delete_comment: function (req, done, fail) { - var allowed = req.comment.author === currentAuthor; - done({ - canDelete: allowed || currentAuthor === '' - }); - } -}); -``` - -##### `tinycomments_can_edit_comment` - -_Optional_ - This option sets the author permissions for _editing comments_. If the `tinycomments_can_edit_comment` option is not included, the current author (`tinycomments_author`) cannot edit comments added by other authors. - -**Type:** `Function` - -**Default Function** -```js -function (req, done, fail) { - var allowed = req.comment.author === ; - done({ - canEdit: allowed - }); -} -``` - -The following example extends the default behavior to allow the author `` to edit other author's comments by adding `|| currentAuthor === ''`. - -###### Example: Using `tinycomments_can_edit_comment` - -```js -var currentAuthor = 'embedded_journalist'; - -tinymce.init({ - selector: '#textarea', - tinycomments_author: currentAuthor, - tinycomments_can_edit_comment: function (req, done, fail) { - var allowed = req.comment.author === currentAuthor; - done({ - canEdit: allowed || currentAuthor === '' - }); - } -}); -``` - -{% include misc/plugin-toolbar-button-id-boilerplate.md %} - -{% include misc/plugin-menu-item-id-boilerplate.md %} - -## Configuring the commented text properties - -The highlight styles are now a part of the overall content skin and are changed through customizing the skin. - -{{site.productname}} open source project [oxide](https://github.com/tinymce/oxide/blob/master/src/less/theme/content/comments/comments.less) (default skin), defines the variables used for changing the annotation colours. - -Refer to the [documentation]({{site.baseurl}}/advanced/creating-a-skin/#creatingaskin) for building a skin using this repo. - -For more information on configuring {{site.productname}} formats, refer to the [formats]({{site.baseurl}}/configure/content-formatting/#formats) section. - -## Using Comments embedded mode with the Full Page plugin - -Users have to be cautious when deciding the order in which the plugins are added in the plugins list. - -Comments can cause an issue if the [Full Page]({{site.baseurl}}/plugins/opensource/fullpage/) plugin `fullpage` appears before Comments plugin `tinycomments` in the plugin list, and `tinycomments` is configured to use `embedded mode`. - -The order that the plugins appear affects the order that the `getContent` hooks are processed in. This creates an issue with `tinycomments` working as expected since the `fullpage` plugin adds outer `` elements before `tinycomments` adds its comment data. This leads to the comment data being in the wrong place. The consequence of this situation is that when a saved document is re-opened, the comment data is lost (but the highlights are still there). - -For a workaround, please ensure that `tinycomments` is listed before `fullpage` in the plugins list. This should result in `tinycomments` working properly. - -## Commands - -The Comments plugin provides the following JavaScript commands. - -{% include commands/comments-cmds.md %} diff --git a/plugins/premium/comments/comments_callback_mode.md b/plugins/premium/comments/comments_callback_mode.md new file mode 100644 index 000000000..dacbbe44a --- /dev/null +++ b/plugins/premium/comments/comments_callback_mode.md @@ -0,0 +1,595 @@ +--- +layout: default +title: Configuring the Comments plugin in callback mode +title_nav: Callback mode +description: Information on configuring the Comments plugin in callback mode +keywords: comments commenting tinycomments callback +--- + +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} + +**Callback mode** is the default mode for [the Comments plugin]({{site.baseurl}}/plugins/premium/comments/). 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](https://netflix.github.io/pollyjs/)) 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. + +{% include live-demo.html id="comments-callback" %} + +### 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. + +## Configuration options + +### Required options + +When using callback mode, the Comments plugin requires callback functions for the following options: + +* [`tinycomments_create`](#tinycomments_create) +* [`tinycomments_reply`](#tinycomments_reply) +* [`tinycomments_edit_comment`](#tinycomments_edit_comment) +* [`tinycomments_delete_comment`](#tinycomments_delete_comment) +* [`tinycomments_delete`](#tinycomments_delete) +* [`tinycomments_delete_all`](#tinycomments_delete_all) +* [`tinycomments_lookup`](#tinycomments_lookup) + +The [`tinycomments_resolve`](#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: + +```js +{ + conversationUid: string, // the new conversation uid + // Optional error callback which will be run if the new conversation could not be created + onError: function (err) { ... }, + // Optional success callback which will be run when the new conversation is successfully created + onSuccess: function (uid) { ... } +} +``` + +For example: + +```js +function create_comment(ref, done, fail) { + let content = ref.content; + let createdAt = ref.createdAt; + + 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: '#editor', + 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 {{site.productname}} 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: + +```js +{ + commentUid: string // the value of the new comment uid +} +``` + +For example: + +```js +function reply_comment(ref, done, fail) { + let conversationUid = ref.conversationUid; + let content = ref.content; + let createdAt = ref.createdAt; + + 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: '#editor', + 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 {{site.productname}} 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: + +```js +{ + 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: + +```js +function edit_comment(ref, done, fail) { + let conversationUid = ref.conversationUid; + let commentUid = ref.commentUid; + let content = ref.content; + let modifiedAt = ref.modifiedAt; + + 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: '#editor', + 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` + +{{site.requires_5_8v}} + +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 {{site.productname}} 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: + +```js +{ + 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) +} +``` + +> **Note**: 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: + +```js +function resolve_comment_thread(ref, done, fail) { + let 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: '#editor', + 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 {{site.productname}} 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: + +```js +{ + 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) +} +``` + +> **Note**: 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: + +```js +function delete_comment(ref, done, fail) { + let conversationUid = ref.conversationUid; + let commentUid = ref.commentUid; + + 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: '#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` + +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 {{site.productname}} 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: + +```js +{ + 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) +} +``` + +> **Note**: 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: + +```js +function delete_comment_thread(ref, done, fail) { + let 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 {{site.productname}} 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: + +```js +{ + 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) +} +``` + +> **Note**: 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: + +```js +function 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: '#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, // 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. 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: + +```js +{ + 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 + 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 + 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 + } + ] + } +} +``` + +> **Note**: The dates should use [ISO 8601 format](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString). This can be generated in JavaScript with: `new Date().toISOString()`. + +For example: + +```js +function lookup_comment({ conversationUid }, done, fail) { + let lookup = async function () { + let convResp = await fetch( + 'https://api.example/conversations/' + conversationUid + ); + if (!convResp.ok) { + throw new Error('Failed to get conversation'); + } + let comments = await convResp.json(); + let usersResp = await fetch('https://api.example/users/'); + if (!usersResp.ok) { + throw new Error('Failed to get users'); + } + let { users } = await usersResp.json(); + let getUser = function (userId) { + return users.find((u) => { + return u.id === userId; + }); + }; + return { + conversation: { + uid: conversationUid, + comments: comments.map((comment) => { + return { + ...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: '#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 +}); +``` + +{% include plugins/comments_open_sidebar.md %} + +{% include plugins/comments_highlighting_css.md %} diff --git a/plugins/premium/comments/comments_commands_events_apis.md b/plugins/premium/comments/comments_commands_events_apis.md new file mode 100644 index 000000000..c62942c52 --- /dev/null +++ b/plugins/premium/comments/comments_commands_events_apis.md @@ -0,0 +1,16 @@ +--- +layout: default +title: Commands for the comments plugin +title_nav: Commands +description: Information on the commands provided with the comments plugin. +keywords: comments commenting tinycomments +--- + +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} + +## Commands + +The Comments plugin provides the following JavaScript commands. + +{% include commands/comments-cmds.md %} \ No newline at end of file diff --git a/plugins/premium/comments/comments_embedded_mode.md b/plugins/premium/comments/comments_embedded_mode.md new file mode 100644 index 000000000..fe5379373 --- /dev/null +++ b/plugins/premium/comments/comments_embedded_mode.md @@ -0,0 +1,211 @@ +--- +layout: default +title: Configuring the Comments plugin in embedded mode +title_nav: Embedded mode +description: Information on configuring the Comments plugin in embedded mode +keywords: comments commenting tinycomments embedded mode +--- + +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} + +## Add the Comments plugin in embeddded mode + +To add the Comments plugin in embedded mode to the {{site.productname}}, configure the following options: + +```js +tinymce.init({ + selector: '#textarea', + plugins: 'tinycomments', + tinycomments_author: 'author', + tinycomments_author_name: 'Name of the commenter', + tinycomments_mode: 'embedded' +}) +``` + +This is the minimum recommended setup for the Comments plugin in embedded mode. If the `tinycomments_author` and `tinycomments_author_name` options are not configured, all users will be assigned the name "_ANON_". + +## Interactive example + +{% include live-demo.html id="comments-embedded" %} + +## Configuration options + +### `tinycomments_author` + +This option sets the author id to be used when creating or replying to comments. + +**Type:** `String` + +**Default Value:** `'Anon'` + +#### Example: Using `tinycomments_author` + +```js +tinymce.init({ + selector: '#textarea', + tinycomments_author: 'embedded_journalist', + tinycomments_mode: 'embedded' +}) +``` + +### `tinycomments_author_name` + +> Available in Tiny Comments version 2.1 onwards. + +_Optional_ - This option sets the author's display name to be used when creating or replying to comments. If this option is omitted, then the author id is used instead. + +**Type:** `String` + +#### Example: Using `tinycomments_author_name` + +```js +tinymce.init({ + selector: '#textarea', + tinycomments_author: 'embedded_journalist', + tinycomments_author_name: 'Embedded Journalist', + tinycomments_mode: 'embedded' +}) +``` + +### `tinycomments_can_delete` + +_Optional_ - This option sets the author permissions for _deleting comment conversations_. If the `tinycomments_can_delete` option is not included, the current author (`tinycomments_author`) cannot delete comment conversations created by other authors. + +**Type:** `Function` + +**Default Function:** + +```js +function (req, done, fail) { + var allowed = req.comments.length > 0 && + req.comments[0].author === ; + done({ + canDelete: allowed + }); +} +``` + +The following example extends the default behavior to allow the author `` to delete other author's comment conversations by adding `|| currentAuthor === ''`. + +#### Example: Using `tinycomments_can_delete` + +```js +var currentAuthor = 'embedded_journalist'; + +tinymce.init({ + selector: '#textarea', + tinycomments_author: currentAuthor, + tinycomments_can_delete: function (req, done, fail) { + var allowed = req.comments.length > 0 && + req.comments[0].author === currentAuthor; + done({ + canDelete: allowed || currentAuthor === '' + }); + } +}); +``` + +### `tinycomments_can_resolve` + +{{site.requires_5_8v}} + +_Optional_ - This option adds a _Resolve Conversation_ item to the dropdown menu of the first comment in a conversation. This callback sets the author permissions for _resolving comment conversations_. + +**Type:** `Function` + +#### Example: Using `tinycomments_can_resolve` + +```js +var currentAuthor = 'embedded_journalist'; + +tinymce.init({ + selector: '#textarea', + tinycomments_author: currentAuthor, + tinycomments_can_resolve: function (req, done, fail) { + var allowed = req.comments.length > 0 && + req.comments[0].author === currentAuthor; + done({ + canResolve: allowed || currentAuthor === '' + }); + } +}); +``` + +### `tinycomments_can_delete_comment` + +_Optional_ - This option sets the author permissions for _deleting comments_. If the `tinycomments_can_delete_comment` option is not included, the current author (`tinycomments_author`) cannot delete comments added by other authors. + +**Type:** `Function` + +**Default Function:** + +```js +function (req, done, fail) { + var allowed = req.comment.author === ; + done({ + canDelete: allowed + }); +} +``` + +The following example extends the default behavior to allow the author `` to delete other author's comments by adding `|| currentAuthor === ''`. + +#### Example: Using `tinycomments_can_delete_comment` + +```js +var currentAuthor = 'embedded_journalist'; + +tinymce.init({ + selector: '#textarea', + tinycomments_author: currentAuthor, + tinycomments_can_delete_comment: function (req, done, fail) { + var allowed = req.comment.author === currentAuthor; + done({ + canDelete: allowed || currentAuthor === '' + }); + } +}); +``` + +### `tinycomments_can_edit_comment` + +_Optional_ - This option sets the author permissions for _editing comments_. If the `tinycomments_can_edit_comment` option is not included, the current author (`tinycomments_author`) cannot edit comments added by other authors. + +**Type:** `Function` + +**Default Function** + +```js +function (req, done, fail) { + var allowed = req.comment.author === ; + done({ + canEdit: allowed + }); +} +``` + +The following example extends the default behavior to allow the author `` to edit other author's comments by adding `|| currentAuthor === ''`. + +#### Example: Using `tinycomments_can_edit_comment` + +```js +var currentAuthor = 'embedded_journalist'; + +tinymce.init({ + selector: '#textarea', + tinycomments_author: currentAuthor, + tinycomments_can_edit_comment: function (req, done, fail) { + var allowed = req.comment.author === currentAuthor; + done({ + canEdit: allowed || currentAuthor === '' + }); + } +}); +``` + +{% include plugins/comments_open_sidebar.md %} + +{% include plugins/comments_embed_fullpage_issues.md %} + +{% include plugins/comments_highlighting_css.md %} diff --git a/plugins/premium/comments/comments_toolbars_menus.md b/plugins/premium/comments/comments_toolbars_menus.md new file mode 100644 index 000000000..36a369796 --- /dev/null +++ b/plugins/premium/comments/comments_toolbars_menus.md @@ -0,0 +1,15 @@ +--- +layout: default +title: Toolbar buttons and menu items for the comments plugin +title_nav: Toolbar buttons and menu items +description: Details of the toolbar buttons and menu items provided for the comments plugin. +keywords: comments commenting tinycomments +--- + + +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} + +{% include misc/plugin-toolbar-button-id-boilerplate.md %} + +{% include misc/plugin-menu-item-id-boilerplate.md %} \ No newline at end of file diff --git a/plugins/premium/comments/comments_using_comments.md b/plugins/premium/comments/comments_using_comments.md new file mode 100644 index 000000000..a0e7a3a1c --- /dev/null +++ b/plugins/premium/comments/comments_using_comments.md @@ -0,0 +1,101 @@ +--- +layout: default +title: Using TinyMCE Comments +title_nav: Using Comments +description: How to add, edit, resolve, and remove comments in TinyMCE +keywords: comments commenting tinycomments +--- + +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} + +I'm trying to: + +- [Add a comment](#addacomment). +- [Edit a comment](#editacomment). +- [Delete a comment](#deleteacomment). +- [Delete a comment thread (conversation)](#deleteacommentthreadconversation). +- [Resolve a comment thread (conversation)](#resolveacommentthreadconversation). +- [Show or view a comment](#showorviewacomment). +- [Delete all comment threads](#deleteallcommentthreads). + +## Add a comment + +1. Select the text from the desired location in the editor body. +1. From the navigation menu, choose **Insert**-> **Add Comment** or click on the **Add comment** ![Add comment]({{site.baseurl}}/images/icons/comment-add.svg) toolbar button to add the comment. +1. The Comment box appears in the sidebar of the editor instance. +1. Type a comment in the comment box, the "_Say something…_" placeholder text will disappear. +1. Press **Clear** to discard or **Save** to store the input comment. + +**Result**: The selected text will be highlighted as per the configured options. The following screen with the option for editing, deleting, and replying to the comment, will appear. + +![Delete Conversation]({{site.baseurl}}/images/comments-edit.png) + +Note: The above procedure can be followed for adding multiple comments to the document. + +## Edit a comment + +Follow this procedure to edit a comment. + +1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. +1. Select **Edit** from the menu items. +1. The comment field becomes editable. Make the required changes. +1. Click **Cancel** to discard or **Save** to store the changes. + +## Delete a comment + +Follow this procedure to delete a comment. This option is not available for the first comment in a conversation. + +1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. +1. Select **Delete** from the menu items. +1. The following options appear in the comments sidebar:
+![delete comment]({{site.baseurl}}/images/comments-delete-comment.png) +1. Click **Cancel** to cancel the action or **Delete** to remove the comment from the conversation. + +## Delete a comment thread (conversation) + +This option is only available for the first comment in a conversation. Once the comment is saved, follow this procedure to delete a conversation. + +1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. +1. Select **Delete conversation** from the menu items. +1. The following decision dialog box will appear:
+![delete conversation]({{site.baseurl}}/images/comments-delete-conversation.png) +1. Click **Cancel** to cancel the action or **Delete** to remove the conversation. + +**Result**: The conversation and all its subsequent comments will be deleted. + +## Resolve a comment thread (conversation) + +{{site.requires_5_8v}} + +> **NOTE**: This feature requires the [`tinycomments_resolve`]({{site.baseurl}}/advanced/configuring-comments-callbacks/#tinycomments_resolve) or [`tinycomments_can_resolve`]({{site.baseurl}}/plugins/premium/comments/#tinycomments_can_resolve) setting to be configured. + +This option is only available for the first comment in a conversation. Once a comment is saved, follow this procedure to resolve a conversation. + +1. Click on the ellipsis ![(ellipsis - 3 horizontal dots)]({{site.baseurl}}/images/icons/image-options.svg) icon above the comments box to expand the menu. +1. Select **Resolve conversation** from the menu items. +1. The following decision dialog box will appear:
+![resolve conversation]({{site.baseurl}}/images/comments-resolve-conversation.png) +1. Click **Cancel** to cancel the action or **Resolve** to resolve the conversation. + +**Result**: The conversation will be resolved. + +## Show or view a comment + +Follow this procedure to display the comments sidebar: + +1. Place the cursor on the desired text in the editor body: +1. From the navigation menu, choose **View** -> **Show Comment** or click on the **Show Comments**![Comments]({{site.baseurl}}/images/comments-toolbar-button.png) toggle toolbar button to display the comment. + +**Result**: The comments sidebar will appear and display the corresponding conversation for the highlighted text. + +## Delete all comment threads + +Follow this procedure to delete all conversations in the document: + +1. From the navigation menu, choose **File** -> **Delete all conversations** option to delete all the comments in a document. +1. The following decision dialog box will appear:
+![Delete all conversations]({{site.baseurl}}/images/comments-delete-conversations.png) +1. Click **Ok** to remove the all the comments or **Cancel** to dismiss the action. + +**Result**: All the comments for the selected document will be deleted. diff --git a/plugins/premium/comments/index.md b/plugins/premium/comments/index.md new file mode 100644 index 000000000..a4e361400 --- /dev/null +++ b/plugins/premium/comments/index.md @@ -0,0 +1,25 @@ +--- +layout: default +title: Tiny Comments +title_nav: Comments +description_short: The TinyMCE Comments plugin +description: This section lists the premium plugins provided by Tiny. +type: folder +--- + +{% assign navigation = site.data.nav %} +{% for entry in navigation %} + {% if entry.url == "plugins" %} + {% for subentry in entry.pages %} + {% if subentry.url == "premium" %} + {% for subsubentry in subentry.pages %} + {% if subsubentry.url == "comments" %} + {% assign links = subsubentry.pages %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} +{% endfor %} + +{% include index.html links=links %} \ No newline at end of file diff --git a/plugins/premium/comments/introduction_to_tiny_comments.md b/plugins/premium/comments/introduction_to_tiny_comments.md new file mode 100644 index 000000000..d4aeb0ae3 --- /dev/null +++ b/plugins/premium/comments/introduction_to_tiny_comments.md @@ -0,0 +1,58 @@ +--- +layout: default +title: Introduction to Tiny Comments +title_nav: Introduction +description: Tiny Comments provides the ability to add comments to the content and collaborate with other users for content editing. +keywords: comments commenting tinycomments +--- + +{% assign pluginname = "Comments" %} +{% assign plugincode = "comments" %} + +## Contents + +* For help using comments in TinyMCE, see: [Using comments]({{site.baseurl}}/plugins/premium/comments/comments_using_comments/). +* For an overview of the TinyMCE Comments plugin, see: [Overview](#overview). +* For information on adding and configuring the comments plugin for TinyMCE, see: [Getting started with the Comments plugin - Selecting a mode](#gettingstartedwiththecommentsplugin-selectingamode). + +## Overview + +{{site.premiumplugin}} + +The Comments plugin provides the ability to start or join a conversation by adding comments to the content within the {{site.productname}} editor. + +### Collaborate on your projects within your content + +The Comments plugin provides: + +* A **user interface** to collaborate on content by creating and replying to comments. +* A way to control the delete and resolve operations on a comment or comment thread. + +### Primary Comments functions + +The Comments plugin allows the user to perform the following functions: + +* Create a comment +* Edit a comment +* Reply to a comment +* Lookup a comment +* Resolve a comment thread +* Delete a comment or comment thread + +### Interactive example + +The following example shows how to configure the Comments plugin in **embedded** mode. For information on configuring the Comments plugin, see: [Comments plugin Modes](#gettingstartedwiththecommentsplugin-selectingamode). + +{% include live-demo.html id="comments-embedded" %} + +## Getting started with the Comments plugin - Selecting a mode + +The Comments plugin is available in two _modes_: **Callback mode** and **Embedded mode**. + +Callback Mode +: This is the default mode for the Comments plugin. This mode is used to store the comments outside the content on a server, such as a database. This mode requires a number of callback functions to handle comment data. +: For instructions on configuring the Comments plugin in callback mode, see: [Configuring the Comments plugin in callback mode]({{site.baseurl}}/plugins/premium/comments/comments_callback_mode/) + +Embedded Mode +: This mode stores the comments within the content. No callbacks need to be configured for this mode. +: For instructions on configuring the Comments plugin in embedded mode, see: [Configuring the Comments plugin Comments in embedded mode]({{site.baseurl}}/plugins/premium/comments/comments_embedded_mode/)