一个划词提示/高亮的组件

71 阅读6分钟

TL;TD: 链接

截屏2025-05-11 14.17.54.png

需求

本身是一个纯业务的需求,就是给一段文字,例如商品介绍,商品有很多商品属性,然后这些商品属性在商品介绍中往往能找到对应的描述,需要支持用户划词挑选中商品介绍文字的特殊词条,比如包含的大小、品牌和产地等等。

单纯的实现标注功能其实非常简单,比如你只需要给一个输入框,用户根据对应的商品属性,在对应属性的输入框里面复制粘贴就可以,但会有各种各样的问题:

  • 交互方式繁琐,你需要选中以后再复制粘贴,往往信息和标注在不同地方,操作繁琐
  • 展示不友好,难看,在后续有人要查看标注的信息,这种信息+标注割裂的展示方式很难聚合信息
  • 准确性,自己手动填写,很难保证用户输入的准确性,毕竟没有任何限制

方案

综合考虑后,方案定在里划词标注的能力,因为我不太喜欢重复造轮子,优先考虑的还是社区的方案,然后看到了社区的一个库 react-text-annotate,和我想实现的就很像,但是它有几个问题不满足我们需求的场景:

  • 删除操作是点击就删除了,这个交互是不合理的,理想的交互应该是点中选择,然后按 delete/backspace 删除
  • 嵌套标注实现不合理
  • 一些奇怪的 bug

嵌套/交集的问题

我想标注 'who jus released' 和 'released her first' 两个部分的内容,其中 released 是交集部分: 截屏2025-05-11 14.43.22.png 尝试划词得到: 截屏2025-05-11 14.46.19.png release 渲染了两次,总之实现的有问题,对于嵌套和交集的场景基本上支持度为 0。

奇怪的 bug

我也不知道做了什么操作,一下出现了很多重复的 ORG 标注,但我肯定只选中了一次 截屏2025-05-11 14.41.00.png 然后点击删除又出现: 截屏2025-05-11 14.42.21.png

功能支持

考虑自己实现后,需要梳理下我需要实现的组件,应该支持哪些功能:

  • 较为自由的配置,例如颜色是支持配置的
  • 交互是鼠标划词
  • 支持 delete/backspace 快捷键删除
  • 标注支持嵌套和交集
  • 组件支持外界控制和及时的事件回调

样式设计

颜色设计

image.png 颜色主要考虑护眼,一段文字有蛮多标注的时候,深色系的背景色会让人看了会极度不适,所以默认颜色我选取了 ZEBRA MILDLINER 系列的一些颜色。

ZEBRA MILDLINER 是日本斑马(ZEBRA)公司推出的一款双头荧光笔,以其柔和的色调和独特的设计在文具爱好者中广受欢迎。这款荧光笔最大的特点是其温和不刺眼的颜色,非常适合长时间阅读和笔记使用。

重点就是 “这款荧光笔最大的特点是其温和不刺眼的颜色,非常适合长时间阅读和笔记使用”。

嵌套交集时的展示

截屏2025-05-11 14.58.51.png 比如我标注了 cold withwith similar,中间的 with 就是交集的部分,当两种、多种颜色有交集的时候怎么处理,拆分过程就是下图:

image.png

至于为什么要这样拆分,在后续的组件实现中会提到,交集区域的颜色是个数组,为了显示所有的颜色,遍历颜色的时候,数组下标没加1,后续的颜色上下就会扩充一些距离,保证后面的颜色不会被遮挡。

交互设计

其实划词部分没有什么要说明,要考虑的是用户点击删除的交互,尤其是嵌套和交集时候的删除。

普通情况下的点击删除设计

如果用户未选择的情况下,可以给定 0.5 的透明度: 截屏2025-05-11 15.11.22.png

选中后,给定 1 的透明度,且高亮后面的注释部分: 截屏2025-05-11 15.11.53.png

嵌套/交集的删除设计

正常的一个注解内容可以分为两个部分: text + hint

截屏2025-05-11 15.14.09.png

hint 部分是唯一的,text 是可能是交集、嵌套的区域,那么对于点击删除的情况可以做以下区分:

  • 点击 hint 部分,那一定是只能选中唯一的注解内容,处理就是正常的选中单个注解
  • 点击 highlight 的 text 部分,需要判断该 text 是否是交集的区域,如果是交集的区域,那么它是那几个注解的交集,那么点击该 text 的时候,所有包含该 text 的注解都应该被选择

如下图所以点击 with 的时候,包含 with 的两个注解内容都被选中: 截屏2025-05-11 15.18.29.png

组件设计

一般来说,如果我们按照惯性,很容易就想到,对于高亮的设计,可以通过特殊的标签实现,类似于标注了 cold with similar 中间的 with 部分:

cold <highlight>with</highlight> similar

但对于交集来说,这样设计是行不通的,因为标签这种设计不存在 <highlight1>cold <highlight2>with</highlight1> similar</highlight2>,简单描述就是 div 的闭合标签一定是 </div> 不可能是 </span>highlight2 的闭合标签不可能是 </highlight1>,所以这种类标签的设计是行不通的。除非你想得到下面的内容: 截屏2025-05-11 14.46.19.png

所以比较好的方式还是拆分,如何拆分,大概是这样的结构:

image.png

这样您就能得到一个扁平的数据结构了,众所周知,扁平数据结构的好处,这种扁平结构使得数据的增删改查操作非常直接,不需要处理复杂的嵌套关系。

Selection API

该 API 没有特殊的介绍意义,更多的信息还是推荐看一下文档。 特意把该 API 拿出来提一嘴,是因为有个地方需要特殊处理一下,startOffsetendOffset 的计算逻辑不是基于所谓的 root 节点:

image.png 所以你需要记录每个节点实际的 start 和 end 位置,计算的时候就不需要很麻烦的计算实际选中内容的 start 和 end 位置,需要注意 start 和 end 可能是“跨节点”的,endOffset 还是需要单独计算,从而获取到所谓的 range数据。

截屏2025-05-11 15.39.06.png

总结

这其实是一个很简单的组件,我的代码写的挺烂的,就不想拿出来解析了,把简单的分析的思路说一下,自己实现起来不会太复杂,感谢阅读。