tinymce开发自定义插件-批注
在tinymce6中开发自定义批注功能
实现效果
1.工具栏按钮,分别为添加批注、显示所有批注、删除批注和隐藏批注。
2.文档中的批注显示和侧边栏中批注详情显示(包括回复)
开发过程
1.在页面增加批注显示区域
之前已经介绍过tinymce编辑器的引入,在引入tinymce的组件中增加一块元素来展示批注,如下
<div class="tinymce-editor">
<div :key="refreshKey">
<Editor v-model="myValue" :init="init" :disabled="disabled" />
</div>
</div>
<div id="comments-area" style="display: none">
</div>
display:none 即在默认情况下是隐藏评论区域的,之后我们用插件中的js代码来控制display就可以改变显隐状态。
2.构建你的批注区域内容
我实现的是树状评论,所以用单独写了个递归组件
<template>
<div class="comment-box">
<div class="comment-tree" :style="{ marginLeft: `${margin}px` }">
<div class="comment-title" @click="scrollToComment(node.commentId)">
<Icon :name="node.parentCommentIndex == 0 ? 'Comment' : 'CommentReply'" :color="'#1296db'"
style="width: 18px; height: 18px; display: inline-block; margin: 0 10px;" />
<!-- <span>
#{{ node.cid ? node.cid : NaN }}  
</span> -->
<span style="font-weight: 600;">
{{ node.title }}
</span> 由
<span style="font-weight: 600">
{{ node.createUserName }}
</span> 于
<span style="font-weight: 600">{{ node.createTime }} </span> {{ node.parentCommentId ==
'comment_0' ? '评注' : '回复' }}
</div>
<div class="comment-content"> {{ node.comment }}</div>
<el-icon style="float: right; color: green; margin-right: 5px; top: -20px;" @click="openReply()">
<ChatDotSquare />
</el-icon>
<el-input v-if="replyFlag" v-model="node.reply" type="textarea"></el-input>
<div v-if="node.treeStructure">
<docComments ref="commentTreeRefs" v-for="child in node.treeStructure" :key="child" :node="child"
:margin="margin + 10" @isTreeReply="isTreeReply" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import Icon from "@/components/Icon.vue"; // 引入图标库
const props = defineProps(['node', 'margin']);
const emit = defineEmits(['isTreeReply', 'scrollToComment'])
const isTreeReply = () => {
emit('isTreeReply')
}
const replyContent = ref<string>('')
const replyParentId = ref<number>()
const replyFlag = ref<boolean>(false)
const openReply = () => {
replyFlag.value = !replyFlag.value;
}
const scrollToComment = (id: string) => {
emit('scrollToComment', id)
}
const commentTreeRefs = ref()
</script>
<style scoped>
.comment-box {
padding: 10px;
}
.comment-tree {
margin: 5px;
}
.comment-title {
background-color: rgb(245, 247, 250);
padding-top: 8px;
padding-bottom: 8px;
padding-right: 8px;
width: auto;
cursor: pointer;
}
.comment-content {
margin: 10px 20px;
/* padding-top: 8px;
padding-bottom: 8px; */
padding-right: 8px;
width: auto;
}
</style>
然后在之前添加的批注区域引用此组件,如何引用就不赘述了,如下
<div id="comments-area" style="display: none">
<div class="comments-area-title">
<el-icon>
<ChatDotSquare />
</el-icon>
<span style="">评注</span>
<Icon class="close-btn" name="CloseCircle" color="#fff" style="width: 20px; height: 20px"
@click="closeComments" />
</div>
<CommentTree v-for="node in treeStructure" :key="node" :id="node.commentId" :node="node" :margin="0"
class="comments-area-comment" @scrollToComment="scrollToComment" />
</div>
批注区域内的结构可以随意构建,如果想构建平面的评论那更简单,直接v-for循环就好了
3.定义批注区域所需要的数据和函数
如下
//评论区功能
const addComment = (comment: any) => {
treeStructure.value.push(comment)
}
const closeComments = () => {
// 关闭评论框
document.getElementById('comments-area')!.style.display = 'none'
}
const scrollToComment = (id: any) => {
//通过锚点跳转到评论图标所在位置
const commentNode = tinymce.activeEditor?.getDoc()?.getElementById(id)
commentNode?.scrollIntoView()
if (commentNode) {
tinymce.activeEditor?.selection.select(commentNode) //选中评论图标
}
const a = document.querySelectorAll('.comments-area-comment')
a.forEach((item) => {
// item.style.background = '#fff';
if (item instanceof HTMLElement) {
item.style.background = '#fff'; // 确保已正确获取到元素后再操作
}
})
// document.getElementById(id).style.background = '#cde6eb'
const element = document.getElementById(id);
if (element !== null) {
element.style.background = '#cde6eb';
}
}ture = ref<any[]>([])
包括了新增评论,关闭评论框,点击跳转到文档中评论图标位置的函数
4.将部分函数绑定到编辑器属性上,方便在插件内调用
// An highlighted block
const init: InitProps = reactive(
{
placeholder: '在这里输入文字',
language_url: '/tinymce/langs/zh_Hans.js', // 汉化路径
language: 'zh_Hans',
//...省略其他初始化的属性
setup: function (editor: any) {
editor.addComment = addComment //将定义的函数绑到editor属性上
}
}
5.制作批注插件
制作自定义插件的过程在之前的文章已经讲过,还没看过的小伙伴可以点这里链接: 自定义插件开发过程
这里直接将plugins中的代码,当然是在public/tinymce/plugins中创建了comment文件夹,文件夹下plugin.js文件中的代码
; (function () {
tinymce.PluginManager.add('comment', function (editor) {
// console.log(editor.settings.commentsProp)
window.deleteCommentById = deleteCommentById
var commentsArray = editor.commentsProp //将页面中的评论数组传进来
var editorHeight = editor.editorHeightProps //传入编辑器的高
// 注册一个命令,用于在光标位置添加批注
editor.addCommand('addComment', function () {
// 弹出评论对话框
editor.windowManager.open({
title: '添加评论',
body: {
type: 'panel',
items: [
{
type: 'textarea',
name: 'commentContent',
label: '评论内容',
multiline: true,
minWidth: 400,
minHeight: 300,
},
],
},
buttons: [
{
type: 'submit',
text: '添加',
primary: true,
},
{
type: 'cancel',
text: '取消',
},
],
onSubmit: function (api) {
// 获取评论内容并处理
var commentText = api.getData().commentContent
if (commentText) {
var comment = {
commentId: 'comment_' + new Date().getTime(),
range: editor.selection.getRng(),
comment: commentText,
}
// commentsArray.push(comment)
editor.addComment(comment)
document.getElementById('comments-area').style.display = 'block'
document.getElementById('comments-area').style.height =
editorHeight + 'px'
showCommentMark(comment)
}
// 在这里进行评论保存或处理的逻辑
// 例如,可以将评论内容插入到编辑器中
// editor.insertContent('<span class="comment">' + comment + '</span>')
// 关闭对话框
api.close()
},
})
})
// 注册一个命令,用于显示所有批注
editor.addCommand('showComments', function () {
console.log(editorHeight)
document.getElementById('comments-area').style.display = 'block'
document.getElementById('comments-area').style.height =
editorHeight + 'px'
})
// 注册一个命令,用于删除当前选中的批注
editor.addCommand('deleteComment', function () {
var commentId = getSelectedCommentId()
console.log(commentId)
if (commentId) {
deleteCommentById(commentId) //删除评论区和编辑区该批注
}
})
// 注册一个命令,用于隐藏批注
editor.addCommand('hideComment', function () {
document.getElementById('comments-area').style.display = 'none'
})
// 在编辑器初始化完成后,绑定事件监听,用于显示批注标记
editor.on('SelectionChange', function () {
const commentId = getSelectedCommentId()
if (commentId) {
document.getElementById('comments-area').style.display = 'block'
document.getElementById('comments-area').style.height =
editorHeight + 'px'
document.getElementById(commentId).scrollIntoView() //跳转到锚点
const a = document.querySelectorAll('.comments-area-comment')
a.forEach((item) => {
item.style.background = '#fff'
})
document.getElementById(commentId).style.background = '#cde6eb'
}
})
// 获取当前选中批注的 ID
function getSelectedCommentId() {
var node = editor.selection.getNode() //由于需要鼠标点在图标上识别而不是在图标后,所以需要找到图标的父节点
if (
node &&
node.tagName === 'IMG' &&
node.getAttribute('data-comment-id')
) {
return node.getAttribute('data-comment-id')
}
return null
}
// 根据批注 ID 删除评论区的批注 -gao
function deleteCommentById(commentId) {
var index = commentsArray.findIndex((comment) => comment.commentId === commentId)
if (index !== -1) {
commentsArray.splice(index, 1)
// editor.selection.getNode().remove() //不要删除选中节点,改为调用下面方法中根据ID删除评论节点
remoceCommentNode(commentId)
}
}
//根据批注 ID 删除tinymce编辑区中的评论节点 -gao
function remoceCommentNode(commentId) {
if (commentId) {
// 获取编辑器内容
var content = editor.getContent()
// 创建一个临时 DOM 元素来处理编辑器内容
var tempElement = document.createElement('div')
tempElement.innerHTML = content
// 查找具有指定 data-comment-id 属性的元素
var elementsToRemove = tempElement.querySelectorAll(
'[data-comment-id="' + commentId + '"]'
)
// 从 DOM 中删除找到的元素
elementsToRemove.forEach(function (element) {
element.remove()
})
// 更新编辑器的内容
editor.setContent(tempElement.innerHTML)
}
}
// 显示批注标记
function showCommentMark(comment) {
const mark = editor.getDoc().createElement('img')
mark.setAttribute('data-comment-id', comment.commentId)
mark.setAttribute('id', comment.commentId)
mark.setAttribute('style', "width: 24px; height: 24px; cursor: pointer;")
mark.setAttribute('src', "http://sztaskhub010.rigoltech.com:8999/file/20230725154616923.png")
editor.selection.setRng(comment.range)
editor.selection.setNode(mark)
}
// 定义插件的按钮和菜单项
editor.ui.registry.addButton('addcomment', {
icon: 'comment-add',
tooltip: '添加评论',
onAction: function () {
editor.execCommand('addComment')
},
})
editor.ui.registry.addButton('showcomments', {
icon: 'comment',
tooltip: '显示所有评论',
onAction: function () {
editor.execCommand('showComments')
},
})
editor.ui.registry.addButton('deletecomment', {
icon: 'comment-remove',
tooltip: '删除评论',
onAction: function () {
editor.execCommand('deleteComment')
},
})
editor.ui.registry.addButton('hideComment', {
icon: 'hide-comment',
tooltip: '隐藏评论',
onAction: function () {
editor.execCommand('hideComment')
},
})
// 添加插件按钮到工具栏
editor.ui.registry.addGroupToolbarButton('comment', {
icon: 'comment-gao',
tooltip: '评论',
items: 'addcomment showcomments deletecomment hideComment',
})
})
})()
我写代码现在都是保姆级注释,这里就不重复讲解了。
总结
如果代码复制粘贴用不了,欢迎在评论区问我,但是最主要还是要吃透这种自定义插件的开发过程,最主要就三点:1.只要你能把自定义的函数绑定到tinymce的editor属性上供插件调用;2.熟悉plugin.js中tinymce编辑器工具栏中按钮的注册方法和使用过程,以及各种api的调用;3.了解富文本编辑器实质上是操作dom元素的各种属性。
吃透这三点,合理的插件全都可以自主开发,这款国外的富文本编辑器对开发者真的非常友好,希望大家可以多多钻研,共同探讨。