求助:优化基于用户搜索的JavaScript<mark>标签高亮功能
问题与解决方案:基于用户输入的列表文本高亮功能优化
问题描述
我正在开发一个功能:根据用户输入,用<mark>标签高亮列表项中的文本,且无法简化<li>标签的HTML结构。目前仅在简单场景下可用,例如搜索“Ba”时能正确生成如下结果:
<li> <input type="checkbox" name="hide-balise" id="hide-balise"> <label for="hide-balise"><mark>Ba</mark>lise</label> </li>
但遇到两个核心问题:
- 大小写不敏感匹配失效:输入小写“css”时,希望保留原HTML中的大写字母,生成
<mark>CSS</mark>的高亮效果,但当前实现会强制替换为小写。 - 搜索词位置限制:当搜索内容位于文本中间或末尾时无法匹配,例如搜索“Sheet”无法添加
<mark>标签。
这些问题与正则表达式的写法直接相关,需要优化正则及整体功能。
原代码
HTML
<input type="text" name="search"> <ul> <li> <input type="checkbox" name="hide-balise" id="hide-balise"> <label for="hide-balise">Balise</label> </li> <li> <input type="checkbox" name="hide-div" id="hide-div"> <label for="hide-div"> <abbr lang="en" title="division">div</abbr> </label> </li> <li> <input type="checkbox" name="hide-div" id="hide-div-with-entities"> <label for="hide-div-with-entities"> <abbr lang="en" title="division"><div></abbr> </label> </li> <li> <input type="checkbox" name="hide-attribut" id="hide-attribut"> <label for="hide-attribut">Attribut</label></li> <li> <input type="checkbox" name="hide-css" id="hide-css"> <label for="hide-css"> <abbr lang="en" title="Cascading Style Sheets">mark</abbr> (<i lang="en">Cascading Style Sheets</i>) </label> </li> <li> <input type="checkbox" name="hide-attribut" id="hide-ancre"> <label for="hide-ancre">Ancre (<i lang="en">Anchor</i>)</label> </li> </ul>
JavaScript
const items = document.querySelectorAll('li'); function addMarks(event) { const regex = new RegExp(`>[a-zA-Z]*${event.currentTarget.value}[a-zA-Z]*<`, 'gi'); for (const li of items) { li.innerHTML.match(regex)?.forEach((match) => { li.innerHTML = li.innerHTML.replace( match, match.replaceAll(event.currentTarget.value, `<mark>${event.currentTarget.value}</mark>`), ); }); } } function clearMarks() { items.forEach((li) => { li.innerHTML = li.innerHTML.replaceAll('<mark>', ''); li.innerHTML = li.innerHTML.replaceAll('</mark>', ''); }); } document.querySelector('input[type=text]').addEventListener('input', (event) => { clearMarks(); if (event.currentTarget.value.trim() !== '') { addMarks(event); } });
问题分析
- 正则匹配范围狭窄:原正则
>[a-zA-Z]*${event.currentTarget.value}[a-zA-Z]*<仅匹配标签首尾包裹的连续字母内容,无法处理文本中间或末尾的搜索词。 - 大小写丢失:替换时直接使用用户输入的文本覆盖原内容,导致原文本的大小写无法保留。
- innerHTML操作风险:直接修改innerHTML可能破坏原有的HTML标签结构(如
<abbr>、<i>),且多次替换会导致性能问题。
优化后的代码
JavaScript
const items = document.querySelectorAll('li'); function addMarks(searchTerm) { // 转义正则特殊字符,避免输入特殊符号导致匹配异常 const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // 不区分大小写匹配搜索词,捕获原文本内容 const regex = new RegExp(`(${escapedTerm})`, 'gi'); items.forEach(li => { // 遍历所有文本节点,避免破坏原有HTML结构 const treeWalker = document.createTreeWalker(li, NodeFilter.SHOW_TEXT, null, false); let currentNode; while (currentNode = treeWalker.nextNode()) { // 跳过已被mark包裹的文本,避免重复标记 if (currentNode.parentElement.tagName !== 'MARK') { const originalText = currentNode.textContent; // 用捕获组保留原文本大小写,添加mark标签 const highlightedText = originalText.replace(regex, '<mark>$1</mark>'); if (highlightedText !== originalText) { const tempSpan = document.createElement('span'); tempSpan.innerHTML = highlightedText; // 将原文本节点替换为高亮后的节点集合 currentNode.replaceWith(...tempSpan.childNodes); } } } }); } function clearMarks() { items.forEach(li => { const marks = li.querySelectorAll('mark'); marks.forEach(mark => { // 移除mark标签,保留其内部内容 mark.replaceWith(...mark.childNodes); }); }); } document.querySelector('input[type=text]').addEventListener('input', (event) => { const searchTerm = event.currentTarget.value.trim(); clearMarks(); if (searchTerm) { addMarks(searchTerm); } });
关键改进点
- 正则转义:对搜索词中的正则特殊字符(如
.、*)进行转义,确保搜索词被精确匹配。 - 文本节点遍历:使用
TreeWalker遍历纯文本节点,直接操作文本内容,完全保留原HTML标签结构。 - 大小写保留:通过正则捕获组
$1直接使用匹配到的原文本内容包裹<mark>,完美保留原文本的大小写格式。 - 任意位置匹配:正则
(${escapedTerm})会匹配文本中任意位置的搜索词,不管是开头、中间还是末尾。 - 避免重复标记:检查文本节点的父元素是否为
<mark>,防止重复添加高亮标签。 - 高效清除高亮:直接移除
<mark>标签并保留其内容,比多次替换innerHTML更高效且安全。
内容的提问来源于stack exchange,提问作者Daniel Schreurs
相关产品推荐
相关产品推荐

