contenteditable编辑器语法高亮后光标跳至0位置的问题求助
解决contenteditable语法高亮后光标偏移越界问题
在Firefox最新版中,使用contenteditable div结合JavaScript实现语法高亮时,将let/const等关键词替换为<span class="keyword">const</span>后光标会跳至开头。尝试保存光标位置恢复时,出现Uncaught DOMException: Selection.setPosition: The offset is out of range.错误。
问题根源
- DOM结构被破坏:直接修改
innerHTML后,编辑器原有的单一文本节点会被拆分为多个span和文本节点的混合结构,editor.firstChild不再是可直接设置偏移的纯文本节点。 - 偏移量计算错误:替换关键词后,HTML字符串长度和纯文本长度不一致,原代码中用
startOffset + highlightedText.length计算的偏移量无法对应新DOM结构中的有效位置。 - 删除操作未处理:原代码在删除内容时直接跳过高亮逻辑,导致已高亮的关键词标签残留,进一步干扰DOM结构和光标定位。
解决方案
核心思路是基于纯文本的全局偏移量保存和恢复光标,不受DOM结构变化影响:
- 保存光标在编辑器纯文本中的全局位置,而非相对于当前节点的局部偏移。
- 替换HTML后,遍历DOM节点累计文本长度,找到全局偏移量对应的目标节点和局部偏移。
- 统一处理输入和删除操作,确保每次内容变化后都重新执行高亮并恢复光标。
修改后的完整代码
const editor = document.getElementById("editor"); let previousText = ""; // 获取光标在纯文本中的全局偏移量 function getGlobalCursorOffset(element) { const selection = window.getSelection(); if (selection.rangeCount === 0) return 0; const range = selection.getRangeAt(0); const preRange = document.createRange(); preRange.selectNodeContents(element); preRange.setEnd(range.startContainer, range.startOffset); return preRange.toString().length; } // 根据全局偏移量定位光标 function setCursorByGlobalOffset(element, offset) { const selection = window.getSelection(); selection.removeAllRanges(); const range = document.createRange(); let currentOffset = 0; const traverse = node => { if (node.nodeType === Node.TEXT_NODE) { if (currentOffset + node.length >= offset) { range.setStart(node, offset - currentOffset); range.collapse(true); selection.addRange(range); return true; } currentOffset += node.length; } else if (node.nodeType === Node.ELEMENT_NODE && node.childNodes) { for (let i = 0; i < node.childNodes.length; i++) { if (traverse(node.childNodes[i])) return true; } } return false; }; traverse(element); } // 执行语法高亮替换 function applySyntaxHighlight(text) { const keywords = ["function", "var", "let", "const", "if", "else", "for", "while", "return"]; const keywordPattern = new RegExp(`\\b(${keywords.join("|")})\\b`, "g"); return text.replace(keywordPattern, '<span class="keyword">$&</span>'); } editor.addEventListener("input", function () { const currentText = editor.textContent; // 先保存光标全局位置 const cursorOffset = getGlobalCursorOffset(editor); // 应用语法高亮 const highlightedHTML = applySyntaxHighlight(currentText); editor.innerHTML = highlightedHTML; // 恢复光标位置 setCursorByGlobalOffset(editor, cursorOffset); previousText = currentText; }); editor.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); const selection = window.getSelection(); const range = selection.getRangeAt(0); // 创建换行节点,兼容contenteditable换行逻辑 const br = document.createElement("br"); range.insertNode(br); // 移动光标到换行后 range.setStartAfter(br); range.setEndAfter(br); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); // 手动触发input事件,确保换行后内容也能高亮 editor.dispatchEvent(new Event("input")); } });
关键改进点
- 全局偏移量:通过
getGlobalCursorOffset计算光标在纯文本中的绝对位置,脱离DOM结构限制。 - 精准定位:
setCursorByGlobalOffset遍历DOM节点,根据全局偏移量找到对应的文本节点和局部偏移,避免越界错误。 - 全场景处理:删除内容时不再跳过高亮逻辑,确保DOM结构始终正确。
- 换行兼容:Enter键处理后手动触发input事件,保证换行后的内容也能应用高亮。
内容的提问来源于stack exchange,提问作者user22191183
相关产品推荐
相关产品推荐

