30 行代码让搜索词高亮

1,557 阅读1分钟

大多数产品都存在搜索框,在搜索结果中高亮搜索词是一种不错的交互方式,能让用户快速筛选搜索结果,提升用户体验,在以前,我们很少会这么做,因为要实现这个功能需要较大的成本。

现在,有了 Custom Highlight API 让我们能轻松实现该功能,不需要多余的依赖,也不需要修改现有组件。

效果

简单例子

<body>Lorem Ipsum.
<script>
  let textNode = document.body.firstChild;

  let r1 = new Range();
  r1.setStart(textNode, 1);
  r1.setEnd(textNode, 5);

  let r2 = new Range();
  r2.setStart(textNode, 3);
  r2.setEnd(textNode, 7);

  // 这里的 `sample` 是一个 CSS name
  // 语法是 https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident
  // 它用于伪元素 `::highlight`,可以对他应用一些有限制的样式
  CSS.highlights.set("sample", new Highlight(r1, r2));
</script>
<style>
  ::highlight(sample) { background-color: rgba(0, 0, 255, 0.3); }
</style>

高亮多个 Range 除了在构造 Highlight 传递多个参数外,也可以使用 Highlight.prototype.add 方法。

创建搜索词 Range

在搜索结果中创建所有搜索词的 Range 是一件很复杂的事情,但我们可以用一种简单的方法来适配大部分用例,只要遍历 Text 节点,匹配他们值即可:

const getRanges = (root: Node, text: string) => {
  const reg = new RegExp([...text].map((c) => `\\u{${c.codePointAt(0)!.toString(16)}}`).join(''), 'gui');
  const ranges: Range[] = [];
  const nodes: Node[] = [root];
  while (!!nodes.length) {
    const node = nodes.pop()!;
    switch (node.nodeType) {
      case Node.TEXT_NODE:
        const matched = node.nodeValue?.matchAll(reg);
        if (matched) {
          for (const arr of matched) {
            if (arr.index !== undefined) {
              const range = new Range();
              range.setStart(node, arr.index);
              range.setEnd(node, arr.index + text.length);
              ranges.push(range);
            }
          }
        }
        break;
      case Node.ELEMENT_NODE:
        if ((node as Element).shadowRoot) nodes.push((node as Element).shadowRoot as Node);
        break;
    }
    if (node.childNodes[0]) nodes.push(node.childNodes[0]);
    if (node.nextSibling) nodes.push(node.nextSibling);
  }
  return ranges;
};

高亮搜索词

当搜索词改变时,执行高亮即可:

if (!search) return CSS.highlights.clear();
CSS.highlights.set("sample", new Highlight(...getRnages(tbody, search)));

Gem 例子

在不同的框架中,执行高亮有不同的方式,在 gem 中这样以副作用这样调用:

this.effect(
  ([search]) => {
    const tbody = document.querySelector('tbody');

    if (!search) return CSS.highlights.clear();
    CSS.highlights.set("sample", new Highlight(...getRnages(tbody, search)));
  },
  () => [this.search],
);

完结 🎉🎉🎉🎉🎉🎉