- offset: range.startOffset
- };
- const end = {
- node: range.endContainer,
- offset: range.endOffset
- };
Range 对象包含了选区的开始与结束信息,其中包括节点(node)与文本偏移量(offset)。节点信息不用多说,这里解释一下 offset 是指什么:例如,标签
这是一段文本的示例
,用户选取的部分是“一段文本”这四个字,这时首尾的 node 均为 p 元素内的文本节点(Text Node),而 startOffset 和 endOffset 分别为 2 和 6。
2)首尾文本节点拆分
理解了 offset 的概念后,自然就发现有个问题需要解决。由于用户选区(selection)可能只包含一个文本节点的一部分(即 offset 不为 0),所以我们最后得到的用户选区所包含的节点里,也只希望有首尾文本节点的这“一部分”。对此,我们可以使用 .splitText() 拆分文本节点:
- // 首节点
- if (curNode === $startNode) {
- if (curNode.nodeType === 3) {
- curNode.splitText(startOffset);
- const node = curNode.nextSibling;
- selectedNodes.push(node);
- }
- }
- // 尾节点
- if (curNode === $endNode) {
- if (curNode.nodeType === 3) {
- const node = curNode;
- node.splitText(endOffset);
- selectedNodes.push(node);
- }
- }
以上代码会依据 offset 对文本节点进行拆分。对于开始节点,只需要收集它的后半部分;而对于结束节点则是前半部分。
3)遍历 DOM 树
到目前为止,我们准确找到了首尾节点,所以下一步就是找出“中间”所有的文本节点。这就需要遍历 DOM 树。
“中间”加上引号是因为,在视觉上这些节点是位于首尾之间的,但由于 DOM 不是线性结构而是树形结构,所以这个“中间”换成程序语言,就是指深度优先遍历时,位于首尾两节点之间的所有文本节点。DFS 的方法有很多,可以递归,也可以用栈+循环,这里就不赘述了。
需要提一下的是,由于我们是要为文本节点添加高亮背景,因此在遍历时只会收集文本节点。
- if (curNode.nodeType === 3) {
- selectedNodes.push(curNode);
- }
3.2. 如何为文本节点添加背景色?
这一步本身并不困难。在上一步的基础上,我们已经选出了所有被用户选中的 文本节点(包括拆分后的首尾节点)。对此,一个最直接的方法就是为其“包裹上”一个带背景样式的元素。
具体的,我们可以给每个文本节点外加上一个 class 为 highlight 的 元素;而背景样式则通过 CSS .highlight 选择器设置。
- // 使用上一步中封装的方法获取选区内的文本节点
- const nodes = getSelectedNodes(start, end);