预期效果
原本想法源自于自己群内一个哥们的非常简单的需求,大概效果就是有一个输入框,有一块渲染的文章,然后把输入的东西在文章中高亮。
我们第一反应是什么?
oneRow(text, val) {
if (text.indexOf(val) !== -1 && val !== '') {
return text.replace(val, '<mark>' + val + '</mark>');
}
}
< Mark >: HTML 标记文本元素 (< Mark >
) 表示为引用或符号目的而标记或突出显示的文本,这是由于标记的段落在封闭上下文中的相关性或重要性造成的。
但是这个方法貌似只能单行操作,而且不支持忽略大小写等等,如果text是一个dom结构,并且需要快速实现一个这样的方法,并且兼容多端应该怎么办?
答:使用正则表达式进行原生替换操作(性能消耗较高)
思路如下:
首先,检查
text
参数是否为空或 false。如果是的话,提前返回。然后,定义一个嵌套辅助函数叫做
dfs
,接收一个node
参数。在 dfs 函数内部,使用 nodeType 属性检查 node 是否是文本节点。如果不是文本节点,则在当前节点的每个子节点上递归调用 dfs。
如果 node 是文本节点,则使用 nodeValue 属性获取该节点的文本内容。
现在,使用 text 参数和 RegExp 构造函数创建正则表达式,并判断是否使用 gi 标志进行全局匹配和不区分大小写。
要查找正则表达式的所有匹配项,请在 nodeText 字符串上使用 matchAll 方法。结果存储在 matches 数组中。
如果找不到匹配项,提前返回。
现在,初始化一个名为 fragments 的空数组来存储文本和 <mark> 元素。
使用 forEach 方法迭代 matches 数组中的每个匹配。
对于每一次匹配,在匹配之前创建一个包含文本的文本节点并且把它加入到 fragments 数组。
现在,创建一个 <mark> 元素,在这里我们可以自定义样式,原生的<mark> 元素带有原生的margin可能会破坏样式结构,并向其追加一个包含匹配文本的文本节点。然后将 <mark> 元素推送到 fragments 数组。
将 lastIndex 变量更新为当前比赛结束的索引。
在遍历所有匹配后,创建一个包含最后一次匹配后剩余文本的文本节点,并将其推送到 fragments 数组中。
现在,在当前文本节点的父节点之前插入每个片段节点,并删除当前文本节点。
最后,在当前节点的每个子节点上调用 dfs 来继续深度优先搜索。
matchAll()
方法返回一个迭代器,该迭代器包含了检索字符串与正则表达式进行匹配的所有结果,该方法一般使用正则表达式匹配后。
源码:
interface useTextHighlightCinfig {
/**
* @description 匹配文本
*/
text: string;
/**
* @description 全局匹配
*/
globalMatching?: boolean;
/**
* @description 忽略大小写
*/
ignoreCase?: boolean;
/**
* @description 样式
*/
style?: any;
}
let oldDom: string | null = null;
function useTextHighlight(node: HTMLElement, config?: useTextHighlightCinfig) {
if (!oldDom) {
oldDom = node.outerHTML;
}
const {
globalMatching = true,
ignoreCase = true,
style = {
padding: "0",
},
text,
} = config || {};
// changeStyle
// 对象转css字符串
const changeStyleStr: string = Object.keys(style)
.map(key => `${key}:${style[key]}`)
.join(";");
const dfs = (node: HTMLElement) => {
// 如果是文本节点
if (node.nodeType === Node.TEXT_NODE) {
// 当前文本
const nodeText: string = node.nodeValue!;
// g为全局匹配
// i为忽略大小写
const regex = new RegExp(`(${text})`, `${globalMatching ? "g" : ""}${ignoreCase ? "i" : ""}`);
// 使用正则表达式查找节点文本中的所有匹配项。
const matches = [...nodeText.matchAll(regex)];
// 未匹配到直接返回
if (matches.length === 0) return;
// 存储文本和 <mark> 元素。
const fragments = [];
let lastIndex = 0;
matches.forEach((match: any) => {
// 为匹配前的文本创建文本节点。
fragments.push(document.createTextNode(nodeText.slice(lastIndex, match.index)));
// mark元素用来标记或突出显示文本内容
const mark = document.createElement("mark");
mark.setAttribute("style", changeStyleStr);
// 将匹配的文本添加到 <mark> 元素。
mark.appendChild(document.createTextNode(match[0]));
fragments.push(mark);
// 将 lastIndex 更新为当前匹配的末尾。
lastIndex = match.index + match[0].length;
});
// 为最后一个匹配后的文本创建文本节点。
fragments.push(document.createTextNode(nodeText.slice(lastIndex)));
// 将新节点插入到旧节点后面,然后删除旧节点
fragments.forEach(fragment => node.parentNode!.insertBefore(fragment, node));
node.parentNode!.removeChild(node); // 移除当前的文本节点。
return;
}
node.childNodes.forEach(dfs as any);
};
console.log("oldDom", oldDom);
node.innerHTML = oldDom;
console.log("node", node);
if (text) {
dfs(node);
}
}
export default useTextHighlight;