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
相关产品推荐
相关产品推荐

