Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
be90ec1
DOC-3131: Initial commit for TinyMCE 7.8.0 release notes.
kemister85 Mar 24, 2025
0421cfb
Merge remote-tracking branch 'origin/release/7.8' into feature/7.8.0/…
kemister85 Mar 31, 2025
7a9c105
DOC-3131: A confirmation dialog now appears when resolving conversati…
kemister85 Apr 8, 2025
76877b0
DOC-3131: Setting editor height to a pt or em value was ignoring min/…
kemister85 Apr 8, 2025
d4732f8
DOC-3131: The `editor.selection.scrollIntoView()` method now pads the…
kemister85 Apr 8, 2025
eff9874
DOC-3131: The change event was not dispatched when deleting a comment…
kemister85 Apr 8, 2025
a44b656
DOC-3131: Focus outline was misaligned with comment card border on sa…
abhinavgandham Apr 8, 2025
a5017e3
DOC-3131: Source Code editor was not scrolling to the editor's caret …
abhinavgandham Apr 8, 2025
fd276f7
DOC-11923: Add note on handling of relative URLs with base_url config…
kemister85 Apr 8, 2025
9776675
DOC-3189: Add new options for preserving MathML elements and attribut…
kemister85 Apr 8, 2025
06bced6
DOC-3161: Add new `tinycomments_fetch_author_info` option and refacto…
kemister85 Apr 8, 2025
ec9f554
DOC-3189: Refactor promotion documentation and update references to n…
kemister85 Apr 8, 2025
b6f83b4
DOC-3131: Keyboard navigation between toolbars no longer loses focus.…
kemister85 Apr 9, 2025
d7d14b6
DOC-3131: New subtoolbar support for context toolbars. (#3664)
kemister85 Apr 9, 2025
cd738d4
DOC-3131: Add image filter assets, and new `uploadcare_filters` optio…
kemister85 Apr 9, 2025
d8b5aad
DOC-3131: Non-uploadcare image URLs would be altered by color adjustm…
kemister85 Apr 9, 2025
8f60d90
DOC-3131: Dragging and dropping image files onto the placeholder was …
kemister85 Apr 9, 2025
f6131b8
DOC-3131: Placeholder element is now responsive and adjust to contain…
abhinavgandham Apr 9, 2025
a0eef1b
DOC-3131: Add changelog.
kemister85 Apr 9, 2025
5f43b72
DOC-3131: It wasn't possible to add comments to selected footnotes el…
kemister85 Apr 9, 2025
7200b42
DOC-3131: The tooltip for the `blockquote` button was not translated …
abhinavgandham Apr 9, 2025
ce365c0
DOC-3131: copy edits, release note re-structure and final version che…
kemister85 Apr 9, 2025
9485c15
DOC-3131: add missed new line space.
kemister85 Apr 9, 2025
10cc0a4
DOC-3131: api-version bump for TinyMCE 7.8.0.
kemister85 Apr 9, 2025
4d4577c
DOC-3131: update productminorversion to 7.8.
kemister85 Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .api-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.7.2
7.8.0
2 changes: 1 addition & 1 deletion antora.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ asciidoc:
# product variables
productname: TinyMCE
productmajorversion: 7
productminorversion: '7.7'
productminorversion: '7.8'
##### product name in codeblock
prodnamecode: tinymce
#### more names
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<textarea id="comments-callback">
<textarea id="comments-callback-with-mentions">
<h2>Welcome to Tiny Comments!</h2>
<p>Please try out this demo of our Tiny Comments premium plugin with @mentions support.</p>
<ol>
Expand Down
284 changes: 158 additions & 126 deletions modules/ROOT/examples/live-demos/comments-callback-with-mentions/index.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,73 @@
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 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 currentUser = {
id: 'jennynichols',
name: 'Jenny Nichols',
fullName: 'Jenny Nichols',
description: 'Marketing Director',
image: "https://i.pravatar.cc/150?img=10"

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': {
uid: 'mce-conversation_19679600221621399703915',
comments: [{
uid: 'mce-conversation_19679600221621399703915',
author: currentUser.id,
authorName: currentUser.fullName,
authorAvatar: currentUser.image,
content: `What do you think about this @${adminUser.id}?`,
createdAt: '2021-05-19T04:48:23.914Z',
modifiedAt: '2021-05-19T04:48:23.914Z'
},
{
...getAuthorInfo(currentUid),
content: `What do you think about this? @${adminUser.id}?`,
createdAt: yesterday,
modifiedAt: yesterday
}, {
uid: 'mce-conversation_19679600221621399703917',
author: adminUser.id,
authorName: adminUser.fullName,
authorAvatar: adminUser.image,
...getAuthorInfo(adminUid),
content: `I think this is a great idea @${currentUser.id}!`,
createdAt: "2024-05-28T12:54:24.126Z",
modifiedAt: "2024-05-28T12:54:24.126Z",
createdAt: anhourago,
modifiedAt: anhourago,
}]
},
'mce-conversation_420304606321716900864126': {
uid: 'mce-conversation_420304606321716900864126',
comments: [{
uid: 'mce-conversation_420304606321716900864126',
author: adminUser.id,
authorName: adminUser.fullName,
authorAvatar: adminUser.image,
...getAuthorInfo(adminUid),
content: `@${currentUser.id} Please revise this sentence, exclamation points are unprofessional!`,
createdAt: '2024-05-28T12:54:24.126Z',
modifiedAt: '2024-05-28T12:54:24.126Z'
createdAt: yesterday,
modifiedAt: anhourago
}]
}
};
Expand Down Expand Up @@ -122,6 +143,10 @@ import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then

