不修改DOM的高亮黑科技,你可能还不知道

大家好,我是CC,在这里欢迎大家的到来~

背景

在传统实现文本高亮时通常使用span标签包裹文本,再给 span 标签添加相应高亮背景色。这种方式会修改原本的DOM结构,逻辑复杂,也会频繁导致页面重绘,消耗浏览器性能。而基于HighLightCSS自定义高亮API的这种方式可以实现在渲染层处理文本高亮,既不影响 DOM 树,而且完全独立于文档结构

实现步骤

创建 Range 对象

标识想要高亮的文本范围。

<div id="foo">纯 CSS 实现文本高亮</div> const parentNode = document.getElementById("foo"); const range1 = new Range(); range1.setStart(parentNode, 1); range1.setEnd(parentNode, 2); const range2 = new Range(); range2.setStart(parentNode, 4); range2.setEnd(parentNode, 6);

为 Range 对象添加 Highlight 对象

多个 Range 对象可以与同一个 Highlight 对象关联,这样会以相同方式高亮显示多个文本片段。

const highlight = new Highlight(range1, range2);

当然也可以在某些场景下创建多个 Highlight 对象,比如在使用协作文本编辑器中每个用户使用不同的文本颜色。

const user1Highlight = new Highlight(user1Range1, user1Range2); const user2Highlight = new Highlight(user2Range1, user2Range2, user2Range3);

HighlightRegistry 注册

注册表是一个 Map 对象,通过名称注册高亮。

CSS.highlights.set("user-1-highlight", user1Highlight); CSS.highlights.set("user-2-highlight", user2Highlight);

当然除了注册之外,也支持删除和清除。

CSS.highlights.delete("user-1-highlight"); CSS.highlights.clear();

::highlight()伪元素定义高亮样式

为文本片段添加自定义样式进行高亮。

::highlight(user-1-highlight) { background-color: yellow; color: black; } ::highlight(user-2-highlight) { background-color: black; color: yellow; }

应用场景

这里举例两个CSS自定义高亮API的应用场景。

搜索高亮文本

在多段文本中搜索检索到文本后直接高亮展示,方便用户查找。

