详解实现一个通用的“划词高亮”在线笔记功能(2)

“中间”加上引号是因为,在视觉上这些节点是位于首尾之间的,但由于 DOM 不是线性结构而是树形结构,所以这个“中间”换成程序语言,就是指深度优先遍历时,位于首尾两节点之间的所有文本节点。DFS 的方法有很多,可以递归,也可以用栈+循环,这里就不赘述了。

需要提一下的是,由于我们是要为文本节点添加高亮背景,因此在遍历时只会收集文本节点。

if (curNode.nodeType === 3) { selectedNodes.push(curNode); }

3.2. 如何为文本节点添加背景色?

这一步本身并不困难。在上一步的基础上,我们已经选出了所有被用户选中的 文本节点(包括拆分后的首尾节点)。对此,一个最直接的方法就是为其“包裹上”一个带背景样式的元素。

具体的,我们可以给每个文本节点外加上一个 class 为 highlight 的 <span> 元素;而背景样式则通过 CSS .highlight 选择器设置。

// 使用上一步中封装的方法获取选区内的文本节点 const nodes = getSelectedNodes(start, end); nodes.forEach(node => { const wrap = document.createElement('span'); wrap.setAttribute('class', 'highlight'); wrap.appendChild(node.cloneNode(false)); node.parentNode.replaceChild(wrap); });

.highlight { background: #ff9; }

这样就可以给被选中的文字添加一个“永久”的高亮背景了。

p.s. 选区的重合问题

然而,文本高亮里还有一个比较棘手的需求 —— 高亮区域的重合。举个例子,最开始的演示图(下图)里,第一个高亮区域和第二个高亮区域之间存在重叠部分,即“本区域高”四个字。

详解实现一个通用的“划词高亮”在线笔记功能

这个问题目前来看似乎还不是问题,但在结合下面要提到的一些功能与需求时,就会变成非常麻烦,甚至无法正常运行(一些开源库这块处理也不尽如人意,这也是没有选择它们的一个原因)。这里简单提一下,具体的情况我会放到后续对应的地方再详细说。

4. 如何实现高亮选区的持久化与还原?

到目前我们已经可以给选中的文本添加高亮背景了。但还有一个大问题:

想象一下,用户辛辛苦苦划了很多重点(高亮),开心地退出页面后,下次访问时发现这些都不能保存时,该有多么得沮丧。因此,如果只是在页面上做“一次性”的文本高亮,那它的使用价值会大大降低。这也就促使我们的“划词高亮”功能要能够保存(持久化)这些高亮选区并正确还原。

持久化高亮选区的核心是找到一种合适的 DOM 节点序列化方法。

通过第三部分可以知道,当确定了首尾节点与文本偏移(offset)信息后,即可为其间文本节点添加背景色。其中,offset 是数值类型,要在服务器保存它自然没有问题;但是 DOM 节点不同,在浏览器中保存它只需要赋值给一个变量,但想在后端保存所谓的 DOM 则不那么直接了。

4.1 序列化 DOM 节点标识

所以这里的核心点就是找到一种方法,能够定位 DOM 节点,同时可以被保存成普通的 JSON Object,用以传给后端保存,这个过程在本文中被称为 DOM 标识 的“序列化”。而下次用户访问时,又可以从后端取回,然后“反序列化”为对应的 DOM 节点。
有几种常见的方式来标识 DOM 节点:

使用 xPath

使用 CSS Selector 语法

使用 tagName + index

这里选择了使用第三种方式来快速实现。需要注意一点,我们通过 Selection API 取到的首尾节点一般是文本节点,而这里要记录的 tagName 和 index 都是该文本节点的父元素节点(Element Node)的,而 childIndex 表示该文本节点是其父亲的第几个儿子:

function serialize(textNode, root = document) { const node = textNode.parentElement; let childIndex = -1; for (let i = 0; i < node.childNodes.length; i++) { if (textNode === node.childNodes[i]) { childIndex = i; break; } } const tagName = node.tagName; const list = root.getElementsByTagName(tagName); for (let index = 0; index < list.length; index++) { if (node === list[index]) { return {tagName, index, childIndex}; } } return {tagName, index: -1, childIndex}; }

通过该方法返回的信息,再加上 offset 信息,即定位选取的起始位置,同时也完全可发送给后端进行保存了。

4.2 反序列化 DOM 节点

基于上一节的序列化方法,从后端获取到数据后,可以很容易反序列化为 DOM 节点:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/a3f3f57670a6765e5bd81812c0fba8f3.html