css highlights 学习之实现一个括号内文本高亮组件

172 阅读2分钟

在 chrome 105 之后谷歌浏览器支持了 CSS 原生高亮 API, 原生 highlights 有很多优势,当然我认为最明显的优势就是它的高亮不会增加任何嵌套标签!!!

这里尝试使用这个 API 来实现一个文本组件,功能是内部括号内的文本会高亮显示。

首先搭一个架子,标签名叫 xtt-hl,继承自 span 标签。

<span is="xtt-hl">(test1) content (test2)</span>
<script>
	customElements.define(
		"xtt-hl",
		class xttHighLightsElement extends HTMLSpanElement {
			constructor() {
				super();
				const shadowRoot = this.attachShadow({ mode: "open" });
				shadowRoot.appendChild(document.createElement("slot"));
			}
		},
		{ extends: "span" }
	);
</script>

可以看到,我们的组件已经可以正常使用了,但是还没有任何效果。 1.png

接下来我们需要使用 CSS 原生高亮 API 来实现高亮效果。

addHighlight() {
	// 首先创建 Range 片段,查找和存储需要高亮文本
	const rangeList = [];
	for (const match of this.textContent.matchAll(/\(.*?\)/g)) {
		const range = new Range();
		range.setStart(this.firstChild, match.index);
		range.setEnd(this.firstChild, match.index + match[0].length);
		rangeList.push(range);
	}

	// 创建高亮文本
	const highlights = new Highlight(...rangeList);

	// 设置高亮文本的样式
	CSS.highlights.set("xtt-hl", highlights);
}

// 然后设置 CSS 规则,在 constructor 中追加
let sheet = new CSSStyleSheet();
sheet.replaceSync(`
	:host ::highlight(xtt-hl) {
		color: aqua;
	}
`);
shadowRoot.adoptedStyleSheets = [sheet];

可以看到括号内的文本已经被高亮了。

2.png

但是当我们尝试追加多个 xtt-hl 元素时,发现非最后一个元素的高亮都失效了。

3.png

这是因为上面所有组件的 CSS.highlights.set 方法的参数是同一个字符串,那么后面在 set 的时候就会覆盖前面已 set 的内容。这里有可能有两种解决方法,一种是使用不同的字符串,另一种是在 set 时合并多次的 Highlight 对象。

这里我们选择第二种方法。

-const highlights = new Highlight(...rangeList);
+let highlights;
+if (CSS.highlights.has("xtt-hl")) {
+	highlights = CSS.highlights.get("xtt-hl");
+	highlights.add(...rangeList);
+} else {
+	highlights = new Highlight(...rangeList);
+}

完成了!!虽然只是一个简单的例子,还有许多不足,如内容改变或元素被移除时没有清除内存等。

4.png

完整代码如下:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<title>highlights</title>
	</head>
	<body>
		<span is="xtt-hl">(test1) content (test2)</span>
		<span is="xtt-hl">(test3) content</span>
		<script>
			customElements.define(
				"xtt-hl",
				class xttHighLightsElement extends HTMLSpanElement {
					constructor() {
						super();
						const shadowRoot = this.attachShadow({ mode: "open" });

						let sheet = new CSSStyleSheet();
						sheet.replaceSync(`
                                                    :host ::highlight(xtt-hl) {
                                                        color: aqua;
                                                    }
                                                `);
						shadowRoot.adoptedStyleSheets = [sheet];
						shadowRoot.appendChild(document.createElement("slot"));
						this.addHighlight();
					}

					addHighlight() {
						const rangeList = [];
						for (const match of this.textContent.matchAll(/\(.*?\)/g)) {
							const range = new Range();
							range.setStart(this.firstChild, match.index);
							range.setEnd(this.firstChild, match.index + match[0].length);
							rangeList.push(range);
						}

						let highlights;

						if (CSS.highlights.has("xtt-hl")) {
							highlights = CSS.highlights.get("xtt-hl");
							highlights.add(...rangeList);
						} else {
							highlights = new Highlight(...rangeList);
						}

						CSS.highlights.set("xtt-hl", highlights);
					}
				},
				{ extends: "span" }
			);
		</script>
	</body>
</html>