import { message } from "antd"; import { useEffect, useRef } from "react"; import "./index.less"; const HighlightText = ({ text, highlightedWords, type = "text" }: { text: string; highlightedWords: string[]; type?: "text" | "html"; }) => { // 将 Node 改为更具体的 HTMLDivElement 以修复 ref 类型错误 const textRef = useRef<HTMLDivElement | null>(null); useEffect(() => { if (!CSS.highlights) { message.warning("CSS Custom Highlight API not supported."); return; } CSS.highlights.clear(); // 支持多个词语:去重、trim,并过滤空字符串 const words = Array.from(new Set(highlightedWords.map((w) => w.trim()).filter(Boolean))); if (!words.length) { return; } if (!textRef.current) return; const treeWalker = document.createTreeWalker(textRef.current, NodeFilter.SHOW_TEXT); const allTextNodes: Text[] = []; let currentNode = treeWalker.nextNode() as Text | null; while (currentNode) { allTextNodes.push(currentNode); currentNode = treeWalker.nextNode() as Text | null; } // 为所有词语在所有文本节点中生成 range const ranges: Range[] = []; for (const el of allTextNodes) { const content = el.textContent || ""; for (const word of words) { let startPos = 0; while (startPos < content.length) { const index = content.indexOf(word, startPos); if (index === -1) break; const range = new Range(); range.setStart(el, index); range.setEnd(el, index + word.length); ranges.push(range); startPos = index + word.length; } } } if (ranges.length) { // 统一用一个高亮名称,样式在 ::highlight(search-results) 中定义 const searchResultsHighlight = new Highlight(...ranges); CSS.highlights.set("search-results", searchResultsHighlight); } }, [highlightedWords, text]); return ( <> {type === "html" ? ( <div ref={textRef} dangerouslySetInnerHTML={ { __html: text } }> </div> ) : ( <div ref={textRef}>{text}</div> )} </> ); }; export default HighlightText;

文本差异对比

对两段文本进行对比时,左侧高亮“不存在于右侧”的文本为绿色(删除),右侧高亮“多于左侧”的文本为红色(新增),可以直观看到差异。

import { message } from "antd"; import { useEffect, useRef } from "react"; import "./index.less"; type DiffProps = { left: string; right: string; type?: "text" | "html"; }; const TextDiff = ({ left, right, type = "text" }: DiffProps) => { const leftRef = useRef<HTMLDivElement | null>(null); const rightRef = useRef<HTMLDivElement | null>(null); useEffect(() => { if (!("highlights" in CSS)) { message.warning("CSS Custom Highlight API not supported."); return; } const collectNodes = (root: HTMLElement | null) => { if (!root) return { nodes: [] as Text[], starts: [] as number[], text: "" }; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes: Text[] = []; const starts: number[] = []; let text = ""; let cur = walker.nextNode() as Text | null; while (cur) { nodes.push(cur); starts.push(text.length); text += cur.textContent || ""; cur = walker.nextNode() as Text | null; } return { nodes, starts, text }; }; const { nodes: leftNodes, starts: leftStarts, text: leftText } = collectNodes(leftRef.current); const { nodes: rightNodes, starts: rightStarts, text: rightText } = collectNodes(rightRef.current); const n = leftText.length; const m = rightText.length; const dp: number[][] = Array(n + 1) .fill(0) .map(() => Array(m + 1).fill(0)); for (let i = 1; i <= n; i++) { for (let j = 1; j <= m; j++) { if (leftText[i - 1] === rightText[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } type Op = { t: "equal" | "del" | "add"; ai?: number; bi?: number }; const ops: Op[] = []; let i = n, j = m; while (i > 0 || j > 0) { if (i > 0 && j > 0 && leftText[i - 1] === rightText[j - 1]) { ops.push({ t: "equal", ai: i - 1, bi: j - 1 }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { ops.push({ t: "add", bi: j - 1 }); j--; } else if (i > 0) { ops.push({ t: "del", ai: i - 1 }); i--; } } ops.reverse(); type Span = { start: number; length: number }; const leftDelSpans: Span[] = []; const rightAddSpans: Span[] = []; let posA = 0; let posB = 0; let delRunStart: number | null = null; let delRunLen = 0; let addRunStart: number | null = null; let addRunLen = 0; const flushDel = () => { if (delRunStart !== null && delRunLen > 0) leftDelSpans.push({ start: delRunStart, length: delRunLen }); delRunStart = null; delRunLen = 0; }; const flushAdd = () => { if (addRunStart !== null && addRunLen > 0) rightAddSpans.push({ start: addRunStart, length: addRunLen }); addRunStart = null; addRunLen = 0; }; for (const op of ops) { if (op.t === "equal") { flushDel(); flushAdd(); posA++; posB++; } else if (op.t === "del") { if (delRunStart === null) delRunStart = posA; delRunLen++; flushAdd(); posA++; } else if (op.t === "add") { if (addRunStart === null) addRunStart = posB; addRunLen++; flushDel(); posB++; } } flushDel(); flushAdd(); const locate = (starts: number[], nodes: Text[], pos: number) => { let idx = 0; while (idx < nodes.length) { const nodeLen = (nodes[idx].textContent || "").length; const s = starts[idx]; if (pos < s + nodeLen) return { nodeIndex: idx, offset: pos - s }; idx++; } const lastIdx = nodes.length - 1; return { nodeIndex: Math.max(0, lastIdx), offset: (nodes[lastIdx]?.textContent || "").length }; }; const spansToRanges = (spans: Span[], starts: number[], nodes: Text[]) => { const ranges: Range[] = []; for (const { start, length } of spans) { const end = start + length; const sLoc = locate(starts, nodes, start); const eLoc = locate(starts, nodes, end); const r = new Range(); r.setStart(nodes[sLoc.nodeIndex], sLoc.offset); r.setEnd(nodes[eLoc.nodeIndex], eLoc.offset); ranges.push(r); } return ranges; }; const leftRanges = spansToRanges(leftDelSpans, leftStarts, leftNodes); const rightRanges = spansToRanges(rightAddSpans, rightStarts, rightNodes); if (leftRanges.length) CSS.highlights.set("diff-del-left", new Highlight(...leftRanges)); else CSS.highlights.delete("diff-del-left"); if (rightRanges.length) CSS.highlights.set("diff-add-right", new Highlight(...rightRanges)); else CSS.highlights.delete("diff-add-right"); }, [left, right, type]); return ( <div className="text-diff-container"> {type === "html" ? ( <div className="text-diff-pane" ref={leftRef} dangerouslySetInnerHTML={{ __html: left }} /> ) : ( <div className="text-diff-pane" ref={leftRef}> {left} </div> )} {type === "html" ? ( <div className="text-diff-pane" ref={rightRef} dangerouslySetInnerHTML={{ __html: right }} /> ) : ( <div className="text-diff-pane" ref={rightRef}> {right} </div> )} </div> ); }; export default TextDiff;

总结

目前来看,CSS Custom Highlight API 就是网页文本高亮的“神器”。特别适合那些需要疯狂标记、又不能动原文档结构的应用,比如在线文档、代码编辑器。只要浏览器支持,用它就对了,绝对是未来的趋势。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1155965.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1 行代码搭 Agentic 大模型应用?这场直播教你 30 分钟快速上手!

还在愁大模型应用开发门槛高&#xff1f;不需要复杂背景&#xff0c;不用重工程经验 —— 1 月 15 日&#xff08;周四&#xff09;17:00-18:00&#xff0c;AtomGit 联合 LazyLLM 带来「LazyLLM Agentic 应用开发快速上手」直播&#xff01;&#x1f399;️ 直播亮点由 LazyLLM…

该设备的驱动程序未被安装(代码 28)怎么办 详细修复方法

在 Windows 系统中&#xff0c;打开设备管理器时&#xff0c;如果看到提示“该设备的驱动程序未被安装&#xff08;代码 28&#xff09;”&#xff0c;通常说明系统未能识别或正确加载对应硬件的驱动程序。该问题多见于新装系统、硬件更换、驱动缺失或系统异常等场景。下面从实…

为什么化工制造业越来越难吸引年轻人?从组织结构到人才逻辑的分析

过去几年&#xff0c;在服务化工、材料、能源等制造业客户的过程中&#xff0c;一个现象反复出现&#xff1a;企业本身并不弱&#xff0c;但年轻人正在快速流失&#xff0c;甚至出现明显断层。从客观条件看&#xff0c;这些企业大多具备以下特征&#xff1a;福利与用工合规薪资…

兴趣岛1元试课靠谱吗?如何运作?拆解其在线教育系统逻辑

兴趣岛靠谱吗&#xff1f;在信息爆炸的时代&#xff0c;消费者面对任何商业模式时&#xff0c;往往容易陷入“非黑即白”的极端判断。近期&#xff0c;围绕兴趣岛的“一元试课”模式&#xff0c;社交媒体上出现不少争议声音&#xff0c;部分不好怀疑的声音将其标签化为“营销套…

工业场景中弧形导轨的安装要点

弧形导轨作为工业自动化中实现弧线运动的核心部件&#xff0c;常用于机械臂关节、旋转工作台、自动化生产线转弯部位&#xff0c;医疗CT机的旋转扫描部件也依赖高精度弧形导轨实现平滑运动。其安装质量直接影响设备运行精度与寿命&#xff0c;从材料准备到定位调试&#xff0c;…

人工智能之核心基础 机器学习 第十三章 自监督学习

人工智能之核心基础 机器学习 第十三章 自监督学习 文章目录人工智能之核心基础 机器学习13.1 自监督学习概述&#x1f4cc; 定义&#xff1a;从无标签数据中**自动生成监督信号**&#x1f50d; 与无监督学习的区别13.2 自监督学习的核心&#xff1a;前置任务设计1️⃣ 掩码填…

项目一多就混乱?试试把大目标拆成7层小动作

我见过太多这样的现场&#xff1a;每天早会一开&#xff0c;大家低头刷手机&#xff0c;汇报永远是“差不多完成了”、“快了快了”&#xff1b;群里消息满天飞&#xff0c;每个人都在跟进&#xff0c;但项目依旧卡在原地&#xff1b;老板问一句&#xff1a;“现在到底卡在哪&a…

一次半夜回滚,让我彻底扔掉了本地开发环境

对于一个初创团队而言&#xff0c;最兴奋的时刻&#xff0c;莫过于核心产品上线的那一刻。我至今还记得那个周五晚上&#xff0c;我们准备了一个月的新版本终于要发布了。团队所有人都挤在会议室&#xff0c;盯着部署脚本&#xff0c;等待见证奇迹。然而&#xff0c;奇迹没有发…

基于STM3251单片机的多功能垃圾桶控制系统

作者贡献介绍 &#x1f497;CSDN从事毕设辅导第一人&#xff0c;本着诚信、靠谱、质量在业界获得优秀口碑&#xff0c;在此非常希望和行业内的前辈交流学习&#xff0c;欢迎成考学历咨询老师、大学老师前来合作交流&#x1f497; 2013年&#xff0c;正式踏入技术写作领域&…

【计算机毕业设计案例】机器学习基于python-AI深度学习对狗表情训练识别基于python-AI深度学习对狗表情训练识别

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

我们如何把“配环境一天”缩短到“3秒启动”?

我写了十年代码&#xff0c;热情被磨灭的瞬间&#xff0c;往往不是因为一个复杂的算法&#xff0c;而是因为那些无穷无尽的琐事。新同事入职&#xff0c;第一天基本废了&#xff0c;全在配环境。我的 MacBook 风扇狂转&#xff0c;就因为跑了个复杂的后端项目。最怕听到那句“在…

千元出头,权限全开!实测最近卖爆的拾光坞G2到底如何!

引言时间已经来到了26年的一月中旬了&#xff0c;从上个月某N150型号预售到现在&#xff0c;熊猫依然是没看到网上有什么用户的测评&#xff0c;当然别人提前就说了是预售模式&#xff0c;所以这一点没啥喷的。在同样的配置下&#xff0c;N150的另一款机型因为其价格的优势最近…

大数据数据服务在物流行业的应用

大数据数据服务在物流行业的创新应用&#xff1a;构建智能物流新生态 摘要/引言 在当今数字化时代&#xff0c;物流行业面临着诸多挑战&#xff0c;如配送效率低下、成本居高不下、库存管理不合理等。大数据数据服务作为一种新兴技术&#xff0c;为解决这些问题提供了有效途径。…

化学研究智能体:AI架构师必须掌握的负载均衡策略

化学研究智能体规模化部署&#xff1a;AI架构师必学的负载均衡策略 引言&#xff1a;化学智能体从实验室到生产的算力瓶颈 当你花费数月时间训练出一个能预测分子性质的化学智能体&#xff0c;从实验室的单节点测试走向生产环境时&#xff0c;可能会遇到这样的场景&#xff1a;…

【计算机毕业设计案例】基于python_CNN深度学习卷积神经网络训练识别猫的表情

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

【毕业设计】基于深度学习对狗表情训练识别基于python-AI深度学习对狗表情训练识别

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

深度学习毕设项目推荐-基于python_CNN深度学习卷积神经网络训练识别猫的表情

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

【计算机毕业设计案例】基于python_CNN深度学习卷积神经网络识别菠萝是否腐烂

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

创新试错提速,国产CAD助力原型快速迭代

投资人最喜欢问&#xff1a;“你们迭代一个版本要多久&#xff1f;”我们的回答是&#xff1a;“看软件的响应速度。”这不是玩笑。从灵光一现到初步模型&#xff0c;CAXA 3D的“创新模式”让我们几小时内就能完成。以前用传统设计软件&#xff0c;从突发的创意灵感落到初步模型…

KubeSphere v4.2.1 重磅发布:精进不止、向新而生

随着云原生平台在企业核心业务中的广泛落地&#xff0c;K8s 已从早期的 “技术尝鲜” 阶段全面迈入 “生产级承载” 时代。越来越多的关键业务系统纷纷构建于 K8s 之上。在此背景下&#xff0c;K8s 面临的核心挑战已不再局限于基础部署与运维&#xff0c;而是逐步转向三大关键维…