如何在富文本编辑器中实现跨多个HTML标签的选中文本高亮
如何在富文本编辑器中实现跨多个HTML标签的选中文本高亮
这个问题绝对是富文本编辑器开发中的经典痛点,我当初做类似功能的时候也卡了好一阵。核心难点就是原生DOM的选区(Selection)和范围(Range)API需要精细操作,才能处理跨标签的选中场景。下面我给你一步步拆解可行的原生JS解决方案:
首先得明确核心思路:我们需要把选中的文本片段,不管跨多少个标签,都拆分成「完全未选中」「部分选中」「完全选中」的节点,然后把所有选中的部分用带高亮类的<span>包裹起来。
第一步:获取并验证当前选区
先拿到用户的选中内容,判断是否有有效的选区:
const selection = window.getSelection(); // 如果没有选中内容或者选区是折叠的(只是光标),直接退出 if (selection.isCollapsed || selection.rangeCount === 0) return; // 取第一个选区范围(一般用户只会有一个选区) const range = selection.getRangeAt(0);
第二步:处理已有的高亮(避免嵌套)
如果之前已经有高亮的文本,直接在上面嵌套新的高亮会导致DOM结构混乱,所以先把已有的<span class="highlight">拆解掉,还原成原始节点:
function unwrapExistingHighlights(range) { // 找到选区范围内所有已存在的高亮元素 const highlights = Array.from(range.commonAncestorContainer.querySelectorAll('.highlight')); highlights.forEach(highlight => { const parent = highlight.parentNode; // 把高亮元素的子节点逐个移到父节点中 while (highlight.firstChild) { parent.insertBefore(highlight.firstChild, highlight); } // 删除空的高亮元素 parent.removeChild(highlight); }); }
第三步:拆分并包裹选中的文本片段
这是最关键的一步,我们要处理两种核心情况:选中内容在单个节点内、跨多个节点(起始和结束节点部分选中,中间节点完全选中)。这里用splitText()方法拆分文本节点,把选中的部分单独分离出来:
function wrapSelectedRange(range) { // 创建高亮元素 const highlightSpan = document.createElement('span'); highlightSpan.className = 'highlight'; // 情况1:选中内容跨多个节点 if (range.startContainer !== range.endContainer) { // 1. 拆分起始节点,把选中的后半部分分离出来 const startSplitNode = range.startContainer.splitText(range.startOffset); // 把分离出的起始选中部分放入高亮元素 highlightSpan.appendChild(startSplitNode); // 2. 处理中间完全被选中的节点,直接移到高亮元素里 let nextNode = startSplitNode.nextSibling; while (nextNode && nextNode !== range.endContainer) { // 先存下下一个节点,避免移动后丢失引用 const tempNode = nextNode; nextNode = nextNode.nextSibling; highlightSpan.appendChild(tempNode); } // 3. 拆分结束节点,把选中的前半部分分离出来并放入高亮元素 const endSplitNode = range.endContainer.splitText(range.endOffset); highlightSpan.appendChild(endSplitNode.previousSibling); // 4. 把高亮元素插入到DOM中合适的位置 range.startContainer.parentNode.insertBefore(highlightSpan, startSplitNode.nextSibling); } // 情况2:选中内容在单个节点内 else { // 拆分起始位置,分离出选中的部分 const selectedTextNode = range.startContainer.splitText(range.startOffset); // 拆分结束位置,把选中部分和未选中后半部分分开 selectedTextNode.splitText(range.endOffset - range.startOffset); // 插入高亮元素并把选中部分放进去 range.startContainer.parentNode.insertBefore(highlightSpan, selectedTextNode); highlightSpan.appendChild(selectedTextNode); } // 清除原有选区,避免后续操作冲突 selection.removeAllRanges(); }
第四步:整合功能并绑定事件
把上面的函数整合起来,绑定到鼠标松开或者高亮按钮的点击事件上:
function applyTextHighlight() { const selection = window.getSelection(); if (selection.isCollapsed || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); // 先拆解已有高亮 unwrapExistingHighlights(range); // 包裹新的高亮 wrapSelectedRange(range); } // 示例:绑定到鼠标松开事件,用户选中后松开鼠标就触发高亮 document.addEventListener('mouseup', applyTextHighlight);
补充说明
splitText()方法只对文本节点有效,所以如果选中的是元素节点(比如整个<b>nervous</b>),代码会直接把这个元素移到高亮span里,这正是我们想要的效果。- 如果你需要支持取消高亮,可以做类似的反向操作:选中已高亮的文本,拆解掉高亮span即可。
- 记得给
.highlight类加CSS样式,比如background-color: yellow;,这样才能看到高亮效果。
备注:内容来源于stack exchange,提问作者Roy Hoffman
相关产品推荐
相关产品推荐

