用CSS自定义高亮API的方法——在网络上高亮显示文本范围的未来

707 阅读10分钟

在软件中对文本范围进行造型是一件非常有用的事情。值得庆幸的是,我们有CSS自定义高亮API可以期待,因为它代表了网络上文本范围样式设计的未来。

Animation screenshot of the CSS Custom Highlight API demo.

举个例子:如果你曾经使用过Google Docs、Word或Dropbox Paper等文本编辑软件,你会发现它们会检测拼写和语法错误,并在其下方显示漂亮的小斜线以吸引注意力。像VS Code这样的代码编辑器对代码错误也有同样的作用。

突出显示文本的另一个非常常见的用例是搜索和突出显示,即给你一个文本输入框,在其中输入内容,搜索页面上的匹配结果,并突出显示它们。现在就试试在你的网络浏览器中按下Ctrl/+F ,然后输入本文中的一些文字。

浏览器本身经常处理这些风格化的情况。可编辑的区域(如<textarea> )会自动得到拼写的斜线。查找命令会自动高亮显示找到的文本。

但是,当我们想自己做这种类型的造型时,怎么办?长期以来,在网络上这样做一直是一个常见的问题。它可能已经花费了许多人的时间,而不是它应该有的。

这不是一个简单的问题,要解决这个问题。我们不只是用一个<span> ,用一个类来包裹文本,然后应用一些CSS。事实上,这需要能够在任意复杂的DOM树上正确地突出多个范围的文本,并可能跨越DOM元素的边界。

对此有两种常见的解决方案,包括。

  1. 塑造文本范围的伪元素,和
  2. 创建你自己的文本高亮系统。

我们先回顾一下它们,然后再看看即将推出的可以改变这一切的CSS自定义高亮API。但如果你是

潜在的解决方案#1:可样式化的文本范围

最著名的可样式化的文本范围可能是用户选择。当你用你的指向性设备在网页中选择一段文字时,一个 Selection对象被自动创建。事实上,现在就试试在这个页面上选择文本,然后在DevTools控制台中运行document.getSelection() 。你应该看到关于所选文本的位置信息。

DevTools window showing the position of the current selection in the console.

事实证明,你也可以从JavaScript中以编程方式创建一个文本选择。这里有一个例子。

// First, create a Range object.
const range = new Range();

// And set its start and end positions.
range.setStart(parentNode, startOffset);
range.setEnd(parentNode, endOffset);

// Then, set the current selection to this range.
document.getSelection().removeAllRanges();
document.getSelection().addRange(range);

拼图的最后一块是对这个范围进行样式设计。CSS有一个伪元素叫做 ::selection来做这件事,而且所有的浏览器都支持它。

::selection {
  background-color: #f06;
  color: white;
}

下面是一个使用这种技术的例子,它可以一个接一个地突出显示一个页面中的所有字。

CodePen嵌入回落

::selection 伪元素的基础上,还有一些其他的伪元素。

  • ::target-text 选择在支持滚动到文本功能的浏览器中被滚动到的文本。(MDN)
  • ::spelling-error 选择被浏览器标记为含有拼写错误的文本。(MDN)
  • ::grammar-error 选择被浏览器标记为包含语法错误的文本。(MDN)

不幸的是,浏览器在这里的支持不是很好,尽管这些范围在它们自己的权利中是有用的,但它们不能被用来为自定义的文本样式--只能是浏览器预设的样式

因此,用户文本选择是很好的,因为它相对简单,不会改变页面的DOM。事实上,Range 对象本质上是页面中段的坐标,而不是需要创建才能存在的HTML元素。

然而,一个主要的缺点是,创建一个选择会重置用户已经手动选择的东西。试着在上面的演示中选择文本来测试这一点。你会看到只要代码把选择移到别的地方,它就会消失。

潜在的解决方案#2:自定义高亮显示系统

如果使用Selection 对象对你来说是不够的,这第二个解决方案几乎是你唯一能做的事情。这个解决方案围绕着自己做所有的事情,使用JavaScript在DOM中插入新的HTML元素来显示你想要的高亮。

不幸的是,这意味着需要编写和维护更多的JavaScript,更不用说它迫使浏览器在高亮显示改变时重新创建页面的布局。此外,还有一些复杂的边缘情况,例如,当你想突出显示一段跨越多个DOM元素的文本时。

Illustration showing a line of HTML with an emphasis element and a strong element with a bright yellow highlight running through them.

有趣的是,CodeMirrorMonaco(支持VS Code的JavaScript文本编辑器库)有他们自己的高亮逻辑。他们使用的方法略有不同,高亮部分包含在DOM树的一个单独部分。文本行和高亮段在DOM的两个不同的地方被渲染,然后相互定位。如果你检查包含文本的DOM子树,就不会有高光。这样,高光部分可以重新渲染,而不会影响到文本行,也不必在其中引入新的元素。

总的来说,感觉缺少一个由浏览器驱动的高亮显示功能。一些能帮助解决所有这些缺点的东西(不干扰用户的文本选择,支持多选,代码简单),并且比定制的解决方案更快。

幸运的是,这正是我们在这里要讨论的问题!

进入CSS自定义高亮API

