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

React中如何阻止contentEditable div光标在onInput时跳至开头?

解决contentEditable div光标跳转问题的React语法高亮编辑器实现

你的问题出在每次输入后通过重写innerHTML更新高亮内容时,DOM结构被完全替换,浏览器自动将光标重置到元素开头。要解决这个问题,核心是在更新DOM前保存光标位置,更新完成后恢复光标。

下面是修改后的完整代码,关键添加了光标位置的保存与恢复逻辑:

const EditableElement = () => {
    const keywords = [
        "def",
        "if",
        "else",
        "elif",
        "for",
        "while",
        "try",
        "except",
    ];

    const [text, setText] = React.useState("");
    const elementRef = React.useRef(null);
    // 保存光标位置信息
    const selectionRef = React.useRef({
        startContainer: null,
        startOffset: 0,
        endContainer: null,
        endOffset: 0,
    });

    // 保存当前光标/选区位置
    const saveSelection = () => {
        const selection = window.getSelection();
        if (selection.rangeCount > 0 && elementRef.current.contains(selection.anchorNode)) {
            const range = selection.getRangeAt(0);
            selectionRef.current = {
                startContainer: range.startContainer,
                startOffset: range.startOffset,
                endContainer: range.endContainer,
                endOffset: range.endOffset,
            };
        }
    };

    // 恢复光标/选区位置
    const restoreSelection = () => {
        const selection = window.getSelection();
        selection.removeAllRanges();
        const range = document.createRange();
        try {
            range.setStart(selectionRef.current.startContainer, selectionRef.current.startOffset);
            range.setEnd(selectionRef.current.endContainer, selectionRef.current.endOffset);
            selection.addRange(range);
        } catch (e) {
            // 处理DOM更新后节点不存在的情况,默认将光标移到末尾
            const node = elementRef.current;
            range.selectNodeContents(node);
            range.collapse(false);
            selection.addRange(range);
        }
    };

    const handleChange = () => {
        // 先保存光标位置
        saveSelection();
        const value = elementRef.current.innerText;
        setText(value);
    };

    const highlightKeywords = (inputText) => {
        let highlightedText = inputText;
        keywords.forEach((keyword) => {
            const regex = new RegExp(`\\b(${keyword})\\b`, "g");
            highlightedText = highlightedText.replace(
                regex,
                `<span class="keyword">$1</span>`
            );
        });
        return highlightedText;
    };

    React.useEffect(() => {
        if (!elementRef.current) return;
        const html = highlightKeywords(text);
        elementRef.current.innerHTML = html;
        // 更新DOM后恢复光标位置
        restoreSelection();
    }, [text]);

    // 处理placeholder显示
    React.useEffect(() => {
        const node = elementRef.current;
        if (!text) {
            node.dataset.empty = true;
        } else {
            delete node.dataset.empty;
        }
    }, [text]);

    return (
        <div
            className="code-editor"
            placeholder="Write your Python code here..."
            contentEditable
            suppressContentEditableWarning
            onInput={handleChange}
            ref={elementRef}
            style={{
                minHeight: "100px",
                padding: "8px",
                border: "1px solid #ccc",
                fontFamily: "monospace",
            }}
        />
    );
};

关键修改说明:

  • 新增selectionRef保存光标位置信息,避免状态更新导致丢失
  • saveSelection函数在输入事件触发时,记录当前光标所在的DOM节点和偏移量
  • restoreSelection函数在DOM更新完成后,重新设置光标位置;如果原节点不存在(比如关键词被包裹成span),则默认将光标移到内容末尾
  • 移除了dangerouslySetInnerHTML,改为在useEffect中直接更新innerHTML,避免初始渲染和更新时的冲突
  • 添加了placeholder的简易实现(可通过CSS美化:.code-editor[data-empty]:before { content: attr(placeholder); color: #999; })

额外注意事项:

  • 关键词高亮的正则表达式\b可能对特殊字符或连写词有匹配问题,可根据需求调整
  • 复杂内容(比如多行、嵌套标签)下的光标恢复可能需要更精细的处理,比如计算高亮后节点的偏移量映射
  • 可以添加防抖处理,避免输入时频繁更新DOM影响性能

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 01:03:15