自定义任意范围高亮 -- CSS Custom Highlight API详解

高亮(Highlight)是一种改变文本外观以突出显示特定内容的技术,有助于提高文本的可读性和信息传达效果。通常用于强调或突出显示文本中的范围内容,帮助读者快速识别重要内容。高亮通常通过改变文本的颜色、背景色或字体样式(如加粗或斜体)来实现。比如在复制文本内容时,选择的文本默认会出现蓝色背景跟字体变白,这就是一种高亮效果。
CSS Custom Highlight API可以通过Javascript创建任意文本范围,并用CSS对文本范围进行样式化以达到高亮效果。目前该API处于实验阶段,但是部分较新的主流浏览器都已经支持了:
选择与范围
在正式学习之前,先来学习一下什么是选择(Selection)和范围(Range)。主要是学习范围,选择跟范围有一些联系,可以了解一下。
范围(Range)
Range
表示包含节点和文本节点部分的文档片段。
实际上范围有多种,Range
是最常见的一种范围类;还有一种StaticRange
类,它与Range
类似,但是它是不可变的范围,一旦创建就不会随着文档的变化而变化,因此性能会比Range
更好。
Range
和StaticRange
都继承于AbstractRange
,是定义所有DOM范围类型的抽象基类。但是它的浏览器兼容性要比Range
和StaticRange
差一点。
由于它们都很类似,这里我们只介绍Range
。
构造函数:
new Range()
// 下面这两种方式也可以创建和获取Range对象。
document.createRange()
document.getSelection().getRangeAt(index)
创建了Range
对象之后,就可以通过range.setStart(node, offset)
和range.setEnd(node, offset)
方法来创建起始点(包含)和结束点(不包含)。
其中,node
参数可以是文本节点或者元素节点,对应offset
参数有两种情况:
- 如果
node
是文本节点,那么offset
就是一个文本位置索引(从0
开始)。🌰:<!-- p唯一子节点是文本节点 --> <p id="test">test</p> <script> const range = new Range(); range.setStart(test.firstChild, 2); range.setEnd(test.firstChild, 4); console.log(range.toString()) // st </script>
- 如果
node
是元素节点,则offset
表示其任意子节点的编号索引,每一个子节点为一项。🌰:<!-- 文本节点“这是”为第0项,i元素为第1项,b元素为第二项 --> <p id="test">这是<i>第一个</i><b>段落</b></p> <script> const range = new Range(); range.setStart(test, 0); range.setEnd(test, 2); console.log(range.toString()) // 这是第一个 </script>
注意:起始点和结束点的node
可以不一样,但是结束点必须在起始点之后。
<p id="test1">这是第一个段落</p>
<p id="test2">这是第二个段落</p>
<script>
const range = new Range();
// 如果将test1和test2互换一下结果是空字符串,因为起始点在结束点之前
range.setStart(test1.firstChild, 5);
range.setEnd(test2.firstChild, 2);
/*
* 打印:段落
* 这是
*/
console.log(range.toString())
</script>
其它部分属性/方法
属性/方法 | 值/参数 | 简介 |
---|---|---|
collapsed | Boolean | 只读属性。用于获取范围的起始点和结束点是否在同一位置。 |
startContainer | DOM Node 对象 | 只读属性。表示范围的起始节点,相当于elenemt.firstChild 。 |
endContainer | DOM Node 对象 | 只读属性。表示范围的结束节点,相当于elenemt.lastChild.firstChild 。 |
startOffset | Number | 只读属性。表示范围在startContainer 中的起始点。相当于range.setStart(node, offset) 方法中的offset 参数。 |
endOffset | Number | 只读属性。表示范围在endContainer 中的结束点。相当于range.setEnd(node, offset) 方法中的offset 参数。 |
commonAncestorContainer | DOM Node 对象 | 范围内的所有节点中最近的共同祖先节点。 |
collapse(toStart) | toStart :Boolean。1. 值为ture 时表示将结束点的位置折叠到起始点(即end = start )。2. 值为false 时表示将起始点折叠到起始点(即start = end )。 | 折叠范围。 |
cloneContents() | - | 复制范围中的内容,并作为DocumentFragment 对象返回。 |
cloneRange() | - | 创建一个具有相同起点和终点的新范围 |
deleteContents() | - | 从文档中删除范围中的内容 |
extractContents() | Boolean | 从文档中删除范围中的内容,并将删除的内容作为DocumentFragment 返回。 |
setEndBefore(referenceNode) | referenceNode :DOM Node 对象。表示范围结束之前的节点 | 设置范围一个节点的结束位置。 |
setEndAfter(referenceNode) | referenceNode :DOM Node 对象。表示范围结束之后的节点 | 设置范围一个节点的结束位置。 |
setStartBefore(referenceNode) | referenceNode :DOM Node 对象。表示范围起始之前的节点 | 设置范围一个节点的结束位置。 |
setStartAfter(referenceNode) | referenceNode :DOM Node 对象。表示范围起始之后的节点 | 设置范围一个节点的结束位置。 |
surroundContents(node) | node :DOM Node 对象。 | 使用node 将所选范围中的内容包裹起来。这样该范围必须包含其中所有元素的开始和结束标签才有效。 |
toString() | - | 返回范围中的文本。 |
startContainer
、endContainer
、startOffset
、endOffset
和commonAncestorContainer
的关系如下:
还有其它用于比较范围的方法比较少用,且与本文关系不大,不过多说明。
选择(Selection)
Range
用于创建范围,但是它并不可视化的,而Selection
对象表示用户选择的文本范围或插入符号的当前位置,这是可视化的。
注意:通常浏览器只支持一个选择,部分浏览器如Firefox可以有多个选择;而范围不受数量限制。
当用户进行选择操作之后,可以通过以下方法获取用户选择的范围内容:
window.getSelection()
// or
document.getSelection()
🌰:
// 在进行选择之后,点击按钮即可查看选择的内容
<p>这是一个段落</p>
<button>查看选择内容</button>
<script>
const btn = document.querySelector('button')
btn.addEventListener('click', () => {
alert(document.getSelection())
})
</script>
部分属性/方法
由于属性/方法会涉及到一些相关术语,这里先来学习一下:
- 锚点(anchor):用户在选择的时候,可以从左到右或者从右到左选择,而锚点表示用户选择内容时的起点,并且这个位置在取消选择到下一次选择之前都不会改变。这也是选择和范围的一大区别:
- 可编辑元素(editing host):表示如设置了
contenteditable
的HTML元素,或启用了designMode
的文档的HTML子元素。 - 选取焦点(focus of a selection):与锚点类似,但是它表示用户停止选择的位置,并且会随着用户的更改(比如移动光标)而变化,此时焦点是移动的选择的结束。
- 范围(range):就是我们上面学习的
Range
。
属性/方法 | 值/参数 | 简介 |
---|---|---|
anchorNode | DOM Node 对象 | 只读属性。表示选择开始的节点。 |
anchorOffset | Number | 只读属性。表示所选内容的锚点在anchorNode 中的偏移量。如果anchorNode 是文本节点,则这是锚之前的anchorNode 中的字符数。如果anchorNode 是一个元素,则这是锚之前的锚节点的子节点数。 |
focusNode | DOM Node 对象 | 只读属性。表示选择结束时的节点。 |
focusOffset | Number | 只读属性。表示所选内容的锚点在focusNode 中的偏移量。 |
isCollapsed | Boolean | 只读属性。判断所选内容的起点和终点是否位于同一位置。 |
rangeCount | Number | 只读属性。返回选定范围中的范围数。 |
type | None (未选择)/Caret (选区被折叠)/Range (选择的是一个范围) | 只读属性。表示当前选择的类型。 |
getRangeAt(index) | index :Number。表示范围的从零开始的索引。除Firefox浏览器之外的浏览器中该值通常仅使用0 。 | 由于获取一个选择的范围。 |
addRange(range) | Range /StaticRange | 将一个范围添加到选择中。如果选择已有关联的范围,则除Firefox外的所有浏览器都将忽略该调用。 |
removeRange(range) | Range /StaticRange | 从选择中删除范围。 |
removeAllRanges() /empty() | - | 删除所有范围。 |
collapse(node, offset) /setPosition(node, offset) | 1. node :DOM Node 对象。2.offset :Number。表示节点中的偏移量。 | 用新的范围替换选定的范围,新范围从给定的node 处开始,到偏移offset 处结束。 |
collapseToStart() | - | 将所选内容折叠到所选内容中第一个范围的起始点。 |
collapseToEnd() | - | 将所选内容折叠到所选内容中第一个范围的结束点。 |
extend(node, offset) | 1. node :DOM Node 对象。2.offset :Number。表示节点中的偏移量。 | 将选择的焦点移到给定的节点,位置偏移offset 。 |
setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset) | 各参数与对应的同名属性相同。 | 用给定的起始点和结束点来替换选择范围。选中它们之间的所有内容。 |
selectAllChildren(node) | DOM Node 对象 | 选择node 的所有子节点。 |
deleteFromDocument() | - | 从文档中删除所选择的内容。 |
containsNode(node, allowPartialContainment = false) | 1. node :DOM Node 对象。2.allowPartialContainment :Boolean。 | 检查选择中是否包含node 的全部或者部分内容。 |
除了上面这些属性和方法之外,在表单元素上也有专门的选择API,但是不属于Selection
和Range
,这里不过多说明。
正文
学习了选择和范围,接下来我们正式进入CSS Custom Highlight API的学习,首先是基本使用步骤:
- 创建
Range
对象,如果需要有多个范围可以创建多个Range
对象。 - 创建
Highlight
对象,并将Range
传递给Highlight
。 - 使用
HighlightRegistry
来注册Highlight
。 - 通过使用
::highlight()
伪元素来设置高光样式。
🌰:
<head>
<style>
::highlight(test-highlight) {
background-color: lightblue;
}
</style>
</head>
<body>
<p>这是第一段很长很长很长很长很长很长很长很长很长很长很长很长的段落</p>
<p>这是第二段很长很长很长很长很长很长很长很长很长很长很长很长的段落</p>
<script>
const p = document.querySelectorAll('p')
const range = new Range()
range.setStart(p[0].firstChild, 10);
range.setEnd(p[1].firstChild, 20);
const highlight = new Highlight(range);
CSS.highlights.set('test-highlight', highlight)
</script>
</body>
接下来将使用这个🌰逐步解析其中的每一步。
接口
Highlight
Highlight
接口即表示自定义高光,类似一个Set
对象,其中包含的范围是继承了AbstractRange
的范围对象。
构造函数:
new Highlight(range1?, range2?, ...)
属性/方法
属性/方法 | 值/参数 | 简介 |
---|---|---|
priority | Number | 用于指定有多个重叠的Highlight 对象时,哪个对象的优先级更高,在应用样式时应用优先级高的样式来解决样式冲突。 |
size | Number | 表示Highlight 对象中的范围数。 |
type | highlight /spelling-error /grammar-error | 指定高亮的含义。用于在使用屏幕阅读器等辅助技术时在向用户表示该高亮的含义。 |
add(range) | Range /StaticRange | 添加新的范围对象。 |
clear() | - | 清空Highlight 中的所有范围。 |
delete() | Range /StaticRange | 从Highlight 中删除指定范围。 |
entries() | - | 返回一个包含按插入顺序排列的Highlight 对象中的每个范围迭代器对象 |
forEach(callbackFn, thisArg) | 与set.forEach() 方法参数一致。 | 按照插入顺序对Highlight 对象中的每个范围对象执行一次回调函数。 |
has(range) | Range /StaticRange | 判断Highlight 对象中是否包含给定的范围对象。 |
keys() /values() | - | 返回一个按插入顺序生成Highlight 对象中的范围的迭代器对象。 |
添加多个范围🌰:
const range1 = new Range()
range.setStart(p1.firstChild, 10);
range.setEnd(p2.firstChild, 20);
const range2 = new Range()
range.setStart(p3.firstChild, 10);
range.setEnd(p4.firstChild, 20);
const highlight = new Highlight(range1, rang2);
// or
const highlight = new Highlight();
highlight.add(range1).add(range2)
HighlightRegistry
HighlightRegistry
接口是一个用于注册要进行样式化Highlight
的类似Map
的对象。可以通过CSS.highlights
属性进行访问:
console.log(CSS.highlights) // HighlightRegistry
属性/方法
属性/方法 | 值/参数 | 简介 |
---|---|---|
size | Number | 只读属性。用于获取已注册的Highlight 数量。 |
clear() | - | 删除注册表所有Highlight 对象。 |
delete(customHighlightName) | String | 从注册表中删除指定名称的Highlight 对象。 |
entries() | - | 返回一个包含按顺序插入插入注册表的Highlight 对象的迭代器对象。 |
forEach(callbackFn, thisArg) | 与map.forEach() 方法参数一致。 | 清空Highlight 中的所有范围。 |
get(name) | String | 从注册表中获取指定名称的Highlight 对象。 |
has(name) | String | 判断注册表中是否包含指定名称的Highlight 对象。 |
keys() /values() | - | 返回一个按插入顺序产生注册表中的Highlight 对象的迭代器对象。 |
set(name, highlight) | 1. name :String。2. highlight :Highlight 对象 | 在注册表中添加或更新指定名称的Highlight 对象。 |
🌰:
CSS.highlights.set("test-highlight-1", highlight1);
CSS.highlights.set("test-highlight-2", highlight2);
CSS.highlights.set("test-highlight-3", highlight3);
console.log(CSS.highlights.size)
console.log(CSS.highlights.get("test-highlight-2"))
::highlight()
伪元素
::highlight()
伪元素选择已注册的Highlight
的范围应用样式。接受一个已注册的Highlight
的名称作为参数。
注意:
- 上面提到过高亮的优先级,默认情况下每个高亮的优先级相同,如果有重叠的情况可能会造成样式冲突,需要通过
priority
属性来设置高亮的优先级。 - 只能在
::highlight()
当中使用color
、background-clor
、text-shadow
、文本装饰等等属性。
🌰:
::highlight(test-highlight-1) {...}
彩色文本🌰
<head>
<style>
::highlight(text-highlight-0) {
color: lightcyan;
}
::highlight(text-highlight-1) {
color: lightblue;
}
::highlight(text-highlight-2) {
color: lightseagreen;
}
::highlight(text-highlight-3) {
color: lightgreen;
}
::highlight(text-highlight-4) {
color: lightcoral;
}
::highlight(text-highlight-5) {
color: lightsalmon;
}
::highlight(text-highlight-6) {
color: lightpink;
}
::highlight(text-highlight-7) {
color: lightyellow;
}
</style>
</head>
<body>
<p>这是一个很长很长很长很长的段落。</p>
<script>
const p = document.querySelector('p')
const rangeList = []
for (let r = 0; r < p.firstChild.length; r += 2) {
let range = new Range()
range.setStart(p.firstChild, r)
range.setEnd(p.firstChild, r + 2)
rangeList.push(range)
}
rangeList.forEach((item, index) => {
CSS.highlights.set(`text-highlight-${index}`, new Highlight(item))
})
</script>
</body>
参考资料
转载自:https://juejin.cn/post/7282746104025628711