如何实现鼠标标记和标记渲染功能

背景需求

1、首先通过接口拿到文本和nlp识别后的标记信息

2、渲染标记到文本中

3、文本区可鼠标滑动添加标记类型,刷新后同样显示对应标记

分析上以上需求,不难发现关键点在于2和3。

当然这只是需求简化后的样子,真实需求取决于业务,我们只讨论内容只有文本的情况,其他情况类似

第二点渲染标记我最先想到的是后端提供标记文本的位置信息,直接插入一个新标签比如mark来包裹被标记的文本然后替换原来的文本。这么看也不是太难实现😃

基于第二点的分析,第三点鼠标选择文本肯定也要提供被选中文本的位置信息给后端从而实现标记持久存在

鼠标选中

正好浏览器也提供了鼠标选择对应的API——getSelection

同时也在网上进行了调研,所有提供鼠标选择功能的组件或者三方库都是基于getSelection来实现的。

那好,先基于该API实现一个基本的文本选择功能

window.addEventListener(‘mouseup’, function () {
let selection = window.getSelection()
})

一个最基础的文本选择功能就完成了。我们打印下selection对象产看下它里面都有些什么属性

null

其中baseOffset和fouceOffset一般表示鼠标选择开始和结束时的位置信息。进一步我们可以拿到选择的range对象

window.addEventListener(‘mouseup’, function () {
let selRange = window.getSelection().getRangeAt(0) // 默认是0,除了火狐浏览器可多选外,其他浏览器只能单选
})

再打印下range对象,可以看到直接就有start和end的偏移量,而startContainer和endContainer的内容是起点文本和终点文本所在的父级文本。

null

好了,文本位置信息可以拿到问题就解决了一大半。不过考虑到我们是在已经包含标记的文本中进行标记,还需要验证下已有标记的文本选择位置是否有变化(因为插入了标记标签改动了文本结构)。

给测试文本加上一段标签,如下,再次选中跟之前一样的文本打印range信息

null

还真的不出所料,位置信息发生了变化,查看startContainer可以看到父文本是从标签后第一个字符开始的,而我们需要的是从整段文本的第一个字符开始

null

这会导致生成标签出现偏移,不符合我们的需求。因此需要一个方法来将文本节点内偏移量“翻译”为其对应的父节点内部的总体文本偏移量 ——参考 web-highlight

经过验证web-highlight是可以满足我们的需求的。

实际开发中还要用到父节点的序号和父节点的标签名,我们业务中的标签名是固定的可以不用特殊处理,其他场景下就需要遍历dom

文本标记

只要有了标记的起止位置和所处父节点序号我们就可以找到对应的标记文本,生成标记节点包裹对应文本。自己实现和使用三方库都能满足要求。

结论

当然,现在一切都只是试验阶段,后端还没提供数据无法做进一步验证。项目紧急的情况优先使用三方件来快速实现功能,后期需求优化再根据需求判断三方件是否满足。不满足也可以参考其序列化的思路重新实现。

后续

不管是三方件还是我们自己实现的高亮都有个共同点——会破坏文档的dom结构也就是要插入比如mark的标签。而我们需求中有一项是文本内容可再编辑,如果标记高亮破坏了dom结构再编辑就不友好了,并且也对全文搜索高亮不友好。后来发现语雀是可以评论的,评论的文本也是会被标记出来的。更神奇的是它没有改变原文本dom结构。很好,那就研究下它到底使用了什么魔法。

null

逐步排除文本区dom,果然有意外发现。有个canvas结构正好覆盖再文本区上。好家伙,看到canvas就要凝神——毕竟canvas通常能实现一些意想不到的功能。

null

删掉canvas结构后刚添加的两个标记也消失了。

null

果然是万能的canvas。

如此大体思路就是:生成一个 canvas 元素,让 canvas 元素与需要划词高亮功能的文本容器元素等宽高,并且重叠在文本容器上,划词的时候获取划词区域的文本节点相对于文本容器的位置信息,然后通过这些位置信息进行高亮背景的渲染。

是否采用该方案看项目的进一步需求,目前的标记高亮实现确实有些问题。

插句题外话,全文搜索高亮的常见方案也是插入标签,不过影响不大,因为并不是永久改变dom,取消搜索就会还原称原文本。浏览器 ctrl+f 原生的搜索高亮也没有插入标签也没用到canvas,更没有多余的dom结构,这就很费解???

如果能实现跟原生浏览器一样的功能就完美了,后面再研究吧。

再续

按照之前的调研结果开始用canvas重构标记功能,啪啪啪一通完成基本功能,直接运用到项目中运行。计划很完美,第一步就失败了😫

null

666,直接给canvas干崩溃了🤣。在实际项目中直接加载了一本书全部内容,所以文本区完整高度很大,而canvas是直接覆盖在文本区上的, canvas 元素高度太大导致内存占用超出问题。无语啊,居然会遇到这种问题。

如何解决???好像可以通过多个canvas依次排列来解决。在不断地调试语雀源码后总算找到了它关于canvas的相关实现,确实是通过多个canvas来解决高度太大的问题。

null

yes,已经看到了胜利的曙光。接下来按部就班的实现对应的功能就行。