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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 17:45:05