在tinymce6中开发自定义批注功能

在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 }} &nbsp
                </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元素的各种属性。
吃透这三点,合理的插件全都可以自主开发,这款国外的富文本编辑器对开发者真的非常友好,希望大家可以多多钻研,共同探讨。

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
tinymce 是一个基于JavaScript的富文本编辑器,它在网页开发广泛应用于文本编辑、内容发布等场景。在 tinymce ,要实现自定义图片上传功能,可以通过插件的方式来进行。 首先,我们需要了解 tinymce 预留的自定义上传接口。 tinymce 提供了`file_picker_callback`接口,可以自定义上传图片的触发方式和上传逻辑。通过这个接口,我们可以按需实现图片上传功能。 具体操作如下: 1. 在页面引入 tinymce 的库文件和插件,确保可以正确加载富文本编辑器。 2. 初始化 tinymce 实例时,需要传入相应的参数,其包括 `file_picker_callback`接口。该接口会在用户点击插入图片按钮时调用。 3. 将`file_picker_callback`接口指定为一个自定义的方法,用于处理图片上传的逻辑。在该方法,可以通过一些文件上传框架(如 FormData、jQuery.ajax()等)实现图片的上传,并将上传成功后的图片地址返回给 tinymce 编辑器。 4. 根据返回的图片地址,我们可以在页面上插入图片。 需要注意的是,上传的图片应该保存在服务器端,并返回图片的 URL 地址,然后通过 `editor.insertContent` 方法插入到编辑器。另外,为了供用户查看已上传的图片,也可以在编辑器上方增加一个图片预览区域。 这样,就完成了在 tinymce 自定义图片上传的功能。用户可以点击插入图片按钮,选择本地图片并上传,然后在编辑器显示上传后的图片。 总结起来,要在 tinymce 实现自定义图片上传的功能,需要了解 tinymce 提供的 `file_picker_callback` 接口,并通过该接口自定义图片的上传逻辑和操作。同时,需要将上传后的图片地址插入到编辑器,使用户能够看到上传的图片。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值