纯CSS实现关键词高亮效果

前几天浏览 MDN 的 Web Api,偶然看到 CSS 自定义高亮 API,感觉挺实用,虽然现在 Firefox 还未支持该 Api,了解一下也不吃亏。

CSS 自定义高亮 API

CSS 自定义高亮 API 提供了一种方法,可以通过使用 JavaScript 创建范围并使用 CSS 定义样式来设置文档中任意文本范围的样式。

高亮关键词,常见做法是用 js 查找 DOM 中匹配的关键词,然后在文字外包一层 span,应用高亮样式,实现起来很简单。使用 CSS 高亮则不会影响页面中的 DOM 结构,看起来更优秀。

使用 CSS 自定义高亮 API 设置网页上文本范围的样式有四个步骤:

  1. 创建 Range 对象。
  2. 为这些范围创建 Highlight 对象。
  3. 使用 HighlightRegistry 进行注册。
  4. 使用 ::highlight() 伪元素定义高亮样式。

代码实现

上面的四个步骤,第一步比较麻烦,代码比较多,而且包含不少陌生的新知识。先上代码,边试验边体会

在 template 中定义一个列表,列表数据从后端返回。高亮关键词通过 el-input 输入。

html 复制代码
  <div class="p-4">
    <div class="my-4">
      <el-input v-model="keyword" @input="input" placeholder="请输入关键词"></el-input>
    </div>

    <ul class="list-client">
      <li v-for="client in tableData" :key="client.id" class="py-2">
        <MoDict :value="client.type" dictName="clientType" />
        {{ client.name }}
      </li>
    </ul>
  </div>

列表数据加载完成后,再建立需要搜索关键词的范围

js 复制代码
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import clientApi from '~/api/client'
import { useTable } from '~/hooks/useTable'

if (!CSS.highlights) {
  ElMessage.error('此浏览器不支持 CSS 自定义高亮 API!')
}

const { tableData, loading, getData } = useTable(clientApi.getClientList, {}, undefined, {
  immediate: false
})

const keyword = ref('')
const allTextNodes = []

onMounted(async () => {
  await getData()
  nextTick(() => {
    const content = document.querySelector('.list-client')
    const treeWalker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT)
    let currentNode = treeWalker.nextNode()
    while (currentNode) {
      allTextNodes.push(currentNode)
      currentNode = treeWalker.nextNode()
    }
  })
})

const input = (e: string) => {
  if (!CSS.highlights) {
    return
  }

  CSS.highlights.clear()

  const str = e.trim().toLowerCase()
  if (!str) {
    return
  }

  const ranges = allTextNodes
    .map((el) => {
      return { el, text: el.textContent.toLowerCase() }
    })
    .map(({ text, el }) => {
      const indices = []
      let startPos = 0
      while (startPos < text.length) {
        const index = text.indexOf(str, startPos)
        if (index === -1) break
        indices.push(index)
        startPos = index + str.length
      }

      return indices.map((index) => {
        const range = new Range()
        range.setStart(el, index)
        range.setEnd(el, index + str.length)
        return range
      })
    })

  const searchResultsHighlight = new Highlight(...ranges.flat())

  CSS.highlights.set('search-results', searchResultsHighlight)
}
</script>

高亮效果 CSS

css 复制代码
::highlight(search-results) {
  background-color: #f06;
  color: white;
}

代码实现还是比较清晰易懂的,但是里面确实有很多没接触过的东西,接下来,摩拳擦掌,开始学习。

TreeWalker

TreeWalker 对象用于表示文档子树中的节点和它们的位置。

TreeWalker 可以使用 Document.createTreeWalker() 方法创建。

createTreeWalker

