Comment 插件可与 Mentions 插件一起使用,以允许用户在评论中提及其他用户。
当用户使用 @ 符号开始输入提及内容时,提及下拉列表将出现。用户可以继续输入以缩小搜索结果范围,然后点击所需用户或使用箭头键导航并按 Enter 键将提及添加到评论中。
保存评论时,提及的用户将以与编辑器中相同的方式出现在评论文本中。
所有提及均以纯文本形式存储在评论文本中,因此要正确显示提及内容,在显示评论时必须在 TinyMCE 配置中包含 Mentions 插件。
要在评论中添加提及,必须在 TinyMCE 配置中包含 Comment 和 Mentions 插件,并设置所有必需的选项。
Callback 模式交互示例
HTML
<textarea id="comments-callback-with-mentions">
<h2>欢迎使用 Tiny Comments!</h2>
<p>请尝试我们支持 @mentions 的 Tiny Comments premium 插件演示。</p>
<ol>
<li>高亮显示要评论的内容。</li>
<li>点击工具栏中的<em>Add Comment</em>图标。</li>
<li>在 Comment 侧边栏底部的文本字段中输入您的评论,并使用<code>@</code>后跟一个名称来提及协作者。</li>
<li>点击<strong>Comment</strong>。</li>
</ol>
<p>您的评论<span class="mce-annotation tox-comment" data-mce-annotation-uid="mce-conversation_19679600221621399703915" data-mce-annotation="tinycomments">then</span>会附加到文本上,<span class="mce-annotation tox-comment" data-mce-annotation-uid="mce-conversation_420304606321716900864126" data-mce-annotation="tinycomments">exactly like this!</span>您可以直接在评论中<span class="mymention" style="color: gray;" data-mention-id="mia-andersson" data-mce-mentions-id="mia-andersson">@Mia Andersson</span>以通知他们。</p>
<p>如果您想在您的环境中试用 Tiny Comments,Tiny Comments 是 premium 插件之一,您可以通过注册 Tiny 账户免费试用 14 天。请务必查看我们的文档。</p>
<h2>一个简单的表格供您使用</h2>
<table style="border-collapse: collapse; width: 100%;" border="1">
<thead>
<tr>
<th>Product</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://www.tiny.cloud/">Tiny Cloud</a></td>
<td>将强大的富文本编辑功能集成到您的应用程序中最简单可靠的方式。</td>
</tr>
<tr>
<td><a href="https://www.tiny.cloud/drive/">Tiny Drive</a></td>
<td>云端 TinyMCE 的图像和文件管理。</td>
</tr>
</tbody>
</table>
<p>感谢您支持 TinyMCE!我们希望它帮助您的用户创建出色的内容。</p>
</textarea>CSS
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;
}JS
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 = (
'<div class="card">' +
'<img class="avatar" src="' + userInfo.image + '">' +
'<h1>' + userInfo.name + '</h1>' +
'<p>' + userInfo.description + '</p>' +
'</div>'
);
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
}]
};
setTimeout(() => done({ conversationUid: uid }), 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 (user_id === collaborator_id) { // 替换为你自己的逻辑,例如检查用户是否创建了对话
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) { // 替换为你自己的逻辑,例如检查用户是否具有管理员权限
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) { // 找到要删除的评论
if (user_id === comment.author) { // 替换为你自己的逻辑,例如检查用户是否具有管理员权限
privileges
return false; // 移除评论
} else {
reason = '无权删除此评论'; // 更新原因
}
}
return true; // 保留评论
});
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 = '未找到评论';
let canEdit = false;
const newcomments = oldcomments.map((comment) => {
if (comment.uid === req.commentUid) { // 找到要删除的评论
if (user_id === comment.author) { // 替换为您自己的逻辑,例如检查用户是否有管理员权限
canEdit = true; // 用户可以编辑评论
return { ...comment, content: req.content, modifiedAt: new Date().toISOString() }; // 更新评论
} else {
reason = '无权编辑此评论'; // 更新原因
}
}
return comment; // 保留评论
});
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) { // 替换为您自己的逻辑,例如检查用户是否有管理员权限
delete conversationDb[req.conversationUid];
setTimeout(() => done({ canDelete: true }), fakeDelay);
} else {
setTimeout(() => done({ canDelete: false, reason: '必须是会话作者' }), 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};
}
});
setTimeout(() => done({ conversations: fetchedConversations }), fakeDelay);
};
tinymce.init({
selector: 'textarea#comments-callback-with-mentions',
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,
user_id,
fetch_users: (userIds) => Promise.all(userIds
.map((userId) =>
fetch(`${API_URL}/${userId}`)
.then((response) => response.json())
.catch(() => ({ id: userId }))))
});