市面上大多数文档编辑器的【划线评论】功能,是如何实现的?

科技   2024-11-11 08:45   重庆  

嗨, 大家好, 我是徐小夕.


最近一直在迭代多模态文档编辑器flowmix/docx, 也研究学习了很多市面上流行的文档编辑器如腾讯文档, 钉钉文档等, 发现一个非常有价值的功能: 划词评论, 如下:

它可以让文档更有交互性, 也能在协同工作中发挥很大的价值, 刚好这几天研究了一下, 在flowmix/docx 多模态编辑器中实现了这个功能, 接下来就和大家分享一下具体的技术实现.

演示地址: http://flowmix.turntip.cn/docx

后续我会持续分享可视化零代码多模态文档编辑器中的最新技术干货和产品思考, 如果大家有收获, 也不防点个赞 + 再看, 鼓励作者创作更多优质的内容~

如何实现富文本编辑器的划词评论功能

本来想划线评论功能画个小半天就实现了, 没想到坑还挺多,对于追求代码质量,代码性能,产品体验的我来说,真的是快要把 javascript 祖师爷扒出来了。

实现划词评论功能之前,们需要考虑如下几个核心要素:

  • 划线评论不能污染文档本身
  • 划线评论内容的加载性能
  • 划线位置计算和评论列表的位置计算
  • 划线评论完整业务数据流设计
  • 文档滚动对划线的位置计算的影响

本来想用 canvas 实现的, 做了一个版本,但是考虑到后期的扩展性和灵活性,我还是重新选择了离屏dom + js 来实现。最终效果如下:

实现划词评论, 我们需要充分掌握 window.getSelection() API, 它对于实现富文本编辑器的各种功能来说都非常重要.

下面是我写的一个案例, 介绍了window.getSelection()的用法, 用来获取选中的文本, 然后进行自定义的操作:

// html
  <p contenteditable="true" id="textArea">这是一段可以划词评论的文本</p>
// js逻辑
  <script>
    // 监听鼠标按下事件
    document.addEventListener('mousedown'function (e{
      if (e.target.id === 'textArea') {
        // 记录起始位置
        startSelection = window.getSelection();
      }
    });

    // 监听鼠标移动事件
    document.addEventListener('mousemove'function (e{
      if (e.target.id === 'textArea') {
        // 记录结束位置
        endSelection = window.getSelection();
      }
    });

    // 监听鼠标抬起事件
    document.addEventListener('mouseup'function (e{
      if (e.target.id === 'textArea') {
        // 获取划词的文本
        const selectedText = startSelection.toString();

        // 在这里可以进行评论相关的操作,如弹出评论框等
        console.log('选中的文本:', selectedText);
      }
    });
  
</script>

当然这个是一个简单的案例, 方便大家更好的理解, 接下来我会分享一个更复杂的案例, 来剖析划词评论的实现原理.

技术实现

首先实现划词评论需要理清实现的子目标, 接下来我来拆解一下划词评论的关键目标:

  • 1.获取当前选中的文本内容和范围;
  • 2.检查选中的文本是否已被选中,如果是则不进行画线处理;
  • 3.获取选中文本的坐标信息;
  • 4.计算选中文本的位置(这里先采用相对于视窗宽度的百分比)
  • 5.将新的选中文本信息添加到 rects 状态数组中, 方便渲染划线
  • 6.实现制定划线的评论功能

以下是从选中到计算坐标的完整实现逻辑, 大家可以参考一下:

const handleSelection = (event: any) => {
      const selection:any = window.getSelection();
      const text = selection?.toString();
      if (text && selection.rangeCount > 0) {
          const range = selection.getRangeAt(0);
          const parentElement = range.commonAncestorContainer;
          // 过滤不可划线的元素
          if(parentElement && parentElement.className && parentElement.className.includes('input')) {
            return
          }
          // 获取文本坐标
          const coordinates = range.getClientRects();
          // const wrap = document.createElement("span");
          // wrap.textContent = "";
          // range.insertNode(wrap);
          const baseWidth = window.innerWidth;
          setRects((prev:any) => [{
            text,
            x: coordinates[0].x / baseWidth * 100 + '%',
            y: coordinates[0].y,
            bottom: coordinates[0].bottom,
            scrollY0,
            w: coordinates[0].width,
            h: coordinates[0].height,
          }, ...prev])
      } 
  }

接下来我们需要添加一个事件监听器来捕获 mouseup 事件,这样当用户释放鼠标按钮时,我们就可以处理选中的文本。

useEffect(() => {
  document.addEventListener('mouseup', handleSelection);
  return () => {
    document.removeEventListener('mouseup', handleSelection);
  };
}, []);

接下来我们来渲染高亮文本划线:

const HighlightedText: React.FC<{rect: any}> = ({ rect }) => {
  return (
    <div
      style={{
        position: 'absolute',
        left: rect.x,
        top: `${rect.y + rect.scrollY}px`,
        width: `${rect.w}px`,
        height: `${rect.h}px`,
        backgroundColor: 'yellow',
        opacity: 0.5,
        pointerEvents: 'none',
      }}
    >

      {rect.text}
    </div>

  );
};

当然我们还需要处理当窗口滚动时的划线位置的动态计算, 我写了一个算法来实时更新位置, 大家可以参考一下:

const resizeWindow = (e: any) => {
    const nw = window.innerWidth;
    const w = memoryManage.get('width');
    const dx =(nw - w) / 2;
    // 踩坑点
    // 百分比计算存在误差, 最好采用像素计算
    setRects([...rects].map((v: any) => {
      const a1 = typeof v.x === 'string' ? parseFloat(v.x) / 100 * w : v.x;
      const a2 = (a1 + dx);
      return {
        ...v,
        x: a2
      }
    }))
    memoryManage.set('width', nw);
  }

  const scrollWindow = (e?: any) => {
    setRects(rects.map((v: any) => {
      return {
        ...v,
        originY: v.originY !== undefined ? v.originY : v.scrollY,
        scrollY: v.originY ? v.originY - window.scrollY : -window.scrollY
      }
    }))
  }

当然还有很多方式可以实现划线评论, 后续我会持续分享和迭代.

聊聊对flowmix/docx文档编辑器的一些迭代

之前我们做的 flowmix/docx 文档编辑器版本主要是为企业提供多模态文档解决方案, 帮助企业把多模态文档编辑器轻松集成到内部项目或者知识库产品中. 这里和大家分享一下目前迭代的功能清单:

11 - 12月对文档引擎的一些迭代规划(包含前后端规划):

  • 支持文档工作流(基于文档内容自动生成可编辑的工作流)

  • 支持高级可视化图表

  • 支持AI侧边栏

  • 支持一键生成PPT

  • 支持多人协同功能

最近会快马加鞭, 做一款类似语雀 & 飞书的文档管理平台, 开放给大家使用. 也会在flowmix视界公众号同步最新的进展:


当然从体验上来讲, 文档还有很多优化的空间, 这块会持续优化和迭代, 并结合业界最佳体验实践, 将文档搭建能力发挥出最大的价值.

编辑器版本体验地址: http://flowmix.turntip.cn/docx

如果你有好的想法和建议, 也欢迎随时留言区交流讨论~

往期精彩:

flowmix/flow, 一款高度可配的可视化流程编辑器
零代码平台创业, 阶段性复盘
从零到一打造面向AI的文档可视化搭建引擎

趣谈前端
徐小夕【知乎专栏作家】掘金签约作者,定期分享前端工程化,可视化,企业实战项目知识,深度复盘企业中经常遇到的500+技术问题解决方案。【关注趣谈前端,前端路上不迷茫】
 最新文章