CSS自定义高亮API是一个新的W3C规范(目前处于工作草案状态),它使得从JavaScript中对任意的文本范围进行样式化成为可能这里的方法与我们之前回顾的用户文本选择技术非常相似。它为开发者提供了一种方法,可以从JavaScript中创建任意的范围,然后用CSS对其进行样式设计。

创建文本范围

第一步是创建你想突出显示的文本范围。 Range在JavaScript中完成。所以,就像我们在设置当前选择时做的那样。

const range = new Range();
range.setStart(parentNode, startOffset);
range.setEnd(parentNode, endOffset);

值得注意的是,如果作为第一个参数传递的节点是一个文本节点,setStartsetEnd 方法的工作方式是不同的。对于文本节点,偏移量与节点中的字符数相对应。对于其他节点,偏移量对应于父节点中的子节点的数量。

另外值得注意的是,setStartsetEnd 并不是描述一个范围开始和结束的唯一方法。看看Range 类上的其他方法,看看其他选项。

创建亮点

第二步是创建 Highlight对象,为上一步创建的范围。一个Highlight 对象可以接收一个或多个Ranges。因此,如果你想以完全相同的方式突出显示一堆文本,你可能应该创建一个Highlight 对象,并用所有对应于这些文本的Ranges来初始化它。

const highlight = new Highlight(range1, range2, ..., rangeN);

但是你也可以根据你的需要创建尽可能多的Highlight 对象。例如,如果你正在建立一个协作式文本编辑器,每个用户得到不同的文本颜色,那么你可以为每个用户创建一个Highlight 对象。然后,每个对象都可以有不同的风格,我们接下来会看到。

注册高光对象

现在,高光对象本身并没有什么作用。它们首先需要在所谓的高光注册表中注册。这是通过使用CSS高光API完成的。这个注册表就像一张地图,你可以通过给它们命名来注册新的高光,也可以删除高光(甚至清除整个注册表)。

下面是如何注册一个高光的方法。

CSS.highlights.set('my-custom-highlight', highlight);

其中my-custom-highlight 是你选择的名称,highlight 是在上一步中创建的Highlight 对象。

亮点的造型

最后一步是对已注册的高亮部分进行实际的样式设计。这是用新的CSS ::highlight()伪元素来完成,使用你在注册Highlight 对象时选择的名称(在我们上面的例子中是my-custom-highlight )。

::highlight(my-custom-highlight) {
  background-color: yellow;
  color: black;
}

值得注意的是,就像::selection ,有一个CSS属性子集只能用于::highlight() 伪元素。

更新高亮部分

有多种方法可以更新页面上的高亮文本。

例如,你可以用CSS.highlights.clear() ,完全清除高亮注册表,然后再从头开始。或者,你也可以更新底层范围,而不必全部重新创建任何对象。为此,再次使用range.setStartrange.setEnd 方法(或其他任何Range 方法),高光部分就会被浏览器重新绘制出来。

但是,Highlight 对象的工作方式就像一个 JavaScript Set,所以这意味着你也可以用highlight.add(newRange) 向现有的Highlight 添加新的Range 对象,或者用highlight.delete(existingRange) 删除一个Range

第三,你也可以从CSS.highlights 注册表中添加或删除特定的Highlight 对象。由于这个API的工作方式像一个JavaScript Map的,你可以 setdelete来更新当前注册的Highlights。

浏览器支持

CSS自定义高亮显示API的规范比较新,它在浏览器中的实现还不完整。因此,尽管这将是对网络平台的一个非常有用的补充,但它还没有完全准备好用于生产。

目前,微软Edge团队正在Chromium中实现CSS自定义高亮API。事实上,该功能现在已经可以通过启用实验性网络平台功能标志(在about:flags )在Canary版本中使用。目前,还没有确切的计划说明该功能何时会在Chrome、Edge和其他基于Chromium的浏览器中出现,但它已经非常接近了。

这个API在Safari 99+中也被支持,但在一个实验标志后面(开发→实验功能→突出显示API),而且界面有一点不同,它使用了 StaticRange对象来代替。

火狐浏览器还不支持该API,不过你可以阅读Mozilla关于它的立场,以了解更多信息。

结束语

那么,这个新的浏览器提供的高亮显示API真的值得吗?是的,值得。

首先,即使CSS自定义高亮API一开始看起来有点复杂(即必须创建范围,然后高亮,然后注册它们,最后给它们定型),它仍然比创建新的DOM元素并将它们插入正确的位置要简单得多。

更重要的是,浏览器引擎可以非常、非常快地对这些范围进行样式设计。

之所以只允许CSS属性的一个子集与::highlight() 伪元素一起使用,是因为这个子集只包含浏览器可以非常有效地应用的属性,而不必重新创建页面的布局。通过在页面中插入新的DOM元素来突出文本范围,需要引擎做更多的工作。

但不要相信我的话。从事API工作的Fernando Fiori创建了这个漂亮的性能比较演示。在我的电脑上,CSS自定义高亮API的执行速度平均是基于DOM的高亮的5倍。

随着Chromium和Safari实验性支持的出现,我们已经接近可以在生产中使用的东西了。我已经迫不及待地希望各浏览器能够持续地支持自定义高亮API,并看看它将释放出哪些功能