React中content editable div重渲染后恢复替换为span的原选中文本选区
解决方案
核心思路是给刚插入的span添加唯一临时标识,重渲染后通过该标识精准定位目标元素,选中后可删除临时标识不影响最终DOM结构。
改动点说明
- 新增
lastInsertedSpanId状态,存储最近插入的span的唯一临时ID - 插入span时添加
data-temp-id自定义属性,值为生成的唯一ID - useEffect中不再通过公共class查询,而是通过临时ID定位刚插入的span,选中后删除该临时属性避免残留
完整修改后代码
import { useEffect, useState } from "react"; import "./styles.css"; // 生成唯一ID的工具函数 const generateUniqueId = () => `span-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; export default function App() { const [htmlContent, setHtmlContent] = useState("<p>Hello World!</p>"); const [selectionRange, setSelectionRange] = useState(null); const [lastInsertedSpanId, setLastInsertedSpanId] = useState(null); useEffect(() => { if (lastInsertedSpanId) { // 通过临时ID精准定位刚插入的span const targetSpan = document.querySelector(`[data-temp-id="${lastInsertedSpanId}"]`); if (targetSpan && targetSpan.firstChild) { // 选中span内的全部文本 const range = new Range(); range.setStart(targetSpan.firstChild, 0); range.setEnd(targetSpan.firstChild, targetSpan.firstChild.length); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); // 删除临时属性,避免残留影响后续操作 targetSpan.removeAttribute('data-temp-id'); // 清空状态,避免下次渲染重复执行 setLastInsertedSpanId(null); } } }, [htmlContent, lastInsertedSpanId]); const storeSelectionRange = () => { if (window.getSelection().toString().length) { const sel = window.getSelection(); const range = sel.getRangeAt(0); setSelectionRange(range); } }; const onClick = () => { if (selectionRange) { // 恢复选区 let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(selectionRange); // 生成唯一临时ID const tempId = generateUniqueId(); // 创建span元素,添加临时ID属性 const newNode = document.createElement("span"); newNode.className = "text-bold"; newNode.setAttribute('data-temp-id', tempId); const newContent = document.createTextNode(sel.toString()); newNode.appendChild(newContent); // 替换选中内容 let range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(newNode); // 存储临时ID到状态 setLastInsertedSpanId(tempId); // 更新HTML内容触发重渲染 const newHtml = document.querySelector(".content").innerHTML; setHtmlContent(newHtml); } }; return ( <div className="container"> <div className="editor"> <div className="content" contentEditable="true" dangerouslySetInnerHTML={{ __html: htmlContent }} onBlur={storeSelectionRange} ></div> <button className="btn" onClick={onClick}> Bold </button> </div> <div className="selected-text">{htmlContent}</div> </div> ); }
额外优化建议
如果需要支持选中跨元素的文本,可进一步用Range的commonAncestorContainer属性做边界判断,避免插入元素结构异常。
内容的提问来源于stack exchange,提问作者BlackMath
相关产品推荐
相关产品推荐