常数 描述
NodeFilter.SHOW_ALL 显示所有节点。
NodeFilter.SHOW_ATTRIBUTE 显示 Attr 节点。
NodeFilter.SHOW_CDATA_SECTION 显示 CDATASection 节点。
NodeFilter.SHOW_COMMENT 显示 Comment节点。
NodeFilter.SHOW_DOCUMENT 显示 Document 节点。
NodeFilter.SHOW_DOCUMENT_FRAGMENT 显示 DocumentFragment 节点。
NodeFilter.SHOW_DOCUMENT_TYPE 显示 DocumentType 节点。
NodeFilter.SHOW_ELEMENT 显示 Element 节点。
NodeFilter.SHOW_ENTITY 已弃用 旧式参数,不再有效。
NodeFilter.SHOW_ENTITY_REFERENCE 已弃用 旧式参数,不再有效。
NodeFilter.SHOW_NOTATION 已弃用 旧式参数,不再有效。
NodeFilter.SHOW_PROCESSING_INSTRUCTION 显示 ProcessingInstruction 节点。
NodeFilter.SHOW_TEXT 显示 Text 节点。

我们在这里使用 NodeFilter.SHOW_TEXT,可以获取指定 DOM 元素的文本内容节点。

js 复制代码
 const treeWalker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT)

treeWalker.nextNode()方法将当前节点移动到文档顺序中的下一个可见节点,并返回找到的节点,并pushallTextNodes数组中,如果不存在这样的节点,则返回null,循环结束。 allTextNodes会在之后用到。

js 复制代码
let currentNode = treeWalker.nextNode()
    while (currentNode) {
      allTextNodes.push(currentNode)
      currentNode = treeWalker.nextNode()
    }

在 input 中输入关键词后,代码继续往下运行 遍历allTextNodes,根据输入的关键词,获取匹配到的 Range。

Range MDN文档:developer.mozilla.org/zh-CN/docs/...

Range 接口表示一个包含节点与文本节点的一部分的文档片段。

js 复制代码
// new 一个 range,然后设置它的起始位置和结束位置。
const range = new Range()
range.setStart(el, index)
range.setEnd(el, index + str.length)
return range

为这些范围创建一个 Highlight 对象 ,使用 HighlightRegistry 进行注册。

js 复制代码
// 为文本范围创建自定义高亮,把二维数据转化为一维数组,
const searchResultsHighlight = new Highlight(...ranges.flat())
// 在 HighlightRegistry 中注册文本范围。
// CSS.highlights.set添加给定名称的 `Highlight` 对象到注册表,如果该名称的对象已存在则覆盖原值。
CSS.highlights.set('search-results', searchResultsHighlight)

最后,使用 ::highlight() 伪元素定义高亮样式。

css 复制代码
::highlight(search-results) {
  background-color: #f06;
  color: white;
}

完成 CSS 高亮的代码非常简单, 其实大部分代码都是获取 Range 的过程。 了解这个新的 Api 同时也让我对 文档对象模型(DOM) 有了更深入的了解,说不准哪天就用到了。 希望 firefox 早日支持 CSS Highlight ,这样我也可以在实际项目中运用它了。

相关推荐
开开心心就好2 小时前
批量提取PDF中的图片,直接导出原图
前端·javascript·支持向量机·智能手机·pdf·html·启发式算法
Setsuna_F_Seiei4 小时前
前端转型 Agent 开发 05 之 Agent Hooks 与 Checkpointer(让 Agent 从全自动转变人为可掌控)
前端·agent·ai编程
百万蹄蹄向前冲6 小时前
风扇转了一晚上MVP专家团翻车事故
前端·人工智能
默_笙6 小时前
🏛 给 AI 配一间办公室:Harness Engineering 六大模块与它的实现
前端·javascript
linux_cfan7 小时前
videojs v10 源代码系列解读:14 · 谓词守卫:在运行时安全地调用能力
前端·javascript·音视频
kyriewen7 小时前
我扒了 10,221 条 JD:腾讯技术岗 75% 在要 AI
前端·人工智能·ai编程
郑州光合科技余经理8 小时前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
IT_陈寒9 小时前
Vue的响应式让我熬到凌晨三点,原来漏了这个小细节
前端·人工智能·后端
Blanche15009 小时前
利用 RAG 为答疑机器人扩展知识范围
前端
天若有情6739 小时前
【纯前端小工具】公历生日转农历,批量查询每年农历生日对应的公历日期(GitHub Pages在线直接用)
前端·javascript·github pages·农历转换·lunisolar·网页小工具