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

contenteditable div实现Python代码高亮时文字反转问题求助

问题根源分析

你遇到的文字“反转”其实是光标位置被强制重置到内容开头导致的假象:

  • 每次input事件触发时,直接用innerHTML替换整个div的内容,会完全重建内部DOM结构,浏览器会自动把光标定位到内容的起始位置。
  • 当你继续输入字符时,新内容会插入到光标所在的开头,比如输入d后光标在开头,接着输入e就变成ed,再输入f就成了fed,看起来像是def被反转了。
  • 用highlight.js时出现同样问题,也是因为它默认更新DOM时没有处理光标位置,导致光标被重置。
解决方案

要解决这个问题,核心是在更新DOM前后保存并恢复光标位置,同时优化内容更新逻辑避免重复破坏DOM结构:

1. 保存与恢复光标位置

在更新innerHTML前,先记录当前光标在内容中的偏移量,更新后再把光标移回原来的位置:

const writableDiv = document.getElementById("code-editor");
const pythonKeywords = ['def', 'class', 'if', 'else']; // 示例关键字
const keywordRegex = new RegExp('\\b(' + pythonKeywords.join('|') + ')\\b', 'g');

writableDiv.addEventListener('input', () => {
    // 保存光标位置
    const selection = window.getSelection();
    const range = selection.getRangeAt(0);
    const preCursorOffset = range.startOffset;
    const preCursorContainer = range.startContainer;
    // 计算光标在文本中的绝对位置(处理嵌套元素)
    const getCursorPosition = (element) => {
        const range = document.createRange();
        range.selectNodeContents(element);
        range.setEnd(preCursorContainer, preCursorOffset);
        return range.toString().length;
    };
    const cursorPos = getCursorPosition(writableDiv);

    // 更新内容并高亮
    let text = writableDiv.innerText;
    text = text.replace(keywordRegex, '<span style="color: #f200ff;">$&</span>');
    writableDiv.innerHTML = text;

    // 恢复光标位置
    const restoreCursor = (element, pos) => {
        const range = document.createRange();
        const selection = window.getSelection();
        let currentPos = 0;
        const traverse = (node) => {
            if (node.nodeType === Node.TEXT_NODE) {
                const textLength = node.textContent.length;
                if (currentPos + textLength >= pos) {
                    range.setStart(node, pos - currentPos);
                    range.collapse(true);
                    return true;
                }
                currentPos += textLength;
            } else {
                for (let i = 0; i < node.childNodes.length; i++) {
                    if (traverse(node.childNodes[i])) return true;
                }
            }
            return false;
        };
        traverse(element);
        selection.removeAllRanges();
        selection.addRange(range);
    };
    restoreCursor(writableDiv, cursorPos);
});

2. 优化高亮逻辑(可选)

避免每次都用innerText获取内容再转innerHTML,可以直接操作DOM节点,减少不必要的DOM重建:

  • 只对新增的内容进行高亮处理,而不是整个文本重新替换
  • 使用documentFragment来构建更新的DOM,一次性插入,提升性能

3. 使用highlight.js的正确姿势

如果用highlight.js,同样需要处理光标位置:

// 初始化highlight.js
hljs.registerLanguage('python', hljs.getLanguage('python'));

writableDiv.addEventListener('input', () => {
    // 先保存光标位置(逻辑同上)
    const selection = window.getSelection();
    const range = selection.getRangeAt(0);
    const preCursorOffset = range.startOffset;
    const preCursorContainer = range.startContainer;
    const getCursorPosition = (element) => {
        const range = document.createRange();
        range.selectNodeContents(element);
        range.setEnd(preCursorContainer, preCursorOffset);
        return range.toString().length;
    };
    const cursorPos = getCursorPosition(writableDiv);

    // 用highlight.js处理高亮
    const code = writableDiv.innerText;
    const result = hljs.highlight(code, {language: 'python'});
    writableDiv.innerHTML = result.value;

    // 恢复光标位置(逻辑同上)
    const restoreCursor = (element, pos) => {
        const range = document.createRange();
        const selection = window.getSelection();
        let currentPos = 0;
        const traverse = (node) => {
            if (node.nodeType === Node.TEXT_NODE) {
                const textLength = node.textContent.length;
                if (currentPos + textLength >= pos) {
                    range.setStart(node, pos - currentPos);
                    range.collapse(true);
                    return true;
                }
                currentPos += textLength;
            } else {
                for (let i = 0; i < node.childNodes.length; i++) {
                    if (traverse(node.childNodes[i])) return true;
                }
            }
            return false;
        };
        traverse(element);
        selection.removeAllRanges();
        selection.addRange(range);
    };
    restoreCursor(writableDiv, cursorPos);
});

内容的提问来源于stack exchange,提问作者Jam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 06:10:30