likes
comments
collection
share

浏览器划词高亮实践与方案探讨

作者站长头像
站长
· 阅读数 17

浏览器划词高亮实践与方案探讨 本文介绍后两种方案的实现,第一种方法已经有成熟的文档和工具了。

获取划词文字

step by step:

  1. document.getSelection
  2. selection.getRangeAt(0)
  3. range.getClientRects() /range.getBoundingClientRect()

通过在页面上选择一部分文字完成划词,监听指定区域内鼠标抬起事件,事件触发后,可以通过document.getSelection 获取用户选区。当然也有没有任何选区,只是单纯的鼠标点击抬起的情况,可以通过判断selection的属性将两者区别开。

通过selection.getRangeAt(0)获取选区的range,参数输入0是因为只需要第一个选区,可以通过selection.rangeCount查看当前有多少个选区。

根据selection和range的一些属性比较,可以判断选区是否跨越了节点,选区是从左往右还是从右往左。对于selection和range的属性,可以参考MDN或这篇文章JavaScript中对光标和选区的操作

这里简单说下,需要限制选区是否跨节点,可以通过range.commonAncestorContainer 属性判断range的节点和selection的节点是否为同一个,如果没有跨节点,range.commonAncestorContainer应该是文本节点。如果限制选区跨节点,需要判断一下是从左到右选区(selection.anchorNode===range.startContainer)还是从右到左选区(selection.anchorNode!==range.startContainer),重新生成一个range。

获取到一个合法的range后,考虑到选区有可能出现换行,使用getClientRects()获取选区的所有位置信息,是个list,包含了每一行的位置信息,在使用canvas方法会用到,如果使用custom highlight 方法的话,getBoundingClientRect() 就够了。

添加划词区域样式

两种不同的方法添加划词区域的样式,它们使用的基础数据是相同的(上文准备的range)。

custom highlight

custom highlight是CSS自定义高亮API。可以自定义文档任意文本范围的样式,类似与选区时,通过::selection 伪类,可以自定义选区样式一样。 这个特性在chorme内核的浏览器105版本后实装,在safari 99以上的版本可以通过实验特性打开,但火狐目前还不支持。所以使用前,最好判断一下浏览器支不支持CSS.highlights

使用方法很简单:

const HighlightObj = new Highlight(range1,range2...)
CSS.highlights.set("highlight-classname",HighlightObj)

我们创建一个list存放每次划词生成的range,并在划词后用上面的方法设置选区样式。

const rangeArr = []
rangeArr.push(range)
CSS.highlights.set('highlight-classname',new Highlight(...rangeArr))

别忘了css

::highlight(highlight-classname){
  background-color: #98e9e9dc;
}

浏览器划词高亮实践与方案探讨

canvas

step by step:

  1. 接上文的 range.getClientRects()
  2. parent.getBoundingClientRect()
  3. ctx.fillRect()

使用canvas就相对来说麻烦一点了。基本原理是这样的,在划词范围的下方覆盖一层canvas,当鼠标抬起完成划词时,在canvas的对应区域绘制填充颜色的长方形。

麻烦的地方有两点: 1.如何将划词区域的坐标与canvas对齐 2.canvas与划词范围的层级关系

假设划词范围的html结构是这样的:

<划词范围>
	<文本内容>
	<canvas>
</划词范围>

看看这张图: 红框范围是canvas,与划词范围一致大小。 蓝框是划词区域 黄框是划词区域相对于划词范围的位置,也是canvas要绘制填充长方形的地方。

浏览器划词高亮实践与方案探讨 这是最基础的样式分析,直接蓝框的left/top减去红框的left/top就可以了。

但如果html结构是这样的呢?

<卡片>
	<卡片标题>
	<乱七八糟的其他组件>
	<文本内容>
	<canvas>
</卡片>