const fakeServer = setupFakeServer();

/********************************
* Mentions functions *
********************************/

const mentions_fetch = (query, success) => {
if (!fetchedUsers) {
fetchedUsers = true;
Expand Down Expand Up @@ -162,6 +187,11 @@ import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then
}
};

/********************************
* 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...'));
Expand All @@ -170,131 +200,132 @@ import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then
conversationDb[uid] = {
uid,
comments: [{
uid,
authorName: currentUser.fullName,
authorAvatar: currentUser.image,
author: currentUser.name,
content: req.content,
createdAt: req.createdAt,
modifiedAt: req.createdAt
uid,
...getAuthorInfo(currentUid),
content: req.content,
createdAt: req.createdAt,
modifiedAt: req.createdAt
}]
};

setTimeout(() => {
done({
conversationUid: uid
});
}, fakeDelay);
setTimeout(() => done({ conversationUid: uid }), fakeDelay);
}
};

const tinycomments_reply = (req, done) => {
const replyUid = 'annotation-' + randomString();
const current = conversationDb[req.conversationUid];
current.comments.push(
{
uid: replyUid,
authorName: currentUser.fullName,
authorAvatar: currentUser.image,
author: currentUser.name,
content: req.content,
createdAt: req.createdAt,
modifiedAt: req.createdAt
}
);

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

const tinycomments_delete = (req, done) => {
delete conversationDb[req.conversationUid];

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

const tinycomments_resolve = (req, done) => {
resolvedConversationDb[req.conversationUid] = conversationDb[req.conversationUid];
delete conversationDb[req.conversationUid];

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

const tinycomments_delete_comment = (req, done) => {
const current = conversationDb[req.conversationUid];
// Should be supported on browsers ...
current.comments = current.comments.filter((f) => {
return f.uid !== req.commentUid;
const oldcomments = conversationDb[req.conversationUid].comments;
let reason = 'Comment not found';

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

setTimeout(() => {
done({
canDelete: true
});
}, fakeDelay);
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 current = conversationDb[req.conversationUid];
// Should be supported on browsers ...
current.comments = current.comments.map((f) => {
return f.uid === req.commentUid ? {
...f,
content: req.content,
modifiedAt: new Date().toISOString()
} : f;
const oldcomments = conversationDb[req.conversationUid].comments;
let reason = 'Comment not found';
let canEdit = false;

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

setTimeout(() => {
done({
canEdit: true
});
}, fakeDelay);

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

const tinycomments_delete_all = (req, done) => {
Object.keys(conversationDb).forEach((k) => {
delete conversationDb[k];
});

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

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

const tinycomments_fetch = (_, done) => {
setTimeout(() => done({
conversations: conversationDb
}), fakeDelay);

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


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

tinymce.init({
selector: 'textarea#comments-callback',
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',
Expand Down Expand Up @@ -331,5 +362,6 @@ import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then
tinycomments_delete_comment,
tinycomments_edit_comment,
tinycomments_fetch,
tinycomments_fetch_author_info
});
});
Loading