使用getSelection获取选区如何忽略HTML标签 实现页面刷新后高亮功能
问题根源
原生Range对象的startOffset、endOffset是相对于选区起止位置所属的单个文本节点的偏移值,并非相对于外层容器的总文本长度。你给出的示例中<br>标签把#content内部的文本拆成了两个独立的文本节点,因此"hello"所在的第一个文本节点偏移为0-4,"John"所在的第二个文本节点偏移同样为0-4,才会出现输出重复的问题。
解决思路
将相对于单个文本节点的偏移转换为相对于外层目标容器的全局文本偏移,持久化存储该全局偏移值;页面重载后,再通过全局偏移反向重建选区,实现高亮。
完整实现代码
1. 选区保存逻辑
// 工具函数:计算当前节点偏移对应的全局文本偏移 function getGlobalOffset(container, targetNode, offsetInNode) { let globalOffset = 0; // 递归遍历容器内所有文本节点 function traverse(node) { if (node.nodeType === Node.TEXT_NODE) { if (node === targetNode) { globalOffset += offsetInNode; return true; // 找到目标节点,终止遍历 } else { globalOffset += node.length; } } else { for (let child of node.childNodes) { if (traverse(child)) return true; } } return false; } traverse(container); return globalOffset; } const contentEle = document.querySelector('#content'); // 避免重复绑定事件,selectionchange和mouseup/touchend绑定二选一即可 contentEle.addEventListener('mouseup', saveSelection); contentEle.addEventListener('touchend', saveSelection); function saveSelection() { const sel = window.getSelection(); if (sel.rangeCount === 0) return; const range = sel.getRangeAt(0); const selectedText = sel.toString().trim(); if (!selectedText) return; // 计算全局起止偏移 const startGlobal = getGlobalOffset(contentEle, range.startContainer, range.startOffset); const endGlobal = getGlobalOffset(contentEle, range.endContainer, range.endOffset); // 持久化存储,这里用localStorage示例,也可以存到后端 localStorage.setItem('selection', JSON.stringify({ start: startGlobal, end: endGlobal, text: selectedText // 存储文本用于后续校验,避免结构变化后高亮错位 })); }
2. 页面重载后重建选区并高亮
// 工具函数:通过全局偏移反向生成Range对象 function createRangeFromGlobal(container, startGlobal, endGlobal) { let currentOffset = 0; let start = null, end = null; const range = document.createRange(); function traverse(node) { if (node.nodeType === Node.TEXT_NODE) { const nodeLen = node.length; // 匹配起始位置 if (!start && currentOffset + nodeLen >= startGlobal) { start = { node, offset: startGlobal - currentOffset }; } // 匹配结束位置 if (!end && currentOffset + nodeLen >= endGlobal) { end = { node, offset: endGlobal - currentOffset }; } if (start && end) return true; currentOffset += nodeLen; } else { for (let child of node.childNodes) { if (traverse(child)) return true; } } return false; } traverse(container); if (start && end) { range.setStart(start.node, start.offset); range.setEnd(end.node, end.offset); } return range; } // 页面加载完成后执行恢复逻辑 window.addEventListener('load', () => { const saved = localStorage.getItem('selection'); if (!saved) return; const { start, end, text } = JSON.parse(saved); const contentEle = document.querySelector('#content'); // 先校验文本是否匹配,避免结构变化导致高亮错误 const fullText = contentEle.innerText.replace(/\s+/g, ' '); const currentText = fullText.slice(start, end).trim(); if (currentText !== text.trim()) return; // 重建选区 const range = createRangeFromGlobal(contentEle, start, end); // 高亮:两种方案二选一即可 // 方案1:用mark标签包裹选区 const mark = document.createElement('mark'); mark.style.backgroundColor = '#ffff00'; range.surroundContents(mark); // 方案2:用原生命令高亮 // const sel = window.getSelection(); // sel.removeAllRanges(); // sel.addRange(range); // document.execCommand('hiliteColor', false, '#ffff00'); // sel.removeAllRanges(); });
注意事项
- 需保证页面重载后
#content的内部DOM结构完全一致,否则全局偏移会出现错位 - 如果容器内容是动态生成的,建议在内容渲染完成后再执行恢复高亮的逻辑
- 如果需要支持跨标签、跨文本节点的复杂选区,建议额外存储选区对应的DOM路径,进一步提高兼容性
内容的提问来源于stack exchange,提问作者me Ya
相关产品推荐
相关产品推荐

