Skip to content

Commit 6c78474

Browse files
MitchC1999kemister85Farzad Hayat
authored
DOC-2558: New comments with mentions page and demos (#3499)
* DOC-2558: New comments with mentions page and demos * DOC-2558: Fix codeQL * Apply suggestions from code review Co-authored-by: Karl Kemister-Sheppard <[email protected]> * Apply suggestions from code review Co-authored-by: Karl Kemister-Sheppard <[email protected]> * Apply suggestions from code review Co-authored-by: Karl Kemister-Sheppard <[email protected]> * Apply suggestions from code review Co-authored-by: Karl Kemister-Sheppard <[email protected]> * DOC-2558: Fix some broken mentions * Apply suggestions from code review Co-authored-by: Farzad Hayat <[email protected]> * Apply suggestions from code review Co-authored-by: Farzad Hayat <[email protected]> * Apply suggestions from code review Co-authored-by: Farzad Hayat <[email protected]> * DOC-2558: Revert URL --------- Co-authored-by: Karl Kemister-Sheppard <[email protected]> Co-authored-by: Farzad Hayat <[email protected]>
1 parent 3056162 commit 6c78474

File tree

9 files changed

+819
-121
lines changed

9 files changed

+819
-121
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<textarea id="comments-callback">
2+
<h2>Welcome to Tiny Comments!</h2>
3+
<p>Please try out this demo of our Tiny Comments premium plugin with @mentions support.</p>
4+
<ol>
5+
<li>Highlight the content you want to comment on.</li>
6+
<li>Click the <em>Add Comment</em> icon in the toolbar.</li>
7+
<li>Type your comment into the text field at the bottom of the Comment sidebar, and use <code>@</code> followed by a name to mention a collaborator.</li>
8+
<li>Click <strong>Comment</strong>.</li>
9+
</ol>
10+
<p>Your comment is <span class="mce-annotation tox-comment" data-mce-annotation-uid="mce-conversation_420304606321716900864126" data-mce-annotation="tinycomments">then</span> attached to the text, <span class="mce-annotation tox-comment" data-mce-annotation-uid="mce-conversation_19679600221621399703915" data-mce-annotation="tinycomments">exactly like this!</span> You can <span class="mymention" style="color: #1b1; background-color: #eee;" data-mention-id="jennynichols" data-mce-mentions-id="jennynichols">@Jenny Nichols</span> directly in your comments to notify them.</p>
11+
<p>If you want to take Tiny Comments for a test drive in your own environment, Tiny Comments is one of the premium plugins you can try for free for 14 days by signing up for a Tiny account. Make sure to check out our documentation as well.</p>
12+
<h2>A simple table to play with</h2>
13+
<table style="border-collapse: collapse; width: 100%;" border="1">
14+
<thead>
15+
<tr>
16+
<th>Product</th>
17+
<th>Value</th>
18+
</tr>
19+
</thead>
20+
<tbody>
21+
<tr>
22+
<td><a href="https://www.tiny.cloud/">Tiny Cloud</a></td>
23+
<td>The easiest and most reliable way to integrate powerful rich text editing into your application.</td>
24+
</tr>
25+
<tr>
26+
<td><a href="https://www.tiny.cloud/drive/">Tiny Drive</a></td>
27+
<td>Image and file management for TinyMCE in the cloud.</td>
28+
</tr>
29+
</tbody>
30+
</table>
31+
<p>Thanks for supporting TinyMCE! We hope it helps your users create great content.</p>
32+
</textarea>
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import ('https://cdn.jsdelivr.net/npm/@faker-js/faker@9/dist/index.min.js').then(({ faker }) => {
2+
const adminUser = {
3+
id: 'johnsmith',
4+
name: 'John Smith',
5+
fullName: 'John Smith',
6+
description: 'Company Founder',
7+
image: "https://i.pravatar.cc/150?img=11"
8+
};
9+
10+
const currentUser = {
11+
id: 'jennynichols',
12+
name: 'Jenny Nichols',
13+
fullName: 'Jenny Nichols',
14+
description: 'Marketing Director',
15+
image: "https://i.pravatar.cc/150?img=10"
16+
};
17+
18+
const conversationDb = {
19+
'mce-conversation_19679600221621399703915': {
20+
uid: 'mce-conversation_19679600221621399703915',
21+
comments: [{
22+
uid: 'mce-conversation_19679600221621399703915',
23+
author: currentUser.id,
24+
authorName: currentUser.fullName,
25+
authorAvatar: currentUser.image,
26+
content: `What do you think about this @${adminUser.id}?`,
27+
createdAt: '2021-05-19T04:48:23.914Z',
28+
modifiedAt: '2021-05-19T04:48:23.914Z'
29+
},
30+
{
31+
uid: 'mce-conversation_19679600221621399703917',
32+
author: adminUser.id,
33+
authorName: adminUser.fullName,
34+
authorAvatar: adminUser.image,
35+
content: `I think this is a great idea @${currentUser.id}!`,
36+
createdAt: "2024-05-28T12:54:24.126Z",
37+
modifiedAt: "2024-05-28T12:54:24.126Z",
38+
}]
39+
},
40+
'mce-conversation_420304606321716900864126': {
41+
uid: 'mce-conversation_420304606321716900864126',
42+
comments: [{
43+
uid: 'mce-conversation_420304606321716900864126',
44+
author: adminUser.id,
45+
authorName: adminUser.fullName,
46+
authorAvatar: adminUser.image,
47+
content: `@${currentUser.id} Please revise this sentence, exclamation points are unprofessional!`,
48+
createdAt: '2024-05-28T12:54:24.126Z',
49+
modifiedAt: '2024-05-28T12:54:24.126Z'
50+
}]
51+
}
52+
};
53+
54+
const fakeDelay = 500;
55+
const numberOfUsers = 200;
56+
const randomString = () => {
57+
return crypto.getRandomValues(new Uint32Array(1))[0].toString(36).substring(2, 14);
58+
};
59+
60+
/* These are "local" caches of the data returned from the fake server */
61+
let fetchedUsers = false;
62+
let usersRequest; // Promise
63+
const userRequest = {};
64+
const resolvedConversationDb = {};
65+
66+
const setupFakeServer = () => {
67+
const images = [ adminUser.image, currentUser.image ];
68+
const userNames = [ adminUser.fullName, currentUser.fullName ];
69+
70+
for (let i = 0; i < numberOfUsers; i++) {
71+
images.push(faker.image.avatar());
72+
userNames.push(`${faker.person.firstName()} ${faker.person.lastName()}`);
73+
}
74+
75+
/* This represents a database of users on the server */
76+
const userDb = {
77+
[adminUser.id]: adminUser,
78+
[currentUser.id]: currentUser
79+
};
80+
userNames.map((fullName) => {
81+
if ((fullName !== currentUser.fullName) && (fullName !== adminUser.fullName)) {
82+
const id = fullName.toLowerCase().replace(/ /g, '');
83+
userDb[id] = {
84+
id,
85+
name: fullName,
86+
fullName,
87+
description: faker.person.jobTitle(),
88+
image: images[Math.floor(images.length * Math.random())]
89+
};
90+
}
91+
});
92+
93+
/* This represents getting the complete list of users from the server with the details required for the mentions "profile" item */
94+
const fetchUsers = () => new Promise((resolve) => {
95+
/* simulate a server delay */
96+
setTimeout(() => {
97+
const users = Object.keys(userDb).map((id) => ({
98+
id,
99+
name: userDb[id].name,
100+
image: userDb[id].image,
101+
description: userDb[id].description
102+
}));
103+
resolve(users);
104+
}, fakeDelay);
105+
});
106+
107+
const fetchUser = (id) => new Promise((resolve, reject) => {
108+
/* simulate a server delay */
109+
setTimeout(() => {
110+
if (Object.prototype.hasOwnProperty.call(userDb, id)) {
111+
resolve(userDb[id]);
112+
}
113+
reject('unknown user id "' + id + '"');
114+
}, fakeDelay);
115+
});
116+
117+
return {
118+
fetchUsers,
119+
fetchUser
120+
};
121+
};
122+
123+
const fakeServer = setupFakeServer();
124+
125+
const mentions_fetch = (query, success) => {
126+
if (!fetchedUsers) {
127+
fetchedUsers = true;
128+
usersRequest = fakeServer.fetchUsers();
129+
}
130+
usersRequest.then((users) => {
131+
const userMatch = (name) => name.toLowerCase().indexOf(query.term.toLowerCase()) !== -1;
132+
success(users.filter((user) => userMatch(user.name) || userMatch(user.id)));
133+
fetchedUsers = false;
134+
});
135+
};
136+
137+
const mentions_menu_hover = (userInfo, success) => {
138+
if (!userRequest[userInfo.id]) {
139+
userRequest[userInfo.id] = fakeServer.fetchUser(userInfo.id);
140+
}
141+
userRequest[userInfo.id].then((userDetail) => {
142+
success({ type: 'profile', user: userDetail });
143+
});
144+
};
145+
146+
const mentions_menu_complete = (editor, userInfo) => {
147+
const span = editor.getDoc().createElement('span');
148+
span.className = 'mymention';
149+
span.setAttribute('style', 'color: #1b1; background-color: #eee;');
150+
span.setAttribute('data-mention-id', userInfo.id);
151+
span.appendChild(editor.getDoc().createTextNode('@' + userInfo.name));
152+
return span;
153+
};
154+
155+
const mentions_select = (mention, success) => {
156+
const id = mention.getAttribute('data-mention-id');
157+
if (id) {
158+
userRequest[id] = fakeServer.fetchUser(id);
159+
userRequest[id].then((userDetail) => {
160+
success({ type: 'profile', user: userDetail });
161+
});
162+
}
163+
};
164+
165+
const tinycomments_create = (req, done, fail) => {
166+
if (req.content === 'fail') {
167+
fail(new Error('Something has gone wrong...'));
168+
} else {
169+
const uid = 'annotation-' + randomString();
170+
conversationDb[uid] = {
171+
uid,
172+
comments: [{
173+
uid,
174+
authorName: currentUser.fullName,
175+
authorAvatar: currentUser.image,
176+
author: currentUser.name,
177+
content: req.content,
178+
createdAt: req.createdAt,
179+
modifiedAt: req.createdAt
180+
}]
181+
};
182+
183+
setTimeout(() => {
184+
done({
185+
conversationUid: uid
186+
});
187+
}, fakeDelay);
188+
}
189+
};
190+
191+
const tinycomments_reply = (req, done) => {
192+
const replyUid = 'annotation-' + randomString();
193+
const current = conversationDb[req.conversationUid];
194+
current.comments.push(
195+
{
196+
uid: replyUid,
197+
authorName: currentUser.fullName,
198+
authorAvatar: currentUser.image,
199+
author: currentUser.name,
200+
content: req.content,
201+
createdAt: req.createdAt,
202+
modifiedAt: req.createdAt
203+
}
204+
);
205+
206+
setTimeout(() => {
207+
done({
208+
commentUid: replyUid
209+
});
210+
}, fakeDelay);
211+
};
212+
213+
const tinycomments_delete = (req, done) => {
214+
delete conversationDb[req.conversationUid];
215+
216+
setTimeout(() => {
217+
done({
218+
canDelete: true
219+
});
220+
}, fakeDelay);
221+
};
222+
223+
const tinycomments_resolve = (req, done) => {
224+
resolvedConversationDb[req.conversationUid] = conversationDb[req.conversationUid];
225+
delete conversationDb[req.conversationUid];
226+
227+
setTimeout(() => {
228+
done({
229+
canResolve: true
230+
});
231+
}, fakeDelay);
232+
};
233+
234+
const tinycomments_delete_comment = (req, done) => {
235+
const current = conversationDb[req.conversationUid];
236+
// Should be supported on browsers ...
237+
current.comments = current.comments.filter((f) => {
238+
return f.uid !== req.commentUid;
239+
});
240+
241+
setTimeout(() => {
242+
done({
243+
canDelete: true
244+
});
245+
}, fakeDelay);
246+
};
247+
248+
const tinycomments_edit_comment = (req, done) => {
249+
const current = conversationDb[req.conversationUid];
250+
// Should be supported on browsers ...
251+
current.comments = current.comments.map((f) => {
252+
return f.uid === req.commentUid ? {
253+
...f,
254+
content: req.content,
255+
modifiedAt: new Date().toISOString()
256+
} : f;
257+
});
258+
259+
setTimeout(() => {
260+
done({
261+
canEdit: true
262+
});
263+
}, fakeDelay);
264+
};
265+
266+
const tinycomments_delete_all = (req, done) => {
267+
Object.keys(conversationDb).forEach((k) => {
268+
delete conversationDb[k];
269+
});
270+
271+
setTimeout(() => {
272+
done({
273+
canDelete: true
274+
});
275+
}, fakeDelay);
276+
};
277+
278+
const tinycomments_lookup = (req, done) => {
279+
setTimeout(() => {
280+
done({
281+
conversation: {
282+
uid: conversationDb[req.conversationUid].uid,
283+
comments: conversationDb[req.conversationUid].comments.slice(0)
284+
}
285+
});
286+
}, fakeDelay);
287+
};
288+
289+
const tinycomments_fetch = (_, done) => {
290+
setTimeout(() => done({
291+
conversations: conversationDb
292+
}), fakeDelay);
293+
};
294+
295+
296+
tinymce.init({
297+
selector: 'textarea#comments-callback',
298+
license_key: 'gpl',
299+
toolbar: 'addcomment showcomments code | bold italic underline',
300+
menubar: 'file edit view insert format tools tc help',
301+
menu: {
302+
tc: {
303+
title: 'TinyComments',
304+
items: 'addcomment showcomments deleteallconversations'
305+
}
306+
},
307+
plugins: [ 'tinycomments', 'mentions', 'help', 'code' ],
308+
tinycomments_mentions_enabled: true,
309+
310+
mentions_item_type: 'profile',
311+
mentions_min_chars: 0,
312+
mentions_selector: '.mymention',
313+
mentions_fetch,
314+
mentions_menu_hover,
315+
mentions_menu_complete,
316+
mentions_select,
317+
318+
tinycomments_mode: 'callback',
319+
tinycomments_author: currentUser.id,
320+
tinycomments_author_name: currentUser.fullName,
321+
tinycomments_avatar: currentUser.image,
322+
tinycomments_create,
323+
tinycomments_reply,
324+
tinycomments_delete,
325+
tinycomments_resolve,
326+
tinycomments_delete_all,
327+
tinycomments_lookup,
328+
tinycomments_delete_comment,
329+
tinycomments_edit_comment,
330+
tinycomments_fetch,
331+
});
332+
});

0 commit comments

Comments
 (0)