React富文本编辑器contenteditable div光标无法移动问题求助
React富文本编辑器光标异常问题修复
问题现象
开发React富文本编辑器时遇到光标异常:每输入一个字符,插入点无法跟随输入位置,始终停留在左侧,字符被插入到光标左侧;即使手动将光标移到单词末尾后尝试输入,光标仍会回到左侧。
问题原因
- DOM重置导致光标丢失:同时使用
dangerouslySetInnerHTML和contentEditable时,每次输入触发setContent更新state,组件重新渲染,编辑器的innerHTML会被强制重置为state中的值,光标位置被强制恢复到初始状态。 - 不必要的focus干扰:
onInput事件中调用e.target.focus()会干扰浏览器默认的光标定位逻辑。
解决方案
- 移除
dangerouslySetInnerHTML的实时强制渲染,让contentEditable元素自主管理内部内容 - 仅在需要时同步编辑器内容到state(比如保存、格式操作后)
- 删除干扰光标的多余
focus()调用
修改后的代码
React组件代码
import React from "react"; import { useState, useRef } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBold, faCode, faFileCode, faFilm, faImage, faItalic, faLink, faList, faListOl, faQuoteRight, faStrikethrough, faUnderline, } from "@fortawesome/free-solid-svg-icons"; import "./textEditor.css"; const TextEditor = () => { const [content, setContent] = useState(""); const editorRef = useRef(null); const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [isUnderline, setIsUnderline] = useState(false); const [isStrikeThrough, setStrikeThrough] = useState(false); const [isBlockquote, setIsBlockquote] = useState(false); const [isHeading1, setIsHeading1] = useState(false); const [isHeading2, setIsHeading2] = useState(false); const [isOrderedList, setIsOrderedList] = useState(false); const [isUnorderedList, setIsUnorderedList] = useState(false); const [isInlineCode, setIsInlineCode] = useState(false); const [isCodeBlock, setIsCodeBlock] = useState(false); // 同步编辑器内容到state(按需调用) const syncContentToState = () => { if (editorRef.current) { setContent(editorRef.current.innerHTML); } }; const handleFormat = (style, value = null) => { switch (style) { case "bold": setIsBold(!isBold); break; case "italic": setIsItalic(!isItalic); break; case "underline": setIsUnderline(!isUnderline); break; case "strikeThrough": setStrikeThrough(!isStrikeThrough); break; case "formatBlock": if (value === "<blockquote>") { setIsBlockquote(!isBlockquote); value = isBlockquote ? "<p>" : "<blockquote>"; } else if (value === "<h1>") { setIsHeading1(!isHeading1); value = isHeading1 ? "<p>" : "<h1>"; } else if (value === "<h2>") { setIsHeading2(!isHeading2); value = isHeading2 ? "<p>" : "<h2>"; } else if (value === "<pre>") { setIsCodeBlock(!isCodeBlock); value = isCodeBlock ? "<p>" : "<pre>"; } break; case "insertOrderedList": setIsOrderedList(!isOrderedList); break; case "insertUnorderedList": setIsUnorderedList(!isUnorderedList); break; case "code": setIsInlineCode(!isInlineCode); break; case "createLink": const url = prompt("Enter the link URL:"); if (url) { document.execCommand("createLink", false, url); } break; default: break; } document.execCommand(style, false, value); syncContentToState(); }; return ( <div className="editor-wrapper"> <div className="editor"> <div className="toolbar"> <button className="editor-btn" onClick={() => handleFormat("bold")}> <FontAwesomeIcon icon={faBold} /> </button> <button className="editor-btn" onClick={() => handleFormat("underline")} > <FontAwesomeIcon icon={faUnderline} /> </button> <button className="editor-btn" onClick={() => handleFormat("italic")}> <FontAwesomeIcon icon={faItalic} /> </button> <button className="editor-btn" onClick={() => handleFormat("strikeThrough")} > <FontAwesomeIcon icon={faStrikethrough} /> </button> <button className="editor-btn" onClick={() => handleFormat("formatBlock", "<blockquote>")} > <FontAwesomeIcon icon={faQuoteRight} /> </button> <button className="editor-btn" style={{ fontWeight: "bold" }} onClick={() => handleFormat("formatBlock", "<h1>")} > H1 </button> <button className="editor-btn" style={{ fontWeight: "bold" }} onClick={() => handleFormat("formatBlock", "<h2>")} > H2 </button> <button className="editor-btn" onClick={() => handleFormat("insertUnorderedList")} > <FontAwesomeIcon icon={faList} /> </button> <button className="editor-btn" onClick={() => handleFormat("insertOrderedList")} > <FontAwesomeIcon icon={faListOl} /> </button> <button className="editor-btn" onClick={() => handleFormat("code")}> <FontAwesomeIcon icon={faCode} /> </button> <button className="editor-btn" onClick={() => handleFormat("formatBlock", "<pre>")} > <FontAwesomeIcon icon={faFileCode} /> </button> <button className="editor-btn" onClick={() => handleFormat("createLink")} > <FontAwesomeIcon icon={faLink} /> </button> <button className="editor-btn" onClick={() => handleFormat("insertImage", prompt("Enter image URL:")) } > <FontAwesomeIcon icon={faImage} /> </button> <button className="editor-btn" onClick={() => handleFormat( "insertHTML", '<iframe width="560" height="315" src="https://www.youtube.com/embed/VIDEO_ID" frameborder="0" allowfullscreen></iframe>' ) } > <FontAwesomeIcon icon={faFilm} /> </button> </div> <div ref={editorRef} className="text-editor" contentEditable="true" dir="ltr" onInput={syncContentToState} dangerouslySetInnerHTML={{ __html: content }} /> <br /> </div> </div> ); }; export default TextEditor;
CSS代码(无需修改)
.editor-wrapper { width: 100%; height: 500px; display: flex; flex-direction: column; align-items: center; } .editor { width: 70%; } .toolbar { border: 1px solid black; } .text-editor { border: 1px solid black; padding-left: 2px; min-height: 100px; resize: vertical; overflow: auto; } .editor:focus { border: none; } .editor-btn { background: none; border: none; cursor: pointer; color: #414141; } .editor-btn:hover { color: blue; } .text-editor blockquote { border-left: 4px solid #414141; margin-right: 2px; } .text-editor pre { background-color: #dbdbdb; word-wrap: break-word; } .text-editor code { background-color: #dbdbdb; }
关键修改说明
- 使用useRef绑定编辑器:通过
editorRef直接操作DOM元素,避免React重新渲染时重置编辑器内容。 - 按需同步内容:仅在输入和格式操作后同步内容到state,不再强制实时覆盖编辑器innerHTML。
- 移除干扰逻辑:删除
onInput中的e.target.focus(),让浏览器自主管理光标位置。
内容的提问来源于stack exchange,提问作者Lakshit Tiwari
相关产品推荐
相关产品推荐

