diff --git a/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.html b/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.html index 5e0878b4c0..5ce9007a4d 100644 --- a/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.html +++ b/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.html @@ -7,7 +7,7 @@

Welcome to Tiny Comments!

  • Type your comment into the text field at the bottom of the Comment sidebar, and use @ followed by a name to mention a collaborator.
  • Click Comment.
  • -

    Your comment is then attached to the text, exactly like this! You can @Jenny Nichols directly in your comments to notify them.

    +

    Your comment is then attached to the text, exactly like this! You can @Mia Andersson directly in your comments to notify them.

    If you want to take Tiny Comments for a test drive in your own environment, Tiny Comments is one of the premium plugins you can try for free for 14 days by signing up for a Tiny account. Make sure to check out our documentation as well.

    A simple table to play with

    @@ -29,4 +29,4 @@

    A simple table to play with

    Thanks for supporting TinyMCE! We hope it helps your users create great content.

    - + \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.js b/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.js index bb74433204..0dce5c9a2f 100644 --- a/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.js +++ b/modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.js @@ -1,198 +1,107 @@ -import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then(({ faker }) => { - const userDb = { - 'michaelcook': { - id: 'michaelcook', - name: 'Michael Cook', - fullName: 'Michael Cook', - description: 'Product Owner', - image: "{{imagesdir}}/avatars/michaelcook.png" - }, - 'kalebwilson': { - id: 'kalebwilson', - name: 'Kaleb Wilson', - fullName: 'Kaleb Wilson', - description: 'Marketing Director', - image: "{{imagesdir}}/avatars/kalebwilson.png" - } - }; - - const currentUid = 'kalebwilson'; - const adminUid = 'michaelcook'; - - const currentUser = userDb[currentUid]; - const adminUser = userDb[adminUid]; - - const now = new Date(); - const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); - const anhourago = new Date(now.getTime() - 60 * 60 * 1000).toISOString(); - - const fillAuthorInfo = (id, fullName, image) => ({ - author: id, - authorName: fullName, - authorAvatar: image, - }); - - const getAuthorInfo = (uid) => { - const user = userDb[uid]; - if (user) { - return fillAuthorInfo(user.id, user.fullName, user.image); - } - return { - author: uid, - authorName: uid, - }; - }; - - const conversationDb = { - 'mce-conversation_19679600221621399703915': { +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const user_id = 'james-wilson'; +const collaborator_id = 'mia-andersson'; + +const now = new Date(); +const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); +const anhourago = new Date(now.getTime() - 60 * 60 * 1000).toISOString(); +const fakeDelay = 200; + +const randomString = () => { + return crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14); +}; + +const conversationDb = { + 'mce-conversation_19679600221621399703915': { + uid: 'mce-conversation_19679600221621399703915', + comments: [{ uid: 'mce-conversation_19679600221621399703915', - comments: [{ - uid: 'mce-conversation_19679600221621399703915', - ...getAuthorInfo(currentUid), - content: `What do you think about this? @${adminUser.id}?`, - createdAt: yesterday, - modifiedAt: yesterday - }, { - uid: 'mce-conversation_19679600221621399703917', - ...getAuthorInfo(adminUid), - content: `I think this is a great idea @${currentUser.id}!`, - createdAt: anhourago, - modifiedAt: anhourago, - }] - }, - 'mce-conversation_420304606321716900864126': { + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', + content: `What do you think about this? @${collaborator_id}?`, + createdAt: yesterday, + modifiedAt: yesterday + }, { + uid: 'mce-conversation_19679600221621399703917', + author: collaborator_id, + authorName: 'Mia Andersson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', + content: `I think this is a great idea @${user_id}!`, + createdAt: anhourago, + modifiedAt: anhourago, + }] + }, + 'mce-conversation_420304606321716900864126': { + uid: 'mce-conversation_420304606321716900864126', + comments: [{ uid: 'mce-conversation_420304606321716900864126', - comments: [{ - uid: 'mce-conversation_420304606321716900864126', - ...getAuthorInfo(adminUid), - content: `@${currentUser.id} Please revise this sentence, exclamation points are unprofessional!`, - createdAt: yesterday, - modifiedAt: anhourago - }] - } - }; - - const fakeDelay = 200; - const numberOfUsers = 200; - const randomString = () => { - return crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14); - }; - - /* These are "local" caches of the data returned from the fake server */ - let fetchedUsers = false; - let usersRequest; // Promise - const userRequest = {}; - const resolvedConversationDb = {}; - - const setupFakeServer = () => { - const images = [ adminUser.image, currentUser.image ]; - const userNames = [ adminUser.fullName, currentUser.fullName ]; + author: collaborator_id, + authorName: 'Mia Andersson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', + content: `@${user_id} Please revise this sentence, exclamation points are unprofessional!`, + createdAt: yesterday, + modifiedAt: anhourago + }] + } +}; - for (let i = 0; i < numberOfUsers; i++) { - images.push(faker.image.avatar()); - userNames.push(`${faker.person.firstName()} ${faker.person.lastName()}`); - } - - /* This represents a database of users on the server */ - const userDb = { - [adminUser.id]: adminUser, - [currentUser.id]: currentUser - }; - userNames.map((fullName) => { - if ((fullName !== currentUser.fullName) && (fullName !== adminUser.fullName)) { - const id = fullName.toLowerCase().replace(/ /g, ''); - userDb[id] = { - id, - name: fullName, - fullName, - description: faker.person.jobTitle(), - image: images[Math.floor(images.length * Math.random())] - }; - } - }); - - /* This represents getting the complete list of users from the server with the details required for the mentions "profile" item */ - const fetchUsers = () => new Promise((resolve) => { - /* simulate a server delay */ - setTimeout(() => { - const users = Object.keys(userDb).map((id) => ({ - id, - name: userDb[id].name, - image: userDb[id].image, - description: userDb[id].description - })); - resolve(users); - }, fakeDelay); - }); - - const fetchUser = (id) => new Promise((resolve, reject) => { - /* simulate a server delay */ - setTimeout(() => { - if (Object.prototype.hasOwnProperty.call(userDb, id)) { - resolve(userDb[id]); - } - reject('unknown user id "' + id + '"'); - }, fakeDelay); - }); +const mentions_fetch = async (query, success) => { + const searchPhrase = query.term.toLowerCase(); + await fetch(`${API_URL}?q=${encodeURIComponent(searchPhrase)}`) + .then((response) => response.json()) + .then((users) => success(users.data.map((userInfo) => ({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + })))) + .catch((error) => console.log(error)); +}; - return { - fetchUsers, - fetchUser - }; - }; +const mentions_menu_complete = (editor, userInfo) => { + const span = editor.getDoc().createElement('span'); + span.className = 'mymention'; + span.setAttribute('data-mention-id', userInfo.id); + span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); + return span; +}; - const fakeServer = setupFakeServer(); - - /******************************** - * Mentions functions * - ********************************/ - - const mentions_fetch = (query, success) => { - if (!fetchedUsers) { - fetchedUsers = true; - usersRequest = fakeServer.fetchUsers(); - } - usersRequest.then((users) => { - const userMatch = (name) => name.toLowerCase().indexOf(query.term.toLowerCase()) !== -1; - success(users.filter((user) => userMatch(user.name) || userMatch(user.id))); - fetchedUsers = false; - }); - }; - - const mentions_menu_hover = (userInfo, success) => { - if (!userRequest[userInfo.id]) { - userRequest[userInfo.id] = fakeServer.fetchUser(userInfo.id); - } - userRequest[userInfo.id].then((userDetail) => { - success({ type: 'profile', user: userDetail }); - }); - }; - - const mentions_menu_complete = (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - span.setAttribute('style', 'color: #37F;'); - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - }; - - const mentions_select = (mention, success) => { - const id = mention.getAttribute('data-mention-id'); - if (id) { - userRequest[id] = fakeServer.fetchUser(id); - userRequest[id].then((userDetail) => { - success({ type: 'profile', user: userDetail }); +const createCard = (userInfo) => { + const div = document.createElement('div'); + div.innerHTML = ( + '
    ' + + '' + + '

    ' + userInfo.name + '

    ' + + '

    ' + userInfo.description + '

    ' + + '
    ' + ); + return div; +}; + +const mentions_select = async (mention, success) => { + const id = mention.getAttribute('data-mention-id'); + await fetch(`${API_URL}/${id}`) + .then((response) => response.json()) + .then((userInfo) => { + const card = createCard({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role }); - } - }; - - /******************************** - * Tiny Comments functions * - * (must call "done" or "fail") * - ********************************/ + success(card); + }) + .catch((error) => console.error(error)); +}; - const tinycomments_create = (req, done, fail) => { +const mentions_menu_hover = async (userInfo, success) => { + const card = createCard(userInfo); + success(card); +}; + +const tinycomments_create = (req, done, fail) => { if (req.content === 'fail') { fail(new Error('Something has gone wrong...')); } else { @@ -201,7 +110,9 @@ import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then uid, comments: [{ uid, - ...getAuthorInfo(currentUid), + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', content: req.content, createdAt: req.createdAt, modifiedAt: req.createdAt @@ -211,157 +122,155 @@ import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then } }; - const tinycomments_reply = (req, done) => { - const replyUid = 'annotation-' + randomString(); - conversationDb[req.conversationUid].comments.push({ - uid: replyUid, - ...getAuthorInfo(currentUid), - content: req.content, - createdAt: req.createdAt, - modifiedAt: req.createdAt - }); - setTimeout(() => done({ commentUid: replyUid }), fakeDelay); - }; +const tinycomments_reply = (req, done) => { + const replyUid = 'annotation-' + randomString(); + conversationDb[req.conversationUid].comments.push({ + uid: replyUid, + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', + content: req.content, + createdAt: req.createdAt, + modifiedAt: req.createdAt + }); + setTimeout(() => done({ commentUid: replyUid }), fakeDelay); +}; - const tinycomments_delete = (req, done) => { - if (currentUid === adminUid) { // Replace wth your own logic, e.g. check if user created the conversation - delete conversationDb[req.conversationUid]; - setTimeout(() => done({ canDelete: true }), fakeDelay); - } else { - setTimeout(() => done({ canDelete: false, reason: 'Must be admin user' }), fakeDelay); - } - }; +const tinycomments_delete = (req, done) => { + if (user_id === collaborator_id) { // Replace wth your own logic, e.g. check if user created the conversation + delete conversationDb[req.conversationUid]; + setTimeout(() => done({ canDelete: true }), fakeDelay); + } else { + setTimeout(() => done({ canDelete: false, reason: 'Must be admin user' }), fakeDelay); + } +}; - const tinycomments_resolve = (req, done) => { - const conversation = conversationDb[req.conversationUid]; - if (currentUid === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges - delete conversationDb[req.conversationUid]; - setTimeout(() => done({ canResolve: true }), fakeDelay); - } else { - setTimeout(() => done({ canResolve: false, reason: 'Must be conversation author' }), fakeDelay); - } - }; +const tinycomments_resolve = (req, done) => { + const conversation = conversationDb[req.conversationUid]; + if (user_id === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges + delete conversationDb[req.conversationUid]; + setTimeout(() => done({ canResolve: true }), fakeDelay); + } else { + setTimeout(() => done({ canResolve: false, reason: 'Must be conversation author' }), fakeDelay); + } +}; - const tinycomments_delete_comment = (req, done) => { - const oldcomments = conversationDb[req.conversationUid].comments; - let reason = 'Comment not found'; +const tinycomments_delete_comment = (req, done) => { + const oldcomments = conversationDb[req.conversationUid].comments; + let reason = 'Comment not found'; - const newcomments = oldcomments.filter((comment) => { - if (comment.uid === req.commentUid) { // Found the comment to delete - if (currentUid === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges - return false; // Remove the comment - } else { - reason = 'Not authorised to delete this comment'; // Update reason - } + const newcomments = oldcomments.filter((comment) => { + if (comment.uid === req.commentUid) { // Found the comment to delete + if (user_id === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges + return false; // Remove the comment + } else { + reason = 'Not authorised to delete this comment'; // Update reason } - return true; // Keep the comment - }); - if (newcomments.length === oldcomments.length) { - setTimeout(() => done({ canDelete: false, reason }), fakeDelay); - } else { - conversationDb[req.conversationUid].comments = newcomments; - setTimeout(() => done({ canDelete: true }), fakeDelay); } - }; + return true; // Keep the comment + }); - const tinycomments_edit_comment = (req, done) => { - const oldcomments = conversationDb[req.conversationUid].comments; - let reason = 'Comment not found'; - let canEdit = false; + if (newcomments.length === oldcomments.length) { + setTimeout(() => done({ canDelete: false, reason }), fakeDelay); + } else { + conversationDb[req.conversationUid].comments = newcomments; + setTimeout(() => done({ canDelete: true }), fakeDelay); + } +}; - const newcomments = oldcomments.map((comment) => { - if (comment.uid === req.commentUid) { // Found the comment to delete - if (currentUid === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges - canEdit = true; // User can edit the comment - return { ...comment, content: req.content, modifiedAt: new Date().toISOString() }; // Update the comment - } else { - reason = 'Not authorised to edit this comment'; // Update reason - } - } - return comment; // Keep the comment - }); +const tinycomments_edit_comment = (req, done) => { + const oldcomments = conversationDb[req.conversationUid].comments; + let reason = 'Comment not found'; + let canEdit = false; - if (canEdit) { - conversationDb[req.conversationUid].comments = newcomments; - setTimeout(() => done({ canEdit }), fakeDelay); - } else { - setTimeout(() => done({ canEdit, reason }), fakeDelay); + const newcomments = oldcomments.map((comment) => { + if (comment.uid === req.commentUid) { // Found the comment to delete + if (user_id === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges + canEdit = true; // User can edit the comment + return { ...comment, content: req.content, modifiedAt: new Date().toISOString() }; // Update the comment + } else { + reason = 'Not authorised to edit this comment'; // Update reason + } } - }; + return comment; // Keep the comment + }); - const tinycomments_delete_all = (req, done) => { - const conversation = conversationDb[req.conversationUid]; - if (currentUid === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges - delete conversationDb[req.conversationUid]; - setTimeout(() => done({ canDelete: true }), fakeDelay); - } else { - setTimeout(() => done({ canDelete: false, reason: 'Must be conversation author' }), fakeDelay); - } - }; + if (canEdit) { + conversationDb[req.conversationUid].comments = newcomments; + setTimeout(() => done({ canEdit }), fakeDelay); + } else { + setTimeout(() => done({ canEdit, reason }), fakeDelay); + } +}; - const tinycomments_lookup = (req, done) => { - setTimeout(() => { - done({ - conversation: { - uid: conversationDb[req.conversationUid].uid, - comments: [...conversationDb[req.conversationUid].comments] - } - }); - }, fakeDelay); - }; +const tinycomments_delete_all = (req, done) => { + const conversation = conversationDb[req.conversationUid]; + if (user_id === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges + delete conversationDb[req.conversationUid]; + setTimeout(() => done({ canDelete: true }), fakeDelay); + } else { + setTimeout(() => done({ canDelete: false, reason: 'Must be conversation author' }), fakeDelay); + } +}; - const tinycomments_fetch = (conversationUids, done) => { - const fetchedConversations = {}; - conversationUids.forEach((uid) => { - const conversation = conversationDb[uid]; - if (conversation) { - fetchedConversations[uid] = {...conversation}; +const tinycomments_lookup = (req, done) => { + setTimeout(() => { + done({ + conversation: { + uid: conversationDb[req.conversationUid].uid, + comments: [...conversationDb[req.conversationUid].comments] } }); - setTimeout(() => done({ conversations: fetchedConversations }), fakeDelay); - }; + }, fakeDelay); +}; - // Read the above `getAuthorInfo` function to see how this could be implemented - const tinycomments_fetch_author_info = (done) => done(getAuthorInfo(currentUid)); - - tinymce.init({ - selector: 'textarea#comments-callback-with-mentions', - license_key: 'gpl', - toolbar: 'addcomment showcomments code | bold italic underline', - menubar: 'file edit view insert format tools tc help', - menu: { - tc: { - title: 'TinyComments', - items: 'addcomment showcomments deleteallconversations' - } - }, - plugins: [ 'tinycomments', 'mentions', 'help', 'code', 'quickbars', 'link', 'lists', 'image' ], - quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments', - quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', - tinycomments_mentions_enabled: true, - sidebar_show: 'showcomments', - - mentions_item_type: 'profile', - mentions_min_chars: 0, - mentions_selector: '.mymention', - mentions_fetch, - mentions_menu_hover, - mentions_menu_complete, - mentions_select, - - tinycomments_mode: 'callback', - tinycomments_author: currentUser.id, - tinycomments_author_name: currentUser.fullName, - tinycomments_avatar: currentUser.image, - tinycomments_create, - tinycomments_reply, - tinycomments_delete, - tinycomments_resolve, - tinycomments_delete_all, - tinycomments_lookup, - tinycomments_delete_comment, - tinycomments_edit_comment, - tinycomments_fetch, - tinycomments_fetch_author_info +const tinycomments_fetch = (conversationUids, done) => { + const fetchedConversations = {}; + conversationUids.forEach((uid) => { + const conversation = conversationDb[uid]; + if (conversation) { + fetchedConversations[uid] = {...conversation}; + } }); -}); + setTimeout(() => done({ conversations: fetchedConversations }), fakeDelay); +}; + +tinymce.init({ + selector: 'textarea#comments-callback-with-mentions', + license_key: 'gpl', + toolbar: 'addcomment showcomments code | bold italic underline', + menubar: 'file edit view insert format tools tc help', + menu: { + tc: { + title: 'TinyComments', + items: 'addcomment showcomments deleteallconversations' + } + }, + plugins: [ 'tinycomments', 'mentions', 'help', 'code', 'quickbars', 'link', 'lists', 'image' ], + quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments', + quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', + + tinycomments_mode: 'callback', + tinycomments_mentions_enabled: true, + sidebar_show: 'showcomments', + tinycomments_create, + tinycomments_reply, + tinycomments_delete, + tinycomments_resolve, + tinycomments_delete_all, + tinycomments_lookup, + tinycomments_delete_comment, + tinycomments_edit_comment, + tinycomments_fetch, + + mentions_item_type: 'profile', + mentions_min_chars: 0, + mentions_selector: '.mymention', + mentions_fetch, + mentions_menu_hover, + mentions_menu_complete, + mentions_select, + tinycomments_author: user_id, + tinycomments_author_name: 'James Wilson', + tinycomments_author_avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg' +}); \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-callback-with-mentions/style.css b/modules/ROOT/examples/live-demos/comments-callback-with-mentions/style.css new file mode 100644 index 0000000000..5e88248737 --- /dev/null +++ b/modules/ROOT/examples/live-demos/comments-callback-with-mentions/style.css @@ -0,0 +1,45 @@ +span.mymention { + color: gray !important; +} + +div.card, +.tox div.card { + width: 240px; + background: white; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0 4px 8px 0 rgba(34, 47, 62, .1); + padding: 8px; + font-size: 14px; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; +} + +div.card::after, +.tox div.card::after { + content: ""; + clear: both; + display: table; +} + +div.card h1, +.tox div.card h1 { + font-size: 14px; + font-weight: bold; + margin: 0 0 8px; + padding: 0; + line-height: normal; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; +} + +div.card p, +.tox div.card p { + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; +} + +div.card img.avatar, +.tox div.card img.avatar { + width: 48px; + height: 48px; + margin-right: 8px; + float: left; +} \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-callback/index.html b/modules/ROOT/examples/live-demos/comments-callback/index.html index 92c933f6e6..c018be0246 100644 --- a/modules/ROOT/examples/live-demos/comments-callback/index.html +++ b/modules/ROOT/examples/live-demos/comments-callback/index.html @@ -7,7 +7,7 @@

    Welcome to Tiny Comments!

  • Type your comment into the text field at the bottom of the Comment sidebar.
  • Click Comment.
  • -

    Your comment is then attached to the text, exactly like this!

    +

    Your comment is then attached to the text, exactly like this!

    If you want to take Tiny Comments for a test drive in your own environment, Tiny Comments is one of the premium plugins you can try for free for 14 days by signing up for a Tiny account. Make sure to check out our documentation as well.

    A simple table to play with

    @@ -29,4 +29,4 @@

    A simple table to play with

    Thanks for supporting TinyMCE! We hope it helps your users create great content.

    - + \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-callback/index.js b/modules/ROOT/examples/live-demos/comments-callback/index.js index 58dcd8ecbe..ac03cd0084 100644 --- a/modules/ROOT/examples/live-demos/comments-callback/index.js +++ b/modules/ROOT/examples/live-demos/comments-callback/index.js @@ -1,46 +1,15 @@ -/******************************** - * Demo-specific configuration * - ********************************/ - -const userDb = { - 'michaelcook': { - id: 'michaelcook', - name: 'Michael Cook', - fullName: 'Michael Cook', - description: 'Product Owner', - image: "{{imagesdir}}/avatars/michaelcook.png" - }, - 'kalebwilson': { - id: 'kalebwilson', - name: 'Kaleb Wilson', - fullName: 'Kaleb Wilson', - description: 'Marketing Director', - image: "{{imagesdir}}/avatars/kalebwilson.png" - } -}; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; -const currentUid = 'kalebwilson'; -const adminUid = 'michaelcook'; +const user_id = 'james-wilson'; +const collaborator_id = 'mia-andersson'; const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); const anhourago = new Date(now.getTime() - 60 * 60 * 1000).toISOString(); +const fakeDelay = 200; -const fillAuthorInfo = (id, fullName, image) => ({ - author: id, - authorName: fullName, - authorAvatar: image, -}); - -const getAuthorInfo = (uid) => { - const user = userDb[uid]; - if (user) { - return fillAuthorInfo(user.id, user.fullName, user.image); - } - return { - author: uid, - authorName: uid, - }; +const randomString = () => { + return crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14); }; const conversationDb = { @@ -48,13 +17,17 @@ const conversationDb = { uid: 'mce-conversation_19679600221621399703915', comments: [{ uid: 'mce-conversation_19679600221621399703915', - ...getAuthorInfo(currentUid), + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', content: `What do you think about this?`, createdAt: yesterday, modifiedAt: yesterday }, { uid: 'mce-conversation_19679600221621399703917', - ...getAuthorInfo(adminUid), + author: collaborator_id, + authorName: 'Mia Andersson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', content: `I think this is a great idea!`, createdAt: anhourago, modifiedAt: anhourago, @@ -64,7 +37,9 @@ const conversationDb = { uid: 'mce-conversation_420304606321716900864126', comments: [{ uid: 'mce-conversation_420304606321716900864126', - ...getAuthorInfo(adminUid), + author: collaborator_id, + authorName: 'Mia Andersson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', content: `Please revise this sentence, exclamation points are unprofessional!`, createdAt: yesterday, modifiedAt: anhourago @@ -72,40 +47,34 @@ const conversationDb = { } }; -const fakeDelay = 200; -const randomString = () => crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14); - -const resolvedConversationDb = {}; - -/******************************** - * Tiny Comments functions * - * (must call "done" or "fail") * - ********************************/ - const tinycomments_create = (req, done, fail) => { - if (req.content === 'fail') { - fail(new Error('Something has gone wrong...')); - } else { - const uid = 'annotation-' + randomString(); - conversationDb[uid] = { - uid, - comments: [{ + if (req.content === 'fail') { + fail(new Error('Something has gone wrong...')); + } else { + const uid = 'annotation-' + randomString(); + conversationDb[uid] = { uid, - ...getAuthorInfo(currentUid), - content: req.content, - createdAt: req.createdAt, - modifiedAt: req.createdAt - }] - }; - setTimeout(() => done({ conversationUid: uid }), fakeDelay); - } -}; + comments: [{ + uid, + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', + content: req.content, + createdAt: req.createdAt, + modifiedAt: req.createdAt + }] + }; + setTimeout(() => done({ conversationUid: uid }), fakeDelay); + } + }; const tinycomments_reply = (req, done) => { const replyUid = 'annotation-' + randomString(); conversationDb[req.conversationUid].comments.push({ uid: replyUid, - ...getAuthorInfo(currentUid), + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', content: req.content, createdAt: req.createdAt, modifiedAt: req.createdAt @@ -114,7 +83,7 @@ const tinycomments_reply = (req, done) => { }; const tinycomments_delete = (req, done) => { - if (currentUid === adminUid) { // Replace wth your own logic, e.g. check if user created the conversation + if (user_id === collaborator_id) { // Replace wth your own logic, e.g. check if user created the conversation delete conversationDb[req.conversationUid]; setTimeout(() => done({ canDelete: true }), fakeDelay); } else { @@ -124,7 +93,7 @@ const tinycomments_delete = (req, done) => { const tinycomments_resolve = (req, done) => { const conversation = conversationDb[req.conversationUid]; - if (currentUid === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges + if (user_id === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges delete conversationDb[req.conversationUid]; setTimeout(() => done({ canResolve: true }), fakeDelay); } else { @@ -138,7 +107,7 @@ const tinycomments_delete_comment = (req, done) => { const newcomments = oldcomments.filter((comment) => { if (comment.uid === req.commentUid) { // Found the comment to delete - if (currentUid === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges + if (user_id === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges return false; // Remove the comment } else { reason = 'Not authorised to delete this comment'; // Update reason @@ -146,6 +115,7 @@ const tinycomments_delete_comment = (req, done) => { } return true; // Keep the comment }); + if (newcomments.length === oldcomments.length) { setTimeout(() => done({ canDelete: false, reason }), fakeDelay); } else { @@ -161,7 +131,7 @@ const tinycomments_edit_comment = (req, done) => { const newcomments = oldcomments.map((comment) => { if (comment.uid === req.commentUid) { // Found the comment to delete - if (currentUid === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges + if (user_id === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges canEdit = true; // User can edit the comment return { ...comment, content: req.content, modifiedAt: new Date().toISOString() }; // Update the comment } else { @@ -181,7 +151,7 @@ const tinycomments_edit_comment = (req, done) => { const tinycomments_delete_all = (req, done) => { const conversation = conversationDb[req.conversationUid]; - if (currentUid === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges + if (user_id === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges delete conversationDb[req.conversationUid]; setTimeout(() => done({ canDelete: true }), fakeDelay); } else { @@ -211,13 +181,9 @@ const tinycomments_fetch = (conversationUids, done) => { setTimeout(() => done({ conversations: fetchedConversations }), fakeDelay); }; -// Read the above `getAuthorInfo` function to see how this could be implemented -const tinycomments_fetch_author_info = (done) => done(getAuthorInfo(currentUid)); - tinymce.init({ selector: 'textarea#comments-callback', license_key: 'gpl', - height: 800, toolbar: 'addcomment showcomments code | bold italic underline', menubar: 'file edit view insert format tools tc help', menu: { @@ -226,11 +192,13 @@ tinymce.init({ items: 'addcomment showcomments deleteallconversations' } }, - plugins: ['tinycomments', 'help', 'code', 'quickbars', 'link', 'lists', 'image'], + plugins: [ 'tinycomments', 'help', 'code', 'quickbars', 'link', 'lists', 'image' ], quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments', quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', - sidebar_show: 'showcomments', + tinycomments_mode: 'callback', + tinycomments_mentions_enabled: true, + sidebar_show: 'showcomments', tinycomments_create, tinycomments_reply, tinycomments_delete, @@ -240,5 +208,7 @@ tinymce.init({ tinycomments_delete_comment, tinycomments_edit_comment, tinycomments_fetch, - tinycomments_fetch_author_info, -}); + tinycomments_author: user_id, + tinycomments_author_name: 'James Wilson', + tinycomments_author_avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg' +}); \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.html b/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.html index 4f012193f1..cd92933adc 100644 --- a/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.html +++ b/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.html @@ -1,35 +1,33 @@ -
    - -
    \ No newline at end of file + \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.js b/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.js index c2639167f7..1c1d871571 100644 --- a/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.js +++ b/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/index.js @@ -1,163 +1,93 @@ -import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then(({ faker }) => { - const adminUser = { - id: 'johnsmith', - name: 'John Smith', - fullName: 'John Smith', - description: 'Company Founder', - image: "https://i.pravatar.cc/150?img=11" - }; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; - const currentUser = { - id: 'jennynichols', - name: 'Jenny Nichols', - fullName: 'Jenny Nichols', - description: 'Marketing Director', - image: "https://i.pravatar.cc/150?img=10" - }; - - const fakeDelay = 500; - const numberOfUsers = 200; - - /* These are "local" caches of the data returned from the fake server */ - let fetchedUsers = false; - let usersRequest; // Promise - const userRequest = {}; - - const setupFakeServer = () => { - const images = [ adminUser.image, currentUser.image ]; - const userNames = [ adminUser.fullName, currentUser.fullName ]; +const user_id = 'james-wilson'; - for (let i = 0; i < numberOfUsers; i++) { - images.push(faker.image.avatar()); - userNames.push(`${faker.person.firstName()} ${faker.person.lastName()}`); - } - - /* This represents a database of users on the server */ - const userDb = { - [adminUser.id]: adminUser, - [currentUser.id]: currentUser - }; - userNames.map((fullName) => { - if ((fullName !== currentUser.fullName) && (fullName !== adminUser.fullName)) { - const id = fullName.toLowerCase().replace(/ /g, ''); - userDb[id] = { - id, - name: fullName, - fullName, - description: faker.person.jobTitle(), - image: images[Math.floor(images.length * Math.random())] - }; - } - }); - - /* This represents getting the complete list of users from the server with the details required for the mentions "profile" item */ - const fetchUsers = () => new Promise((resolve) => { - /* simulate a server delay */ - setTimeout(() => { - const users = Object.keys(userDb).map((id) => ({ - id, - name: userDb[id].name, - image: userDb[id].image, - description: userDb[id].description - })); - resolve(users); - }, fakeDelay); - }); - - const fetchUser = (id) => new Promise((resolve, reject) => { - /* simulate a server delay */ - setTimeout(() => { - if (Object.prototype.hasOwnProperty.call(userDb, id)) { - resolve(userDb[id]); - } - reject('unknown user id "' + id + '"'); - }, fakeDelay); - }); +const mentions_fetch = async (query, success) => { + const searchPhrase = query.term.toLowerCase(); + await fetch(`${API_URL}?q=${encodeURIComponent(searchPhrase)}`) + .then((response) => response.json()) + .then((users) => success(users.data.map((userInfo) => ({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + })))) + .catch((error) => console.log(error)); +}; - return { - fetchUsers, - fetchUser - }; - }; +const mentions_menu_complete = (editor, userInfo) => { + const span = editor.getDoc().createElement('span'); + span.className = 'mymention'; + span.setAttribute('data-mention-id', userInfo.id); + span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); + return span; +}; - const fakeServer = setupFakeServer(); - - const mentions_fetch = (query, success) => { - if (!fetchedUsers) { - fetchedUsers = true; - usersRequest = fakeServer.fetchUsers(); - } - usersRequest.then((users) => { - const userMatch = (name) => name.toLowerCase().indexOf(query.term.toLowerCase()) !== -1; - success(users.filter((user) => userMatch(user.name) || userMatch(user.id))); - fetchedUsers = false; - }); - }; - - const mentions_menu_hover = (userInfo, success) => { - if (!userRequest[userInfo.id]) { - userRequest[userInfo.id] = fakeServer.fetchUser(userInfo.id); - } - userRequest[userInfo.id].then((userDetail) => { - success({ type: 'profile', user: userDetail }); - }); - }; - - const mentions_menu_complete = (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - span.setAttribute('style', 'color: #37F;'); - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - }; - - const mentions_select = (mention, success) => { - const id = mention.getAttribute('data-mention-id'); - if (id) { - userRequest[id] = fakeServer.fetchUser(id); - userRequest[id].then((userDetail) => { - success({ type: 'profile', user: userDetail }); +const createCard = (userInfo) => { + const div = document.createElement('div'); + div.innerHTML = ( + '
    ' + + '' + + '

    ' + userInfo.name + '

    ' + + '

    ' + userInfo.description + '

    ' + + '
    ' + ); + return div; +}; + +const mentions_select = async (mention, success) => { + const id = mention.getAttribute('data-mention-id'); + await fetch(`${API_URL}/${id}`) + .then((response) => response.json()) + .then((userInfo) => { + const card = createCard({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role }); + success(card); + }) + .catch((error) => console.error(error)); +}; + +const mentions_menu_hover = async (userInfo, success) => { + const card = createCard(userInfo); + success(card); +}; + +const tinycomments_can_resolve = (req, done, _fail) => { + const allowed = req.comments.length > 0 && req.comments[0].author === user_id; + done({ canResolve: allowed }); +}; + +tinymce.init({ + selector: 'textarea#comments-embedded-with-mentions', + plugins: [ 'tinycomments', 'mentions', 'help', 'code', 'quickbars', 'link', 'lists', 'image' ], + toolbar: 'addcomment showcomments code | bold italic underline', + menubar: 'file edit view insert format tools tc help', + menu: { + tc: { + title: 'TinyComments', + items: 'addcomment showcomments deleteallconversations' } - }; + }, + quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments', + quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', + + tinycomments_mode: 'embedded', + sidebar_show: 'showcomments', + tinycomments_mentions_enabled: true, + tinycomments_can_resolve, + tinycomments_author: user_id, + tinycomments_author_name: 'James Wilson', + tinycomments_author_avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', - const tinycomments_can_resolve = (req, done, _fail) => { - const allowed = req.comments.length > 0 && req.comments[0].author === author; - done({ - canResolve: allowed - }); - }; - - tinymce.init({ - selector: 'textarea#comments-embedded', - license_key: 'gpl', - toolbar: 'addcomment showcomments code | bold italic underline', - menubar: 'file edit view insert format tools tc help', - menu: { - tc: { - title: 'TinyComments', - items: 'addcomment showcomments deleteallconversations' - } - }, - plugins: [ 'tinycomments', 'mentions', 'help', 'code', 'quickbars', 'link', 'lists', 'image' ], - quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments', - quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', - tinycomments_mentions_enabled: true, - - mentions_item_type: 'profile', - mentions_min_chars: 0, - mentions_selector: '.mymention', - mentions_fetch, - mentions_menu_hover, - mentions_menu_complete, - mentions_select, - - tinycomments_mode: 'embedded', - sidebar_show: 'showcomments', - tinycomments_author: currentUser.id, - tinycomments_author_name: currentUser.fullName, - tinycomments_avatar: currentUser.image, - tinycomments_can_resolve, - }); -}); + mentions_item_type: 'profile', + mentions_min_chars: 0, + mentions_selector: '.mymention', + mentions_fetch, + mentions_menu_hover, + mentions_menu_complete, + mentions_select, +}); \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/style.css b/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/style.css new file mode 100644 index 0000000000..5e88248737 --- /dev/null +++ b/modules/ROOT/examples/live-demos/comments-embedded-with-mentions/style.css @@ -0,0 +1,45 @@ +span.mymention { + color: gray !important; +} + +div.card, +.tox div.card { + width: 240px; + background: white; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0 4px 8px 0 rgba(34, 47, 62, .1); + padding: 8px; + font-size: 14px; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; +} + +div.card::after, +.tox div.card::after { + content: ""; + clear: both; + display: table; +} + +div.card h1, +.tox div.card h1 { + font-size: 14px; + font-weight: bold; + margin: 0 0 8px; + padding: 0; + line-height: normal; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; +} + +div.card p, +.tox div.card p { + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; +} + +div.card img.avatar, +.tox div.card img.avatar { + width: 48px; + height: 48px; + margin-right: 8px; + float: left; +} \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-embedded/index.html b/modules/ROOT/examples/live-demos/comments-embedded/index.html index 17ed2eaa24..98d8a62a76 100644 --- a/modules/ROOT/examples/live-demos/comments-embedded/index.html +++ b/modules/ROOT/examples/live-demos/comments-embedded/index.html @@ -1,36 +1,34 @@ -
    - -
    \ No newline at end of file + \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/comments-embedded/index.js b/modules/ROOT/examples/live-demos/comments-embedded/index.js index d0aa9d9c21..eb2365ea46 100644 --- a/modules/ROOT/examples/live-demos/comments-embedded/index.js +++ b/modules/ROOT/examples/live-demos/comments-embedded/index.js @@ -1,10 +1,16 @@ -const currentAuthor = 'A Tiny User'; -const userAllowedToResolve = 'Admin1'; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const user_id = 'james-wilson'; + +const tinycomments_can_resolve = (req, done, _fail) => { + const allowed = req.comments.length > 0 && req.comments[0].author === user_id; + done({ canResolve: allowed }); +}; tinymce.init({ selector: 'textarea#comments-embedded', - plugins: 'code tinycomments quickbars link lists image', - toolbar: 'addcomment showcomments | bold italic underline', + plugins: [ 'tinycomments', 'help', 'code', 'quickbars', 'link', 'lists', 'image' ], + toolbar: 'addcomment showcomments code | bold italic underline', menubar: 'file edit view insert format tools tc', menu: { tc: { @@ -14,15 +20,11 @@ tinymce.init({ }, quickbars_selection_toolbar: 'alignleft aligncenter alignright | addcomment showcomments', quickbars_image_toolbar: 'alignleft aligncenter alignright | rotateleft rotateright | imageoptions', + content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', tinycomments_mode: 'embedded', sidebar_show: 'showcomments', - tinycomments_author: currentAuthor, - tinycomments_can_resolve: (req, done, fail) => { - const allowed = req.comments.length > 0 && - req.comments[0].author === currentAuthor; - done({ - canResolve: allowed || currentAuthor === userAllowedToResolve - }); - }, - content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', -}); + tinycomments_can_resolve, + tinycomments_author: user_id, + tinycomments_author_name: 'James Wilson', + tinycomments_author_avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg' +}); \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/full-featured/example.js b/modules/ROOT/examples/live-demos/full-featured/example.js deleted file mode 100644 index f3069b013d..0000000000 --- a/modules/ROOT/examples/live-demos/full-featured/example.js +++ /dev/null @@ -1,442 +0,0 @@ -const fetchApi = import('https://cdn.skypack.dev/@microsoft/fetch-event-source@2.0.1') - .then((module) => module.fetchEventSource); - - -// This example stores the OpenAI API key in the client side integration. This is not recommended for any purpose. -// Instead, an alternate method for retrieving the API key should be used. -const openai_api_key = ""; - -const isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches; - -tinymce.init({ - selector: 'textarea#full-featured', - plugins: 'importword exportword exportpdf ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker editimage help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate markdown revisionhistory', - tinydrive_token_provider: 'URL_TO_YOUR_TOKEN_PROVIDER', - tinydrive_dropbox_app_key: 'YOUR_DROPBOX_APP_KEY', - tinydrive_google_drive_key: 'YOUR_GOOGLE_DRIVE_KEY', - tinydrive_google_drive_client_id: 'YOUR_GOOGLE_DRIVE_CLIENT_ID', - mobile: { - plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate', - }, - menu: { - tc: { - title: 'Comments', - items: 'addcomment showcomments deleteallconversations' - } - }, - menubar: 'file edit view insert format tools table tc help', - toolbar: "undo redo | importword exportword exportpdf | revisionhistory | aidialog aishortcuts | blocks fontsizeinput | bold italic | align numlist bullist | link image | table math media pageembed | lineheight outdent indent | strikethrough forecolor backcolor formatpainter removeformat | charmap emoticons checklist | code fullscreen preview | save print | pagebreak anchor codesample footnotes mergetags | addtemplate inserttemplate | addcomment showcomments | ltr rtl casechange | spellcheckdialog a11ycheck", // Note: if a toolbar item requires a plugin, the item will not present in the toolbar if the plugin is not also loaded. - autosave_ask_before_unload: true, - autosave_interval: '30s', - autosave_prefix: '{path}{query}-{id}-', - autosave_restore_when_empty: false, - autosave_retention: '2m', - image_advtab: true, - typography_rules: [ - 'common/punctuation/quote', - 'en-US/dash/main', - 'common/nbsp/afterParagraphMark', - 'common/nbsp/afterSectionMark', - 'common/nbsp/afterShortWord', - 'common/nbsp/beforeShortLastNumber', - 'common/nbsp/beforeShortLastWord', - 'common/nbsp/dpi', - 'common/punctuation/apostrophe', - 'common/space/delBeforePunctuation', - 'common/space/afterComma', - 'common/space/afterColon', - 'common/space/afterExclamationMark', - 'common/space/afterQuestionMark', - 'common/space/afterSemicolon', - 'common/space/beforeBracket', - 'common/space/bracket', - 'common/space/delBeforeDot', - 'common/space/squareBracket', - 'common/number/mathSigns', - 'common/number/times', - 'common/number/fraction', - 'common/symbols/arrow', - 'common/symbols/cf', - 'common/symbols/copy', - 'common/punctuation/delDoublePunctuation', - 'common/punctuation/hellip' - ], - typography_ignore: [ 'code' ], - advtemplate_list: () => { - return Promise.resolve([ - { - id: '1', - title: 'Resolving tickets', - content: '

    As we have not heard back from you in over a week, we have gone ahead and resolved your ticket.

    ' - }, - { - id: '2', - title: 'Quick replies', - items: [ - { - id: '3', - title: 'Message received', - content: '

    Just a quick note to say we have received your message, and will get back to you within 48 hours.

    ' - }, - { - id: '4', - title: 'Progress update', - content: '

    Just a quick note to let you know we are still working on your case

    ' - } - ] - } - ]); - }, - link_list: [ - { title: 'My page 1', value: 'https://www.tiny.cloud' }, - { title: 'My page 2', value: 'http://www.moxiecode.com' } - ], - image_list: [ - { title: 'My page 1', value: 'https://www.tiny.cloud' }, - { title: 'My page 2', value: 'http://www.moxiecode.com' } - ], - image_class_list: [ - { title: 'None', value: '' }, - { title: 'Some class', value: 'class-name' } - ], - importcss_append: true, - height: 600, - image_caption: true, - quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', - noneditable_class: 'mceNonEditable', - toolbar_mode: 'sliding', - spellchecker_ignore_list: ['Ephox', 'Moxiecode', 'tinymce', 'TinyMCE'], - tinycomments_mode: 'embedded', - content_style: '.mymention{ color: gray; }', - contextmenu: 'link image editimage table configurepermanentpen', - a11y_advanced_options: true, - autocorrect_capitalize: true, - mergetags_list: [ - { - title: 'Client', - menu: [ - { - value: 'Client.LastCallDate', - title: 'Call date' - }, - { - value: 'Client.Name', - title: 'Client name' - } - ] - }, - { - title: 'Proposal', - menu: [ - { - value: 'Proposal.SubmissionDate', - title: 'Submission date' - } - ] - }, - { - value: 'Consultant', - title: 'Consultant' - }, - { - value: 'Salutation', - title: 'Salutation' - } - ], - // For revision history plugin - revisionhistory_fetch: () => { - return Promise.resolve([ - { - revisionId: '3', - createdAt: '2023-11-24T22:26:21.578Z', - author: { - id: 'husky', - name: 'A Tiny Husky', - avatar: '{{imagesdir}}/tiny-husky.jpg' - }, - content: ` -

    TinyMCE Logo

    -

    Welcome to the TinyMCE editor demo!

    -

    A simple table to play with

    - - - - - - - - - - - - - - - - - - - - -
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    -

    Found a bug?

    -

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    -

    Finally ...

    -

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    -

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    - `, - }, - { - revisionId: '2', - createdAt: '2023-11-25T08:30:21.578Z', - author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png' - }, - content: ` -

    TinyMCE Logo

    -

    Welcome to the TinyMCE editor demo!

    -

    Got questions or need help?

    -
      -
    1. Our documentation is a great resource for learning how to configure TinyMCE.
    2. -
    3. Have a specific question? Try the tinymce tag at Stack Overflow.
    4. -
    5. We also offer enterprise grade support as part of TinyMCE premium plans.
    6. -
    -

    A simple table to play with

    - - - - - - - - - - - - - - - - - - - - -
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    -

    Found a bug?

    -

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    -

    Finally ...

    -

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    -

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    - `, - }, - { - revisionId: '1', - createdAt: '2023-11-29T10:11:21.578Z', - author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png' - }, - content: ` -

    TinyMCE Logo

    -

    Welcome to the TinyMCE editor demo!

    -

    Got questions or need help?

    - -

    A simple table to play with

    - - - - - - - - - - - - - - - - - - - - -
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    -

    Found a bug?

    -

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    -

    Finally ...

    -

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    -

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    - `, - }, - ]); - }, - ai_request: (request, respondWith) => { - respondWith.stream((signal, streamMessage) => { - // Adds each previous query and response as individual messages - const conversation = request.thread.flatMap((event) => { - if (event.response) { - return [ - { role: "user", content: event.request.query }, - { role: "assistant", content: event.response.data }, - ]; - } else { - return []; - } - }); - - // System messages provided by the plugin to format the output as HTML content. - const systemMessages = request.system.map((content) => ({ - role: "system", - content, - })); - - // Forms the new query sent to the API - const content = - request.context.length === 0 || conversation.length > 0 - ? request.query - : `Question: ${request.query} Context: """${request.context}"""`; - - const messages = [ - ...conversation, - ...systemMessages, - { role: "user", content }, - ]; - - let hasHead = false; - let markdownHead = ""; - - const hasMarkdown = (message) => { - if (message.includes("`") && markdownHead !== "```") { - const numBackticks = message.split("`").length - 1; - markdownHead += "`".repeat(numBackticks); - if (hasHead && markdownHead === "```") { - markdownHead = ""; - hasHead = false; - } - return true; - } else if (message.includes("html") && markdownHead === "```") { - markdownHead = ""; - hasHead = true; - return true; - } - return false; - }; - - const requestBody = { - model: "gpt-4o", - temperature: 0.7, - max_tokens: 4000, - messages, - stream: true, - }; - - const openAiOptions = { - signal, - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${openai_api_key}`, - }, - body: JSON.stringify(requestBody), - }; - - const onopen = async (response) => { - if (response) { - const contentType = response.headers.get("content-type"); - if (response.ok && contentType?.includes("text/event-stream")) { - return; - } else if (contentType?.includes("application/json")) { - const data = await response.json(); - if (data.error) { - throw new Error(`${data.error.type}: ${data.error.message}`); - } - } - } else { - throw new Error("Failed to communicate with the ChatGPT API"); - } - }; - - // This function passes each new message into the plugin via the `streamMessage` callback. - const onmessage = (ev) => { - const data = ev.data; - if (data !== "[DONE]") { - const parsedData = JSON.parse(data); - const firstChoice = parsedData?.choices[0]; - const message = firstChoice?.delta?.content; - if (message && message !== "") { - if (!hasMarkdown(message)) { - streamMessage(message); - } - } - } - }; - - const onerror = (error) => { - // Stop operation and do not retry by the fetch-event-source - throw error; - }; - - // Use microsoft's fetch-event-source library to work around the 2000 character limit - // of the browser `EventSource` API, which requires query strings - return fetchApi - .then((fetchEventSource) => - fetchEventSource("https://api.openai.com/v1/chat/completions", { - ...openAiOptions, - openWhenHidden: true, - onopen, - onmessage, - onerror, - }) - ) - .then(async (response) => { - if (response && !response.ok) { - const data = await response.json(); - if (data.error) { - throw new Error(`${data.error.type}: ${data.error.message}`); - } - } - }) - .catch(onerror); - }); - }, - exportpdf_converter_options: { - 'format': 'Letter', - 'margin_top': '1in', - 'margin_right': '1in', - 'margin_bottom': '1in', - 'margin_left': '1in' - }, - exportword_converter_options: { - 'document': { - 'size': 'Letter' - } - }, - importword_converter_options: { - 'formatting': { - 'styles': 'inline', - 'resets': 'inline', - 'defaults': 'inline', - } - }, - /* - The following settings require more configuration than shown here. - For information on configuring the mentions plugin, see: - https://www.tiny.cloud/docs/tinymce/latest/mentions/. - */ - mentions_selector: ".mymention", - mentions_fetch: mentions_fetch, // TODO: Implement mentions_fetch - mentions_menu_hover: mentions_menu_hover, // TODO: Implement mentions_menu_hover - mentions_menu_complete: mentions_menu_complete, // TODO: Implement mentions_menu_complete - mentions_select: mentions_select, // TODO: Implement mentions_select - mentions_item_type: "profile", -}); diff --git a/modules/ROOT/examples/live-demos/full-featured/index.html b/modules/ROOT/examples/live-demos/full-featured/index.html index a35479e914..00afa78168 100644 --- a/modules/ROOT/examples/live-demos/full-featured/index.html +++ b/modules/ROOT/examples/live-demos/full-featured/index.html @@ -11,11 +11,20 @@
    And visit the pricing

    Got questions or need help?

    +

    Please try out this demo of our Tiny Comments premium plugin with @mentions support.

    +
      +
    1. Highlight the content you want to comment on.
    2. +
    3. Click the Add Comment icon in the toolbar.
    4. +
    5. Type your comment into the text field at the bottom of the Comment sidebar, and use @ followed by a name to mention a collaborator.
    6. +
    7. Click Comment.
    8. +
    +

    Your comment is then attached to the text, exactly like this! You can @Mia Andersson directly in your comments to notify them.

    +

    A simple table to play with

    @@ -40,7 +49,6 @@

    A simple table to play with

    -

    Character strings to demonstrate some of the Advanced Typography plugin’s features

    Select the characters in the list below and choose Tools → Typography → Enhance.

    @@ -70,11 +78,11 @@

    🤖 Try out AI

    Note on the included Templates demonstration

    -

    The included Templates demonstration uses the advtemplate_list configuration option to return a local promise containing a basic Template structure with self-contained examples.

    +

    The included Templates demonstration uses the advtemplate_list configuration option to return a local promise containing a basic Template structure with self-contained examples.

    This example allows for the loading of and interacting with the Template user-interface but cannot be used to create, edit, or save Template items.

    -

    See the Templates documentation for details on how to setup Templates to interact with external data sources.

    +

    See the Templates documentation for details on how to setup Templates to interact with external data sources.

    Found a bug?

    @@ -88,4 +96,4 @@

    Finally…

    All the best from the TinyMCE team.

    - + \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/full-featured/index.js b/modules/ROOT/examples/live-demos/full-featured/index.js index 3a6655ff7f..81797ee419 100644 --- a/modules/ROOT/examples/live-demos/full-featured/index.js +++ b/modules/ROOT/examples/live-demos/full-featured/index.js @@ -1,592 +1,730 @@ const fetchApi = import('https://cdn.skypack.dev/@microsoft/fetch-event-source@2.0.1') .then((module) => module.fetchEventSource); - - -/* Script to import faker.js for generating random data for demonstration purposes */ -tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/faker.min.js']).then(() => { - - /* - ** This is to simulate requesting information from a server. - ** - ** It has 2 functions: - ** fetchUsers() - returns a complete list of users' ids and names. - ** fetchUser(id) - returns the full information about a single user id. - ** - ** Both of these functions have a slight delay to simulate a server request. - */ - const fakeServer = (() => { - /* Some user profile images for our fake server (original source: unsplash) */ - const images = [ - 'Abdullah_Hadley', 'Abriella_Bond', 'Addilynn_Dodge', 'Adolfo_Hess', 'Alejandra_Stallings', 'Alfredo_Schafer', 'Aliah_Pitts', 'Amilia_Luna', 'Andi_Lane', 'Angelina_Winn', 'Arden_Dean', 'Ariyanna_Hicks', 'Asiya_Wolff', 'Brantlee_Adair', 'Carys_Metz', 'Daniela_Dewitt', 'Della_Case', 'Dianna_Smiley', 'Eliana_Stout', 'Elliana_Palacios', 'Fischer_Garland', 'Glen_Rouse', 'Grace_Gross', 'Heath_Atwood', 'Jakoby_Roman', 'Judy_Sewell', 'Kaine_Hudson', 'Kathryn_Mcgee', 'Kayley_Dwyer', 'Korbyn_Colon', 'Lana_Steiner', 'Loren_Spears', 'Lourdes_Browning', 'Makinley_Oneill', 'Mariana_Dickey', 'Miyah_Myles', 'Moira_Baxter', 'Muhammed_Sizemore', 'Natali_Craig', 'Nevaeh_Cates', 'Oscar_Khan', 'Rodrigo_Hawkins', 'Ryu_Duke', 'Tripp_Mckay', 'Vivianna_Kiser', 'Yamilet_Booker', 'Yarely_Barr', 'Zachary_Albright', 'Zahir_Mays', 'Zechariah_Burrell' - ]; - - /* Create an array of 200 random names using faker.js */ - const userNames = []; - for (let i = 0; i < 200; i++) { - userNames.push(faker.name.findName()); - } - - /* This represents a database of users on the server */ - const userDb = {}; - userNames.map((fullName) => { - const id = fullName.toLowerCase().replace(/ /g, ''); - return { - id: id, - name: fullName, - fullName: fullName, - description: faker.name.jobTitle(), - image: '{{imagesdir}}/unsplash/uifaces-unsplash-portrait-' + images[Math.floor(images.length * Math.random())] + '.jpg' +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const user_id = 'james-wilson'; +const collaborator_id = 'mia-andersson'; + +const now = new Date(); +const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); +const anhourago = new Date(now.getTime() - 60 * 60 * 1000).toISOString(); +const fakeDelay = 200; + +const randomString = () => { + return crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14); +}; + +const conversationDb = { + 'mce-conversation_19679600221621399703915': { + uid: 'mce-conversation_19679600221621399703915', + comments: [{ + uid: 'mce-conversation_19679600221621399703915', + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', + content: `What do you think about this? @${collaborator_id}?`, + createdAt: yesterday, + modifiedAt: yesterday + }, { + uid: 'mce-conversation_19679600221621399703917', + author: collaborator_id, + authorName: 'Mia Andersson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', + content: `I think this is a great idea @${user_id}!`, + createdAt: anhourago, + modifiedAt: anhourago, + }] + }, + 'mce-conversation_420304606321716900864126': { + uid: 'mce-conversation_420304606321716900864126', + comments: [{ + uid: 'mce-conversation_420304606321716900864126', + author: collaborator_id, + authorName: 'Mia Andersson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', + content: `@${user_id} Please revise this sentence, exclamation points are unprofessional!`, + createdAt: yesterday, + modifiedAt: anhourago + }] + } +}; + +const mentions_fetch = async (query, success) => { + const searchPhrase = query.term.toLowerCase(); + await fetch(`${API_URL}?q=${encodeURIComponent(searchPhrase)}`) + .then((response) => response.json()) + .then((users) => success(users.data.map((userInfo) => ({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + })))) + .catch((error) => console.log(error)); +}; + +const mentions_menu_complete = (editor, userInfo) => { + const span = editor.getDoc().createElement('span'); + span.className = 'mymention'; + span.setAttribute('data-mention-id', userInfo.id); + span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); + return span; +}; + +const createCard = (userInfo) => { + const div = document.createElement('div'); + div.innerHTML = ( + '
    ' + + '' + + '

    ' + userInfo.name + '

    ' + + '

    ' + userInfo.description + '

    ' + + '
    ' + ); + return div; +}; + +const mentions_select = async (mention, success) => { + const id = mention.getAttribute('data-mention-id'); + await fetch(`${API_URL}/${id}`) + .then((response) => response.json()) + .then((userInfo) => { + const card = createCard({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + }); + success(card); + }) + .catch((error) => console.error(error)); +}; + +const mentions_menu_hover = async (userInfo, success) => { + const card = createCard(userInfo); + success(card); +}; + +const tinycomments_create = (req, done, fail) => { + if (req.content === 'fail') { + fail(new Error('Something has gone wrong...')); + } else { + const uid = 'annotation-' + randomString(); + conversationDb[uid] = { + uid, + comments: [{ + uid, + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', + content: req.content, + createdAt: req.createdAt, + modifiedAt: req.createdAt + }] }; - }).forEach((user) => { - userDb[user.id] = user; - }); - - /* This represents getting the complete list of users from the server with the details required for the mentions "profile" item */ - const fetchUsers = () => new Promise((resolve, _reject) => { - /* simulate a server delay */ - setTimeout(() => { - const users = Object.keys(userDb).map(id => ({ - id: id, - name: userDb[id].name, - image: userDb[id].image, - description: userDb[id].description - })); - resolve(users); - }, 500); - }); - - /* This represents requesting all the details of a single user from the server database */ - const fetchUser = (id) => new Promise((resolve, reject) => { - /* simulate a server delay */ - setTimeout(() => { - if (Object.prototype.hasOwnProperty.call(userDb, id)) { - resolve(userDb[id]); - } - reject(`unknown user id "${id}"`); - }, 300); - }); - - return { - fetchUsers: fetchUsers, - fetchUser: fetchUser - }; - })(); - - /* These are "local" caches of the data returned from the fake server */ - let usersRequest = null; - const userRequest = {}; - - const mentions_fetch = (query, success) => { - /* Fetch your full user list from somewhere */ - if (usersRequest === null) { - usersRequest = fakeServer.fetchUsers(); + setTimeout(() => done({ conversationUid: uid }), fakeDelay); } - usersRequest.then((users) => { - /* `query.term` is the text the user typed after the '@' */ - users = users.filter((user) => user.name.indexOf(query.term.toLowerCase()) !== -1); - users = users.slice(0, 10); - - /* Where the user object must contain the properties `id` and `name` - but you could additionally include anything else you deem useful. */ - success(users); - }); }; - const mentions_menu_hover = (userInfo, success) => { - /* Request more information about the user from the server and cache it locally */ - if (!userRequest[userInfo.id]) { - userRequest[userInfo.id] = fakeServer.fetchUser(userInfo.id); +const tinycomments_reply = (req, done) => { + const replyUid = 'annotation-' + randomString(); + conversationDb[req.conversationUid].comments.push({ + uid: replyUid, + author: user_id, + authorName: 'James Wilson', + authorAvatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', + content: req.content, + createdAt: req.createdAt, + modifiedAt: req.createdAt + }); + setTimeout(() => done({ commentUid: replyUid }), fakeDelay); +}; + +const tinycomments_delete = (req, done) => { + if (user_id === collaborator_id) { // Replace wth your own logic, e.g. check if user created the conversation + delete conversationDb[req.conversationUid]; + setTimeout(() => done({ canDelete: true }), fakeDelay); + } else { + setTimeout(() => done({ canDelete: false, reason: 'Must be admin user' }), fakeDelay); + } +}; + +const tinycomments_resolve = (req, done) => { + const conversation = conversationDb[req.conversationUid]; + if (user_id === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges + delete conversationDb[req.conversationUid]; + setTimeout(() => done({ canResolve: true }), fakeDelay); + } else { + setTimeout(() => done({ canResolve: false, reason: 'Must be conversation author' }), fakeDelay); + } +}; + +const tinycomments_delete_comment = (req, done) => { + const oldcomments = conversationDb[req.conversationUid].comments; + let reason = 'Comment not found'; + + const newcomments = oldcomments.filter((comment) => { + if (comment.uid === req.commentUid) { // Found the comment to delete + if (user_id === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges + return false; // Remove the comment + } else { + reason = 'Not authorised to delete this comment'; // Update reason + } } - userRequest[userInfo.id].then(userDetail => { - const div = document.createElement('div'); - - div.innerHTML = ( - '
    ' + - '' + - '

    ' + userDetail.fullName + '

    ' + - '

    ' + userDetail.description + '

    ' + - '
    ' - ); - - success(div); - }); - }; + return true; // Keep the comment + }); - const mentions_menu_complete = (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - }; + if (newcomments.length === oldcomments.length) { + setTimeout(() => done({ canDelete: false, reason }), fakeDelay); + } else { + conversationDb[req.conversationUid].comments = newcomments; + setTimeout(() => done({ canDelete: true }), fakeDelay); + } +}; + +const tinycomments_edit_comment = (req, done) => { + const oldcomments = conversationDb[req.conversationUid].comments; + let reason = 'Comment not found'; + let canEdit = false; + + const newcomments = oldcomments.map((comment) => { + if (comment.uid === req.commentUid) { // Found the comment to delete + if (user_id === comment.author) { // Replace with your own logic, e.g. check if user has admin privileges + canEdit = true; // User can edit the comment + return { ...comment, content: req.content, modifiedAt: new Date().toISOString() }; // Update the comment + } else { + reason = 'Not authorised to edit this comment'; // Update reason + } + } + return comment; // Keep the comment + }); - const mentions_select = (mention, success) => { - /* `mention` is the element we previously created with `mentions_menu_complete` - in this case we have chosen to store the id as an attribute */ - const id = mention.getAttribute('data-mention-id'); - /* Request more information about the user from the server and cache it locally */ - if (!userRequest[id]) { - userRequest[id] = fakeServer.fetchUser(id); + if (canEdit) { + conversationDb[req.conversationUid].comments = newcomments; + setTimeout(() => done({ canEdit }), fakeDelay); + } else { + setTimeout(() => done({ canEdit, reason }), fakeDelay); + } +}; + +const tinycomments_delete_all = (req, done) => { + const conversation = conversationDb[req.conversationUid]; + if (user_id === conversation.comments[0].author) { // Replace wth your own logic, e.g. check if user has admin priveleges + delete conversationDb[req.conversationUid]; + setTimeout(() => done({ canDelete: true }), fakeDelay); + } else { + setTimeout(() => done({ canDelete: false, reason: 'Must be conversation author' }), fakeDelay); + } +}; + +const tinycomments_lookup = (req, done) => { + setTimeout(() => { + done({ + conversation: { + uid: conversationDb[req.conversationUid].uid, + comments: [...conversationDb[req.conversationUid].comments] + } + }); + }, fakeDelay); +}; + +const tinycomments_fetch = (conversationUids, done) => { + const fetchedConversations = {}; + conversationUids.forEach((uid) => { + const conversation = conversationDb[uid]; + if (conversation) { + fetchedConversations[uid] = {...conversation}; } - userRequest[id].then((userDetail) => { - const div = document.createElement('div'); - div.innerHTML = ( - '
    ' + - '' + - '

    ' + userDetail.fullName + '

    ' + - '

    ' + userDetail.description + '

    ' + - '
    ' - ); - success(div); + }); + setTimeout(() => done({ conversations: fetchedConversations }), fakeDelay); +}; + +const ai_request = (request, respondWith) => { + respondWith.stream((signal, streamMessage) => { + // Adds each previous query and response as individual messages + const conversation = request.thread.flatMap((event) => { + if (event.response) { + return [ + { role: "user", content: event.request.query }, + { role: "assistant", content: event.response.data }, + ]; + } else { + return []; + } }); - }; - const isSmallScreen = window.matchMedia('(max-width: 1023.5px)').matches; - - const ai_request = (request, respondWith) => { - respondWith.stream((signal, streamMessage) => { - // Adds each previous query and response as individual messages - const conversation = request.thread.flatMap((event) => { - if (event.response) { - return [ - { role: "user", content: event.request.query }, - { role: "assistant", content: event.response.data }, - ]; - } else { - return []; - } - }); + // System messages provided by the plugin to format the output as HTML content. + const systemMessages = request.system.map((content) => ({ + role: "system", + content, + })); + + // Forms the new query sent to the API + const content = + request.context.length === 0 || conversation.length > 0 + ? request.query + : `Question: ${request.query} Context: """${request.context}"""`; + + const messages = [ + ...conversation, + ...systemMessages, + { role: "user", content }, + ]; - // System messages provided by the plugin to format the output as HTML content. - const systemMessages = request.system.map((content) => ({ - role: "system", - content, - })); - - // Forms the new query sent to the API - const content = - request.context.length === 0 || conversation.length > 0 - ? request.query - : `Question: ${request.query} Context: """${request.context}"""`; - - const messages = [ - ...conversation, - ...systemMessages, - { role: "user", content }, - ]; - - let hasHead = false; - let markdownHead = ""; - - const hasMarkdown = (message) => { - if (message.includes("`") && markdownHead !== "```") { - const numBackticks = message.split("`").length - 1; - markdownHead += "`".repeat(numBackticks); - if (hasHead && markdownHead === "```") { - markdownHead = ""; - hasHead = false; - } - return true; - } else if (message.includes("html") && markdownHead === "```") { + let hasHead = false; + let markdownHead = ""; + + const hasMarkdown = (message) => { + if (message.includes("`") && markdownHead !== "```") { + const numBackticks = message.split("`").length - 1; + markdownHead += "`".repeat(numBackticks); + if (hasHead && markdownHead === "```") { markdownHead = ""; - hasHead = true; - return true; + hasHead = false; } - return false; - }; + return true; + } else if (message.includes("html") && markdownHead === "```") { + markdownHead = ""; + hasHead = true; + return true; + } + return false; + }; - const requestBody = { - model: "gpt-4o", - temperature: 0.7, - max_tokens: 4000, - messages, - stream: true, - }; + const requestBody = { + model: "gpt-4o", + temperature: 0.7, + max_tokens: 4000, + messages, + stream: true, + }; - const openAiOptions = { - signal, - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer {{ openai_proxy_token }}`, - }, - body: JSON.stringify(requestBody), - }; + const openAiOptions = { + signal, + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer {{ openai_proxy_token }}`, + }, + body: JSON.stringify(requestBody), + }; - const onopen = async (response) => { - if (response) { - const contentType = response.headers.get("content-type"); - if (response.ok && contentType?.includes("text/event-stream")) { - return; - } else if (contentType?.includes("application/json")) { - const data = await response.json(); - if (data.error) { - throw new Error(`${data.error.type}: ${data.error.message}`); - } + const onopen = async (response) => { + if (response) { + const contentType = response.headers.get("content-type"); + if (response.ok && contentType?.includes("text/event-stream")) { + return; + } else if (contentType?.includes("application/json")) { + const data = await response.json(); + if (data.error) { + throw new Error(`${data.error.type}: ${data.error.message}`); } - } else { - throw new Error("Failed to communicate with the ChatGPT API"); } - }; + } else { + throw new Error("Failed to communicate with the ChatGPT API"); + } + }; - // This function passes each new message into the plugin via the `streamMessage` callback. - const onmessage = (ev) => { - const data = ev.data; - if (data !== "[DONE]") { - const parsedData = JSON.parse(data); - const firstChoice = parsedData?.choices[0]; - const message = firstChoice?.delta?.content; - if (message && message !== "") { - if (!hasMarkdown(message)) { - streamMessage(message); - } + // This function passes each new message into the plugin via the `streamMessage` callback. + const onmessage = (ev) => { + const data = ev.data; + if (data !== "[DONE]") { + const parsedData = JSON.parse(data); + const firstChoice = parsedData?.choices[0]; + const message = firstChoice?.delta?.content; + if (message && message !== "") { + if (!hasMarkdown(message)) { + streamMessage(message); } } - }; + } + }; - const onerror = (error) => { - // Stop operation and do not retry by the fetch-event-source - throw error; - }; + const onerror = (error) => { + // Stop operation and do not retry by the fetch-event-source + throw error; + }; - // Use microsoft's fetch-event-source library to work around the 2000 character limit - // of the browser `EventSource` API, which requires query strings - return fetchApi - .then((fetchEventSource) => - fetchEventSource("{{ openai_proxy_url }}", { - ...openAiOptions, - openWhenHidden: true, - onopen, - onmessage, - onerror, - }) - ) - .then(async (response) => { - if (response && !response.ok) { - const data = await response.json(); - if (data.error) { - throw new Error(`${data.error.type}: ${data.error.message}`); - } - } + // Use microsoft's fetch-event-source library to work around the 2000 character limit + // of the browser `EventSource` API, which requires query strings + return fetchApi + .then((fetchEventSource) => + fetchEventSource("{{ openai_proxy_url }}", { + ...openAiOptions, + openWhenHidden: true, + onopen, + onmessage, + onerror, }) - .catch(onerror); - }); - }; - - // Fetch revisions - const fetchRevisions = () => { - return Promise.resolve([ - { - revisionId: '3', - createdAt: '2023-11-24T22:26:21.578Z', - author: { - id: 'husky', - name: 'A Tiny Husky', - avatar: '{{imagesdir}}/tiny-husky.jpg' - }, - content: ` -

    TinyMCE Logo

    -

    Welcome to the TinyMCE editor demo!

    -

    A simple table to play with

    - - - - - - - - - - - - - - - - - - - - -
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    -

    Found a bug?

    -

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    -

    Finally ...

    -

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    -

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    - `, - }, - { - revisionId: '2', - createdAt: '2023-11-25T08:30:21.578Z', - author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png' - }, - content: ` -

    TinyMCE Logo

    -

    Welcome to the TinyMCE editor demo!

    -

    Got questions or need help?

    -
      -
    1. Our documentation is a great resource for learning how to configure TinyMCE.
    2. -
    3. Have a specific question? Try the tinymce tag at Stack Overflow.
    4. -
    5. We also offer enterprise grade support as part of TinyMCE premium plans.
    6. -
    -

    A simple table to play with

    - - - - - - - - - - - - - - - - - - - - -
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    -

    Found a bug?

    -

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    -

    Finally ...

    -

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    -

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    - `, - }, - { - revisionId: '1', - createdAt: '2023-11-29T10:11:21.578Z', - author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png' - }, - content: ` -

    TinyMCE Logo

    -

    Welcome to the TinyMCE editor demo!

    -

    Got questions or need help?

    - -

    A simple table to play with

    - - - - - - - - - - - - - - - - - - - - -
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    -

    Found a bug?

    -

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    -

    Finally ...

    -

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    -

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    - `, - }, - ]); - }; - - tinymce.init({ - selector: 'textarea#full-featured', - plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker editimage help formatpainter permanentpen pageembed charmap tinycomments mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate markdown revisionhistory importword exportword exportpdf', - editimage_cors_hosts: ['picsum.photos'], - tinydrive_token_provider: (success, failure) => { - success({ token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ks_BdfH4CWilyzLNk8S2gDARFhuxIauLa8PwhdEQhEo' }); + ) + .then(async (response) => { + if (response && !response.ok) { + const data = await response.json(); + if (data.error) { + throw new Error(`${data.error.type}: ${data.error.message}`); + } + } + }) + .catch(onerror); + }); +}; + +const revisions = [ + { + revisionId: '3', + createdAt: '2023-11-24T22:26:21.578Z', + author: { + id: 'james-wilson', + name: 'James Wilson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', }, - tinydrive_demo_files_url: '{{imagesdir}}/tiny-drive-demo/demo_files.json', - tinydrive_dropbox_app_key: 'jee1s9eykoh752j', - tinydrive_google_drive_key: 'AIzaSyAsVRuCBc-BLQ1xNKtnLHB3AeoK-xmOrTc', - tinydrive_google_drive_client_id: '748627179519-p9vv3va1mppc66fikai92b3ru73mpukf.apps.googleusercontent.com', - mobile: { - plugins: 'ai preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars linkchecker emoticons advtable footnotes mergetags autocorrect typography advtemplate', + content: ` +

    TinyMCE Logo

    +

    Welcome to the TinyMCE editor demo!

    +

    A simple table to play with

    + + + + + + + + + + + + + + + + + + + + +
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    +

    Found a bug?

    +

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    +

    Finally ...

    +

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    +

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    + `, + }, + { + revisionId: '2', + createdAt: '2023-11-25T08:30:21.578Z', + author: { + id: 'mia.andersson', + name: 'Mia Andersson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', }, - menu: { - tc: { - title: 'Comments', - items: 'addcomment showcomments deleteallconversations' - } + content: ` +

    TinyMCE Logo

    +

    Welcome to the TinyMCE editor demo!

    +

    Got questions or need help?

    +
      +
    1. Our documentation is a great resource for learning how to configure TinyMCE.
    2. +
    3. Have a specific question? Try the tinymce tag at Stack Overflow.
    4. +
    5. We also offer enterprise grade support as part of TinyMCE premium plans.
    6. +
    +

    A simple table to play with

    + + + + + + + + + + + + + + + + + + + + +
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    +

    Found a bug?

    +

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    +

    Finally ...

    +

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    +

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    + `, + }, + { + revisionId: '1', + createdAt: '2023-11-29T10:11:21.578Z', + author: { + id: 'mia.andersson', + name: 'Mia Andersson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', }, - menubar: 'file edit view insert format tools table tc help', - toolbar: "undo redo | importword exportword exportpdf | revisionhistory | aidialog aishortcuts | blocks fontsizeinput | bold italic | align numlist bullist | link image | table math media pageembed | lineheight outdent indent | strikethrough forecolor backcolor formatpainter removeformat | charmap emoticons checklist | code fullscreen preview | save print | pagebreak anchor codesample footnotes mergetags | addtemplate inserttemplate | addcomment showcomments | ltr rtl casechange | spellcheckdialog a11ycheck", // Note: if a toolbar item requires a plugin, the item will not present in the toolbar if the plugin is not also loaded. - autosave_ask_before_unload: true, - autosave_interval: '30s', - autosave_prefix: '{path}{query}-{id}-', - autosave_restore_when_empty: false, - autosave_retention: '2m', - image_advtab: true, - typography_default_lang: 'en-US', - typography_langs: [ - 'en-US', - 'es', - 'de' - ], - typography_rules: [ - 'common/punctuation/quote', - 'en-US/dash/main', - 'common/nbsp/afterParagraphMark', - 'common/nbsp/afterSectionMark', - 'common/nbsp/afterShortWord', - 'common/nbsp/beforeShortLastNumber', - 'common/nbsp/beforeShortLastWord', - 'common/nbsp/dpi', - 'common/punctuation/apostrophe', - 'common/space/delBeforePunctuation', - 'common/space/afterComma', - 'common/space/afterColon', - 'common/space/afterExclamationMark', - 'common/space/afterQuestionMark', - 'common/space/afterSemicolon', - 'common/space/beforeBracket', - 'common/space/bracket', - 'common/space/delBeforeDot', - 'common/space/squareBracket', - 'common/number/mathSigns', - 'common/number/times', - 'common/number/fraction', - 'common/symbols/arrow', - 'common/symbols/cf', - 'common/symbols/copy', - 'common/punctuation/delDoublePunctuation', - 'common/punctuation/hellip' - ], - typography_ignore: [ 'code' ], - advtemplate_templates: [ - { - id: '1', - title: 'Resolving tickets', - content: '

    As we have not heard back from you in over a week, we have gone ahead and resolved your ticket.

    ' - }, - { - id: '2', - title: 'Quick replies', - items: [ - { - id: '3', - title: 'Message received', - content: '

    Just a quick note to say we have received your message, and will get back to you within 48 hours.

    ' - }, - { - id: '4', - title: 'Progress update', - content: '

    Just a quick note to let you know we are still working on your case

    ' - } - ] - } - ], - link_list: [ - { title: 'My page 1', value: 'https://www.tiny.cloud' }, - { title: 'My page 2', value: 'http://www.moxiecode.com' } - ], - image_list: [ - { title: 'My page 1', value: 'https://www.tiny.cloud' }, - { title: 'My page 2', value: 'http://www.moxiecode.com' } - ], - image_class_list: [ - { title: 'None', value: '' }, - { title: 'Some class', value: 'class-name' } - ], - importcss_append: true, - height: 600, - image_caption: true, - quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', - noneditable_class: 'mceNonEditable', - toolbar_mode: 'sliding', - spellchecker_ignore_list: ['Ephox', 'Moxiecode', 'tinymce', 'TinyMCE'], - tinycomments_mode: 'embedded', - content_style: '.mymention{ color: gray; }' + - 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', - contextmenu: 'link image editimage table spellchecker configurepermanentpen', - a11y_advanced_options: true, - mentions_selector: '.mymention', - mentions_fetch: mentions_fetch, - mentions_menu_hover: mentions_menu_hover, - mentions_menu_complete: mentions_menu_complete, - mentions_select: mentions_select, - mentions_item_type: 'profile', - autocorrect_capitalize: true, - mergetags_list: [ - { - title: 'Client', - menu: [ + content: ` +

    TinyMCE Logo

    +

    Welcome to the TinyMCE editor demo!

    +

    Got questions or need help?

    + +

    A simple table to play with

    + + + + + + + + + + + + + + + + + + + + +
    ProductCostReally?
    TinyMCEFreeYES!
    PluploadFreeYES!
    +

    Found a bug?

    +

    If you think you have found a bug please create an issue on the GitHub repo to report it to the developers.

    +

    Finally ...

    +

    Don't forget to check out our other product Plupload, your ultimate upload solution featuring HTML5 upload support.

    +

    Thanks for supporting TinyMCE! We hope it helps you and your users create great content.
    All the best from the TinyMCE team.

    + `, + } +]; + +const revisionhistory_fetch = () => new Promise((resolve) => { + setTimeout(() => { + const sortedRevisions = revisions + .sort((a, b) => new Date(a.createdAt) < new Date(b.createdAt) ? 1 : -1); + resolve(sortedRevisions); + }, fakeDelay); +}); + +const revisionhistory_fetch_revision = (_editor, revision) => new Promise((resolve, reject) => { + setTimeout(() => { + const revision = revisions.find((r) => r.revisionId === revision.revisionId); + if (revision) { + resolve(revision); + } else { + reject(`Revision ${revision.revisionId} is not found`); + } + }, fakeDelay); +}); + +tinymce.init({ + selector: 'textarea#full-featured', + plugins: [ + 'ai', 'suggestededits', 'preview', 'powerpaste', 'casechange', 'importcss', 'tinydrive', 'searchreplace', + 'autolink', 'autosave', 'save', 'directionality', 'advcode', 'visualblocks', 'visualchars', 'fullscreen', + 'image', 'link', 'math', 'media', 'mediaembed', 'codesample', 'table', 'charmap', 'pagebreak', 'nonbreaking', + 'anchor', 'tableofcontents', 'insertdatetime', 'advlist', 'lists', 'checklist', 'wordcount', 'tinymcespellchecker', + 'a11ychecker', 'editimage', 'help', 'formatpainter', 'permanentpen', 'pageembed', 'charmap', 'tinycomments', 'mentions', + 'quickbars', 'emoticons', 'advtable', 'footnotes', 'mergetags', 'autocorrect', 'typography', 'advtemplate', 'markdown', + 'revisionhistory', 'importword', 'exportword', 'exportpdf' + ], + menu: { + tc: { + title: 'Comments', + items: 'addcomment showcomments deleteallconversations' + } + }, + menubar: 'file edit view insert format tools table tc help', + // Note: if a toolbar item requires a plugin, the item will not present in the toolbar if the plugin is not also loaded. + toolbar: "undo redo | importword exportword exportpdf | suggestededits | revisionhistory | aidialog aishortcuts | blocks fontsizeinput | bold italic | align numlist bullist | link image | table math media pageembed | lineheight outdent indent | strikethrough forecolor backcolor formatpainter removeformat | charmap emoticons checklist | code fullscreen preview | save print | pagebreak anchor codesample footnotes mergetags | addtemplate inserttemplate | addcomment showcomments | ltr rtl casechange | spellcheckdialog a11ycheck", + mobile: { + plugins: 'ai suggestededits preview powerpaste casechange importcss tinydrive searchreplace autolink autosave save directionality advcode visualblocks visualchars fullscreen image link math media mediaembed codesample table charmap pagebreak nonbreaking anchor tableofcontents insertdatetime advlist lists checklist wordcount tinymcespellchecker a11ychecker help formatpainter pageembed charmap mentions quickbars emoticons advtable footnotes mergetags autocorrect typography advtemplate', + }, + + editimage_cors_hosts: ['picsum.photos'], + tinydrive_token_provider: (success, failure) => { + success({ token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huZG9lIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Ks_BdfH4CWilyzLNk8S2gDARFhuxIauLa8PwhdEQhEo' }); + }, + tinydrive_demo_files_url: '{{imagesdir}}/tiny-drive-demo/demo_files.json', + tinydrive_dropbox_app_key: 'jee1s9eykoh752j', + tinydrive_google_drive_key: 'AIzaSyAsVRuCBc-BLQ1xNKtnLHB3AeoK-xmOrTc', + tinydrive_google_drive_client_id: '748627179519-p9vv3va1mppc66fikai92b3ru73mpukf.apps.googleusercontent.com', + + autosave_ask_before_unload: true, + autosave_interval: '30s', + autosave_prefix: '{path}{query}-{id}-', + autosave_restore_when_empty: false, + autosave_retention: '2m', + image_advtab: true, + typography_default_lang: 'en-US', + typography_langs: [ + 'en-US', + 'es', + 'de' + ], + typography_rules: [ + 'common/punctuation/quote', + 'en-US/dash/main', + 'common/nbsp/afterParagraphMark', + 'common/nbsp/afterSectionMark', + 'common/nbsp/afterShortWord', + 'common/nbsp/beforeShortLastNumber', + 'common/nbsp/beforeShortLastWord', + 'common/nbsp/dpi', + 'common/punctuation/apostrophe', + 'common/space/delBeforePunctuation', + 'common/space/afterComma', + 'common/space/afterColon', + 'common/space/afterExclamationMark', + 'common/space/afterQuestionMark', + 'common/space/afterSemicolon', + 'common/space/beforeBracket', + 'common/space/bracket', + 'common/space/delBeforeDot', + 'common/space/squareBracket', + 'common/number/mathSigns', + 'common/number/times', + 'common/number/fraction', + 'common/symbols/arrow', + 'common/symbols/cf', + 'common/symbols/copy', + 'common/punctuation/delDoublePunctuation', + 'common/punctuation/hellip' + ], + typography_ignore: [ 'code' ], + advtemplate_templates: [ + { + id: '1', + title: 'Resolving tickets', + content: '

    As we have not heard back from you in over a week, we have gone ahead and resolved your ticket.

    ' + }, + { + id: '2', + title: 'Quick replies', + items: [ { - value: 'Client.LastCallDate', - title: 'Call date' + id: '3', + title: 'Message received', + content: '

    Just a quick note to say we have received your message, and will get back to you within 48 hours.

    ' }, { - value: 'Client.Name', - title: 'Client name' - } - ] - }, - { - title: 'Proposal', - menu: [ - { - value: 'Proposal.SubmissionDate', - title: 'Submission date' + id: '4', + title: 'Progress update', + content: '

    Just a quick note to let you know we are still working on your case

    ' } ] - }, - { - value: 'Consultant', - title: 'Consultant' - }, - { - value: 'Salutation', - title: 'Salutation' - } - ], - ai_request, - exportpdf_converter_options: { - 'format': 'Letter', - 'margin_top': '1in', - 'margin_right': '1in', - 'margin_bottom': '1in', - 'margin_left': '1in' - }, - exportword_converter_options: { - 'document': { - 'size': 'Letter' - } + } + ], + link_list: [ + { title: 'My page 1', value: 'https://www.tiny.cloud' }, + { title: 'My page 2', value: 'http://www.moxiecode.com' } + ], + image_list: [ + { title: 'My page 1', value: 'https://www.tiny.cloud' }, + { title: 'My page 2', value: 'http://www.moxiecode.com' } + ], + image_class_list: [ + { title: 'None', value: '' }, + { title: 'Some class', value: 'class-name' } + ], + importcss_append: true, + height: 600, + image_caption: true, + quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', + noneditable_class: 'mceNonEditable', + toolbar_mode: 'sliding', + spellchecker_ignore_list: ['Ephox', 'Moxiecode', 'tinymce', 'TinyMCE'], + content_style: '.mymention{ color: gray; }' + + 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', + contextmenu: 'link image editimage table spellchecker configurepermanentpen', + a11y_advanced_options: true, + + ai_request, + + tinycomments_mode: 'callback', + tinycomments_mentions_enabled: true, + sidebar_show: 'showcomments', + tinycomments_create, + tinycomments_reply, + tinycomments_delete, + tinycomments_resolve, + tinycomments_delete_all, + tinycomments_lookup, + tinycomments_delete_comment, + tinycomments_edit_comment, + tinycomments_fetch, + + mentions_item_type: 'profile', + mentions_min_chars: 0, + mentions_selector: '.mymention', + mentions_fetch, + mentions_menu_hover, + mentions_menu_complete, + mentions_select, + + autocorrect_capitalize: true, + mergetags_list: [ + { + title: 'Client', + menu: [ + { + value: 'Client.LastCallDate', + title: 'Call date' + }, + { + value: 'Client.Name', + title: 'Client name' + } + ] }, - importword_converter_options: { - 'formatting': { - 'styles': 'inline', - 'resets': 'inline', - 'defaults': 'inline', - } + { + title: 'Proposal', + menu: [ + { + value: 'Proposal.SubmissionDate', + title: 'Submission date' + } + ] }, - revisionhistory_fetch: fetchRevisions, - revisionhistory_author: { - id: 'john.doe', - name: 'John Doe' + { + value: 'Consultant', + title: 'Consultant' }, - revisionhistory_display_author: true, - }); -}); + { + value: 'Salutation', + title: 'Salutation' + } + ], + exportpdf_converter_options: { + 'format': 'Letter', + 'margin_top': '1in', + 'margin_right': '1in', + 'margin_bottom': '1in', + 'margin_left': '1in' + }, + exportword_converter_options: { + 'document': { + 'size': 'Letter' + } + }, + importword_converter_options: { + 'formatting': { + 'styles': 'inline', + 'resets': 'inline', + 'defaults': 'inline', + } + }, + revisionhistory_fetch, + revisionhistory_fetch_revision, + revisionhistory_display_author: true, + suggestededits_content: 'html', + suggestededits_access: 'full', + tinycomments_author: user_id, + tinycomments_author_name: 'James Wilson', + tinycomments_author_avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg' +}); \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/full-featured/style.css b/modules/ROOT/examples/live-demos/full-featured/style.css index 0bdb3ae26d..5e88248737 100644 --- a/modules/ROOT/examples/live-demos/full-featured/style.css +++ b/modules/ROOT/examples/live-demos/full-featured/style.css @@ -1,6 +1,5 @@ - -textarea#mentions { - height: 350px; +span.mymention { + color: gray !important; } div.card, diff --git a/modules/ROOT/examples/live-demos/mentions/index.html b/modules/ROOT/examples/live-demos/mentions/index.html index 69895e4e2c..e3a5de27cb 100644 --- a/modules/ROOT/examples/live-demos/mentions/index.html +++ b/modules/ROOT/examples/live-demos/mentions/index.html @@ -1,7 +1,46 @@ +

    And visit the pricing page to learn more about our Premium plugins.

    + +

    A simple table to play with

    + + + + + + + + + + + + + + + + + + + + + +
    ProductCostReally?
    TinyMCE CloudGet started for freeYes!
    PluploadFreeYes!
    + +

    Found a bug?

    + +

    If you believe you have found a bug please create an issue on the GitHub repo to report it to the developers.

    + +

    Finally…

    + +

    Don't forget to check out Plupload, the upload solution featuring HTML5 upload support.

    +

    Thanks for supporting TinyMCE. We hope it helps you and your users create great content.

    +

    All the best from the TinyMCE team.

    + + \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/mentions/index.js b/modules/ROOT/examples/live-demos/mentions/index.js index 57d75acbe3..02efd6779d 100644 --- a/modules/ROOT/examples/live-demos/mentions/index.js +++ b/modules/ROOT/examples/live-demos/mentions/index.js @@ -1,155 +1,73 @@ -/* Script to import faker.js for generating random data for demonstration purposes */ -tinymce.ScriptLoader.loadScripts(['https://cdn.jsdelivr.net/npm/faker@5/dist/faker.min.js']).then(() => { - - /* - ** This is to simulate requesting information from a server. - ** - ** It has 2 functions: - ** fetchUsers() - returns a complete list of users' ids and names. - ** fetchUser(id) - returns the full information about a single user id. - ** - ** Both of these functions have a slight delay to simulate a server request. - */ - const fakeServer = (() => { - /* Some user profile images for our fake server (original source: unsplash) */ - const images = [ - 'Abdullah_Hadley', 'Abriella_Bond', 'Addilynn_Dodge', 'Adolfo_Hess', 'Alejandra_Stallings', 'Alfredo_Schafer', 'Aliah_Pitts', 'Amilia_Luna', 'Andi_Lane', 'Angelina_Winn', 'Arden_Dean', 'Ariyanna_Hicks', 'Asiya_Wolff', 'Brantlee_Adair', 'Carys_Metz', 'Daniela_Dewitt', 'Della_Case', 'Dianna_Smiley', 'Eliana_Stout', 'Elliana_Palacios', 'Fischer_Garland', 'Glen_Rouse', 'Grace_Gross', 'Heath_Atwood', 'Jakoby_Roman', 'Judy_Sewell', 'Kaine_Hudson', 'Kathryn_Mcgee', 'Kayley_Dwyer', 'Korbyn_Colon', 'Lana_Steiner', 'Loren_Spears', 'Lourdes_Browning', 'Makinley_Oneill', 'Mariana_Dickey', 'Miyah_Myles', 'Moira_Baxter', 'Muhammed_Sizemore', 'Natali_Craig', 'Nevaeh_Cates', 'Oscar_Khan', 'Rodrigo_Hawkins', 'Ryu_Duke', 'Tripp_Mckay', 'Vivianna_Kiser', 'Yamilet_Booker', 'Yarely_Barr', 'Zachary_Albright', 'Zahir_Mays', 'Zechariah_Burrell' - ]; - - /* Create an array of 200 random names using faker.js */ - const userNames = []; - for (let i = 0; i < 200; i++) { - userNames.push(faker.name.findName()); - } - userNames.sort((a, b) => a.localeCompare(b)); - - /* This represents a database of users on the server */ - const userDb = {}; - userNames.map((fullName) => { - const id = fullName.toLowerCase().replace(/ /g, ''); - return { - id: id, - name: fullName, - fullName: fullName, - description: faker.name.jobTitle(), - image: '{{imagesdir}}/unsplash/uifaces-unsplash-portrait-' + images[Math.floor(images.length * Math.random())] + '.jpg' - }; - }).forEach((user) => { - userDb[user.id] = user; - }); - - /* This represents getting the complete list of users from the server with the details required for the mentions "profile" item */ - const fetchUsers = () => new Promise((resolve, _reject) => { - /* simulate a server delay */ - setTimeout(() => { - const users = Object.keys(userDb).map((id) => ({ - id: id, - name: userDb[id].name, - image: userDb[id].image, - description: userDb[id].description - })); - resolve(users); - }, 500); - }); - - /* This represents requesting all the details of a single user from the server database */ - const fetchUser = (id) => new Promise((resolve, reject) => { - /* simulate a server delay */ - setTimeout(() => { - if (Object.prototype.hasOwnProperty.call(userDb, id)) { - resolve(userDb[id]); - } - reject('unknown user id "' + id + '"'); - }, 300); - }); - - return { - fetchUsers: fetchUsers, - fetchUser: fetchUser - }; - })(); - - /* These are "local" caches of the data returned from the fake server */ - let usersRequest = null; - const userRequest = {}; - - const mentions_fetch = (query, success) => { - /* Fetch your full user list from somewhere */ - if (usersRequest === null) { - usersRequest = fakeServer.fetchUsers(); - } - usersRequest.then((users) => { - /* `query.term` is the text the user typed after the '@' */ - users = users.filter((user) => user.name.toLowerCase().includes(query.term.toLowerCase())) - - users = users.slice(0, 10); - - /* Where the user object must contain the properties `id` and `name` - but you could additionally include anything else you deem useful. */ - success(users); - }); - }; - - const mentions_menu_hover = (userInfo, success) => { - /* Request more information about the user from the server and cache it locally */ - if (!userRequest[userInfo.id]) { - userRequest[userInfo.id] = fakeServer.fetchUser(userInfo.id); - } - userRequest[userInfo.id].then((userDetail) => { - const div = document.createElement('div'); - - div.innerHTML = ( - '
    ' + - '' + - '

    ' + userDetail.fullName + '

    ' + - '

    ' + userDetail.description + '

    ' + - '
    ' - ); - - success(div); - }); - }; - - const mentions_menu_complete = (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - }; - - const mentions_select = (mention, success) => { - /* `mention` is the element we previously created with `mentions_menu_complete` - in this case we have chosen to store the id as an attribute */ - const id = mention.getAttribute('data-mention-id'); - /* Request more information about the user from the server and cache it locally */ - if (!userRequest[id]) { - userRequest[id] = fakeServer.fetchUser(id); - } - userRequest[id].then((userDetail) => { - const div = document.createElement('div'); - div.innerHTML = ( - '
    ' + - '' + - '

    ' + userDetail.fullName + '

    ' + - '

    ' + userDetail.description + '

    ' + - '
    ' - ); - success(div); - }); - }; - - tinymce.init({ - selector: 'textarea#mentions', - plugins: 'mentions', - content_style: '.mymention{ color: gray; }' + - 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', - +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const mentions_fetch = async (query, success) => { + const searchPhrase = query.term.toLowerCase(); + await fetch(`${API_URL}?q=${encodeURIComponent(searchPhrase)}`) + .then((response) => response.json()) + .then((users) => success(users.data.map((userInfo) => ({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + })))) + .catch((error) => console.log(error)); +}; + +const mentions_menu_complete = (editor, userInfo) => { + const span = editor.getDoc().createElement('span'); + span.className = 'mymention'; + span.setAttribute('data-mention-id', userInfo.id); + span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); + return span; +}; + +const createCard = (userInfo) => { + const div = document.createElement('div'); + div.innerHTML = ( + '
    ' + + '' + + '

    ' + userInfo.name + '

    ' + + '

    ' + userInfo.description + '

    ' + + '
    ' + ); + return div; +}; + +const mentions_select = async (mention, success) => { + const id = mention.getAttribute('data-mention-id'); + await fetch(`${API_URL}/${id}`) + .then((response) => response.json()) + .then((userInfo) => { + const card = createCard({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + }); + success(card); + }) + .catch((error) => console.error(error)); +}; + +const mentions_menu_hover = async (userInfo, success) => { + const card = createCard(userInfo); + success(card); +}; + +tinymce.init({ + selector: "textarea", + plugins: [ + "advlist", "anchor", "autolink", "charmap", "code", "fullscreen", + "help", "image", "insertdatetime", "link", "lists", "media", + "preview", "searchreplace", "table", "visualblocks", "mentions" + ], + toolbar: "undo redo | styles | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image", + content_style: '.mymention{ color: gray; }', + mentions_fetch, + mentions_item_type: 'profile', + mentions_menu_complete, mentions_selector: '.mymention', - mentions_fetch: mentions_fetch, - mentions_menu_hover: mentions_menu_hover, - mentions_menu_complete: mentions_menu_complete, - mentions_select: mentions_select, - mentions_item_type: 'profile' - }); -}); + mentions_select, + mentions_menu_hover, + tinycomments_mode: 'embedded' +}); \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/mentions/style.css b/modules/ROOT/examples/live-demos/mentions/style.css index ee9d933713..522cb564e9 100644 --- a/modules/ROOT/examples/live-demos/mentions/style.css +++ b/modules/ROOT/examples/live-demos/mentions/style.css @@ -2,6 +2,10 @@ textarea#mentions { height: 350px; } +.mymention { + color: gray !important; +} + div.card, .tox div.card { width: 240px; @@ -42,4 +46,4 @@ div.card img.avatar, height: 48px; margin-right: 8px; float: left; -} +} \ No newline at end of file diff --git a/modules/ROOT/examples/live-demos/revisionhistory/index.js b/modules/ROOT/examples/live-demos/revisionhistory/index.js index 946faa0854..28f7d32d05 100644 --- a/modules/ROOT/examples/live-demos/revisionhistory/index.js +++ b/modules/ROOT/examples/live-demos/revisionhistory/index.js @@ -1,60 +1,14 @@ -const getRandomDelay = () => { - const minDelay = 500; - const maxDelay = 2000; - return Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; -}; - -const lightRevisions = [ - { - revisionId: '3', - author: { - id: 'tiny.husky', - name: 'A Tiny Husky', - avatar: '{{imagesdir}}/tiny-husky.jpg', - }, - createdAt: '2023-11-25T08:30:21.578Z' - }, - { - revisionId: '2', - author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png', - }, - createdAt: '2023-11-24T22:26:21.578Z', - }, - { - revisionId: '1', - author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png', - }, - createdAt: '2023-11-23T20:26:21.578Z', - }, -]; - -const revisionhistory_fetch = () => - new Promise((resolve) => { - setTimeout(() => { - resolve( - lightRevisions - .sort((a, b) => - new Date(a.createdAt) < new Date(b.createdAt) ? -1 : 1 - ) - .reverse() - ); - }, getRandomDelay()); - }); +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; +const fakeDelay = 200; const revisions = [ { revisionId: '3', createdAt: '2023-11-24T22:26:21.578Z', author: { - id: 'tiny.husky', - name: 'A Tiny Husky', - avatar: '{{imagesdir}}/tiny-husky.jpg', + id: 'james-wilson', + name: 'James Wilson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', }, content: `

    TinyMCE Logo

    @@ -92,9 +46,9 @@ const revisions = [ revisionId: '2', createdAt: '2023-11-25T08:30:21.578Z', author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png', + id: 'mia.andersson', + name: 'Mia Andersson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', }, content: `

    TinyMCE Logo

    @@ -138,9 +92,9 @@ const revisions = [ revisionId: '1', createdAt: '2023-11-29T10:11:21.578Z', author: { - id: 'tiny.user', - name: 'A Tiny User', - avatar: '{{imagesdir}}/logos/android-chrome-192x192.png', + id: 'mia.andersson', + name: 'Mia Andersson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_mia-andersson_128_e6f9424b.jpg', }, content: `

    TinyMCE Logo

    @@ -182,29 +136,37 @@ const revisions = [ } ]; -const revisionhistory_fetch_revision = (_editor, revision) => - new Promise((resolve, reject) => { - setTimeout(() => { - const newRevision = revisions.find((r) => r.revisionId === revision.revisionId); - if (newRevision === undefined) { - reject(`Revision ${revision.revisionId} is not found`); - } else { - resolve(newRevision); - } - }, getRandomDelay()); - }); +const revisionhistory_fetch = () => new Promise((resolve) => { + setTimeout(() => { + const sortedRevisions = revisions + .sort((a, b) => new Date(a.createdAt) < new Date(b.createdAt) ? 1 : -1); + resolve(sortedRevisions); + }, fakeDelay); +}); + +const revisionhistory_fetch_revision = (_editor, revision) => new Promise((resolve, reject) => { + setTimeout(() => { + const revision = revisions.find((r) => r.revisionId === revision.revisionId); + if (revision) { + resolve(revision); + } else { + reject(`Revision ${revision.revisionId} is not found`); + } + }, fakeDelay); +}); tinymce.init({ selector: 'textarea#revisionhistory', height: 800, - plugins: 'revisionhistory', - toolbar: 'revisionhistory', + plugins: 'revisionhistory help code link lists image', + toolbar: 'undo redo | styles | bold italic underline | revisionhistory | link image | code', content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', revisionhistory_fetch, revisionhistory_fetch_revision, + revisionhistory_display_author: true, revisionhistory_author: { - id: 'john.doe', - name: 'John Doe' + id: 'james-wilson', + name: 'James Wilson', + avatar: 'https://sneak-preview.tiny.cloud/demouserdirectory/images/employee_james-wilson_128_52f19412.jpg', }, - revisionhistory_display_author: true -}); +}); \ No newline at end of file diff --git a/modules/ROOT/partials/configuration/mentions_fetch.adoc b/modules/ROOT/partials/configuration/mentions_fetch.adoc index 4836dc0aef..69acbe312f 100644 --- a/modules/ROOT/partials/configuration/mentions_fetch.adoc +++ b/modules/ROOT/partials/configuration/mentions_fetch.adoc @@ -15,29 +15,25 @@ For information on the user properties to pass to the success callback for the a [source,js] ---- -let usersRequest = null; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const mentions_fetch = async (query, success) => { + const searchPhrase = query.term.toLowerCase(); + await fetch(`${API_URL}?q=${encodeURIComponent(searchPhrase)}`) + .then((response) => response.json()) + .then((users) => success(users.data.map((userInfo) => ({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + })))) + .catch((error) => console.log(error)); +}; tinymce.init({ selector: 'textarea', plugins: 'mentions', - mentions_fetch: (query, success) => { - // Fetch your full user list from the server and cache locally - if (usersRequest === null) { - usersRequest = fetch('/users'); - } - usersRequest.then((users) => { - // `query.term` is the text the user typed after the '@' - users = users.filter((user) => { - return user.name.toLowerCase().includes(query.term.toLowerCase()); - }); - - users = users.slice(0, 10); - - // Where the user object must contain the properties `id` and `name` - // but you could additionally include anything else you deem useful. - success(users); - }); - } + mentions_fetch }); ---- @@ -50,31 +46,34 @@ The `+success+` callback can be passed an optional array of extra items. When cl [source,js] ---- +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const mentions_fetch = async (query, success) => { + const searchPhrase = query.term.toLowerCase(); + await fetch(`${API_URL}?q=${encodeURIComponent(searchPhrase)}`) + .then((response) => response.json()) + .then((users) => { + const mentions = users.data.map((userInfo) => ({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + ...(query.meta && query.meta.email) + ? { email: userInfo.custom.email } + : {} + })); + const extras = [{ + text: 'Include email...', + meta: { email: true } + }]; + success(mentions, extras); + }) + .catch((error) => console.log(error)); +}; + tinymce.init({ selector: 'textarea', plugins: 'mentions', - mentions_fetch: (query, success) => { - // query.term is the text the user typed after the '@' - let url = '/users?query=' + query.term; - const isFullTextSearch = query.meta && query.meta.fullTextSearch; - if (isFullTextSearch) { - url += '&full=true' - } - - // Extras are shown at the end of the list and will reload the menu - // by passing the meta to the fetch function - const extras = isFullTextSearch ? [ ] : [ - { - text: 'Full user search...', - meta: { fullTextSearch: true } - } - ]; - - fetch(url).then((users) => { - // Where the user object must contain the properties `id` and `name` - // but you could additionally include anything else you deem useful. - success(users, extras); - }); - } + mentions_fetch }); ----- +---- \ No newline at end of file diff --git a/modules/ROOT/partials/configuration/mentions_menu_complete.adoc b/modules/ROOT/partials/configuration/mentions_menu_complete.adoc index 5bafd6d0d4..979bc2131d 100644 --- a/modules/ROOT/partials/configuration/mentions_menu_complete.adoc +++ b/modules/ROOT/partials/configuration/mentions_menu_complete.adoc @@ -9,17 +9,20 @@ This option overrides the default logic for inserting the mention into the edito [source,js] ---- +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const mentions_menu_complete = (editor, userInfo) => { + const span = editor.getDoc().createElement('span'); + span.className = 'mymention'; + span.setAttribute('data-mention-id', userInfo.id); + span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); + return span; +}; + tinymce.init({ selector: 'textarea', plugins: 'mentions', mentions_selector: 'span.mymention', - mentions_menu_complete: (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - // store the user id in the mention so it can be identified later - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - } + mentions_menu_complete }); ----- +---- \ No newline at end of file diff --git a/modules/ROOT/partials/configuration/mentions_menu_hover.adoc b/modules/ROOT/partials/configuration/mentions_menu_hover.adoc index fc50ef57af..35623631df 100644 --- a/modules/ROOT/partials/configuration/mentions_menu_hover.adoc +++ b/modules/ROOT/partials/configuration/mentions_menu_hover.adoc @@ -9,31 +9,30 @@ This option enables you to provide an element to present next to the menu item b [source,js] ---- -const userRequest = {}; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const createCard = (userInfo) => { + const div = document.createElement('div'); + div.innerHTML = ( + '
    ' + + '' + + '

    ' + userInfo.name + '

    ' + + '

    ' + userInfo.description + '

    ' + + '
    ' + ); + return div; +}; + +const mentions_menu_hover = async (userInfo, success) => { + const card = createCard(userInfo); + success(card); +}; tinymce.init({ selector: 'textarea', plugins: 'mentions', - mentions_menu_hover: (userInfo, success) => { - // request more information about the user from the server and cache it locally - if (!userRequest[userInfo.id]) { - userRequest[userInfo.id] = fetch('/user?id=' + userInfo.id); - } - userRequest[userInfo.id].then((userDetail) => { - const div = document.createElement('div'); - - div.innerHTML = ( - '
    ' + - '

    ' + userDetail.fullName + '

    ' + - '' + - '

    ' + userDetail.description + '

    ' + - '
    ' - ); - - success(div); - }); - } + mentions_selector: '.mymention', + mentions_menu_hover }); ---- @@ -45,18 +44,11 @@ If `+mentions_menu_hover+` is resolved with an object specifying the type and us [source,js] ---- -const userRequest = {}; tinymce.init({ selector: 'textarea', plugins: 'mentions', - mentions_menu_hover: (userInfo, success) => { - // request more information about the user from the server and cache it locally - if (!userRequest[userInfo.id]) { - userRequest[userInfo.id] = fetch('/user?id=' + userInfo.id); - } - userRequest[userInfo.id].then((userDetail) => { - success({ type: 'profile', user: userDetail }); - }); - } + mentions_selector: '.mymention', + mentions_menu_hover: (userInfo, success) => + success({ type: 'profile', user: userInfo }) }); ----- +---- \ No newline at end of file diff --git a/modules/ROOT/partials/configuration/mentions_select.adoc b/modules/ROOT/partials/configuration/mentions_select.adoc index 8bc67c707f..a27390ef86 100644 --- a/modules/ROOT/partials/configuration/mentions_select.adoc +++ b/modules/ROOT/partials/configuration/mentions_select.adoc @@ -9,40 +9,41 @@ This option enables a hover card to be presented when a user hovers over a menti [source,js] ---- -const userRequest = {}; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const createCard = (userInfo) => { + const div = document.createElement('div'); + div.innerHTML = ( + '
    ' + + '' + + '

    ' + userInfo.name + '

    ' + + '

    ' + userInfo.description + '

    ' + + '
    ' + ); + return div; +}; + +const mentions_select = async (mention, success) => { + const id = mention.getAttribute('data-mention-id'); + await fetch(`${API_URL}/${id}`) + .then((response) => response.json()) + .then((userInfo) => { + const card = createCard({ + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + }); + success(card); + }) + .catch((error) => console.error(error)); +}; tinymce.init({ selector: 'textarea', plugins: 'mentions', - mentions_selector: 'span.mymention', - mentions_menu_complete: (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - }, - mentions_select: (mention, success) => { - // `mention` is the element we previously created with `mentions_menu_complete` - // in this case we have chosen to store the id as an attribute - const id = mention.getAttribute('data-mention-id'); - // request more information about the user from the server and cache locally - if (!userRequest[id]) { - userRequest[id] = fetch('/user?id=' + id); - } - userRequest[id].then((userDetail) => { - const div = document.createElement('div'); - div.innerHTML = ( - '
    ' + - '

    ' + userDetail.fullName + '

    ' + - '' + - '

    ' + userDetail.description + '

    ' + - '
    ' - ); - success(div); - }); - } + mentions_selector: '.mymention', + mentions_select }); ---- @@ -54,29 +55,28 @@ If `+mentions_select+` is resolved with an object specifying the type and user d [source,js] ---- -const userRequest = {}; +const API_URL = 'https://demouserdirectory.tiny.cloud/v1/users'; + +const mentions_select = async (mention, success) => { + const id = mention.getAttribute('data-mention-id'); + await fetch(`${API_URL}/${id}`) + .then((response) => response.json()) + .then((userInfo) => { + const user = { + id: userInfo.id, + name: userInfo.name, + image: userInfo.avatar, + description: userInfo.custom.role + }; + success({ type: 'profile', user }); + }) + .catch((error) => console.error(error)); +}; + tinymce.init({ selector: 'textarea', plugins: 'mentions', - mentions_selector: 'span.mymention', - mentions_menu_complete: (editor, userInfo) => { - const span = editor.getDoc().createElement('span'); - span.className = 'mymention'; - span.setAttribute('data-mention-id', userInfo.id); - span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name)); - return span; - }, - mentions_select: (mention, success) => { - // `mention` is the element we previously created with `mentions_menu_complete` - // in this case we have chosen to store the id as an attribute - const id = mention.getAttribute('data-mention-id'); - // request more information about the user from the server and cache locally - if (!userRequest[id]) { - userRequest[id] = fetch('/user?id=' + id); - } - userRequest[id].then((userDetail) => { - success({ type: 'profile', user: userDetail }); - }); - } + mentions_selector: '.mymention', + mentions_select }); ----- +---- \ No newline at end of file