卡片内的区域都可以被划词,但……如果组件自带背景色,就会遮住canvas的图像,将canvas置于组件上方会遮住原先的文字。所以这种方法要求划词范围的背景色为透明,如果有背景色样式的需求,只能通过其他手段(伪元素,增加绝对定位的div等)实现。 而且对样式也有局限性,划词部分高亮很简单,添加一个填充长方形就可以了,但如果需要将划词部分的字体颜色也改变呢,基础的canvas就做不到了。

在页面加载完成后,设定canvas的宽高并获取ctx:

    const parentBounding = card.getBoundingClientRect()

    innerCanvas.width = parentBounding.width

    innerCanvas.height = parentBounding.height

    ctx = innerCanvas.getContext("2d")

注意,当card的宽高发生改变时,需要清除canvas画布,同步更改canvas宽高并重新绘制

鼠标抬起,完成划词后,获取新鲜的range

    const range  = selection.getRangeAt(0)

获取range相对于canvas的位置,在canvas对应位置绘制填充长方形

const boundings = range.getClientRects()

const suppleBoundings = innerCanvas.getBoundingClientRect()

for (const bounding of boundings) {

        const currentRect = {

            width: bounding.width,

            height: bounding.height,

            left: bounding.left - suppleBoundings.left,

            top: bounding.top - suppleBoundings.top

        }

        const {left,top,width,height} = currentRect

        ctx.fillStyle = 'red'

        ctx.fillRect(left, top, width, height)

    }

浏览器划词高亮实践与方案探讨

点击和回显划词区域

回显不多说了,无论选择的是上面哪种方法,所有的选区range信息都存在rangeArr里。 如果选择的是custom highlight方法,重新执行一次CSS.highlights.set('highlight-classname',new Highlight(...rangeArr)) 即可。 如果选择的是canvas方法,遍历rangeArr,一次执行绘图方法即可。

如果需要点击划词区域,出现popover。建议单独写一个全局绝对定位的组件。鼠标抬起时,获取当前鼠标的坐标。判断rangeArr中是否有符合位置的range,若有,在指定位置显示popover。 可以在向rangeArr添加range的时候,就存一份range的位置信息

const {left,right,top,bottom} = range.getBoundingClientRect()
const rangeInfo = {
    BoundingXRange:[left,right],
    BoundingYRange:[top,bottom],
    rangeText:'...'
}

文本可编辑的情况

在找其他高亮方案参考的时候,发现都提出了一个问题,一旦文本内容发生改变,划词区域就和文字对不上了。

想了一段时间,提供一个思路,之所以说是思路,是因为我还没写出来……

既然允许文本在任意内容编辑,而我们又希望能最大程度保留划词区域,只需要规定规定以下在什么情况下,保留/更新片段即可。

假设段落为今天天气很好,可以出去玩。 ,其中一个片段为今天天气很好,此片段可以最小化分割为['今','天','天','气','很','好']

看看这几个例子:

  • 今天天气很好 -> 今天天气不好(头尾都没被替换,更新后的片段为 今天天气不好
  • 今天天气很好 -> 今天天气超级无敌棒 (尾被替换,更新后的片段为 今天天气
  • 今天天气很好 -> 阿巴阿巴好(头被替换了,但尾没有)此时的片段只剩下
  • 今天天气很好 -> 一张可湿水面纸 (头尾都被替换了,舍弃该标签)

那么规则我们可以定为:当文档内容发生改变时,先确定头尾是否被替换。

  • 头尾没被替换,更新片段
  • 头被替换了,获取剩余符合原先片段最大的子片段,更新片段
  • 尾被替换了,获取剩余符合原先片段最大的子片段,更新片段
  • 头尾都被替换了,舍弃这个标签

参考资料

  1. 如何用JS实现“划词高亮”的在线笔记功能
  2. 原生 CSS Custom Highlight 终于来了~
  3. 前端实现搜索并高亮文字的两种方式
  4. 纯 JS 实现语雀的划词高亮功能
  5. JavaScript中对光标和选区的操作
转载自:https://juejin.cn/post/7234779572759625788
评论
请登录