You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

contenteditable编辑器语法高亮后光标跳至0位置的问题求助

解决contenteditable语法高亮后光标偏移越界问题

在Firefox最新版中,使用contenteditable div结合JavaScript实现语法高亮时,将let/const等关键词替换为<span class="keyword">const</span>后光标会跳至开头。尝试保存光标位置恢复时,出现Uncaught DOMException: Selection.setPosition: The offset is out of range.错误。

问题根源

  1. DOM结构被破坏:直接修改innerHTML后,编辑器原有的单一文本节点会被拆分为多个span和文本节点的混合结构,editor.firstChild不再是可直接设置偏移的纯文本节点。
  2. 偏移量计算错误:替换关键词后,HTML字符串长度和纯文本长度不一致,原代码中用startOffset + highlightedText.length计算的偏移量无法对应新DOM结构中的有效位置。
  3. 删除操作未处理:原代码在删除内容时直接跳过高亮逻辑,导致已高亮的关键词标签残留,进一步干扰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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 10:25:59