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

React中contenteditable div重渲染后如何恢复文本选中状态

解决方案

核心原因

你之前存储的原生Range对象绑定了旧DOM节点的引用,当setHtmlContent触发重渲染时,contentEditable容器内的DOM会被dangerouslySetInnerHTML完全替换,旧Range关联的节点已经被销毁,所以直接恢复旧Range无效。

改造方案

我们需要将选区信息序列化为不带DOM引用的普通对象存储,重渲染后再根据序列化信息在新DOM上重建选区:

  1. 新增选区序列化/反序列化工具函数
  2. 存储序列化后的选区信息,而非原生Range对象
  3. 重渲染完成后根据序列化信息重建选区

完整修改后代码

import { useEffect, useState, useRef } from "react";
import "./styles.css";

export default function App() {
  const [htmlContent, setHtmlContent] = useState("<p>Hello World!</p>");
  const [savedSelection, setSavedSelection] = useState(null);
  const editorRef = useRef(null);

  // 序列化选区:转换为可存储的普通对象
  const serializeRange = (range, root) => {
    const getNodePath = (node) => {
      const path = [];
      while (node && node !== root) {
        const parent = node.parentNode;
        if (!parent) break;
        const index = Array.from(parent.childNodes).indexOf(node);
        path.unshift(index);
        node = parent;
      }
      return path;
    };
    return {
      startPath: getNodePath(range.startContainer),
      startOffset: range.startOffset,
      endPath: getNodePath(range.endContainer),
      endOffset: range.endOffset,
    };
  };

  // 反序列化选区:根据存储的信息在新DOM上重建Range
  const deserializeRange = (serialized, root) => {
    const getNodeFromPath = (path) => {
      let node = root;
      for (const index of path) {
        node = node.childNodes[index];
        if (!node) return null;
      }
      return node;
    };
    const range = document.createRange();
    const startNode = getNodeFromPath(serialized.startPath);
    const endNode = getNodeFromPath(serialized.endPath);
    if (!startNode || !endNode) return null;
    range.setStart(startNode, serialized.startOffset);
    range.setEnd(endNode, serialized.endOffset);
    return range;
  };

  // 重渲染后恢复选区
  useEffect(() => {
    if (savedSelection && editorRef.current) {
      const range = deserializeRange(savedSelection, editorRef.current);
      if (range) {
        const sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
      }
    }
  }, [htmlContent, savedSelection]);

  const storeSelectionRange = () => {
    const sel = window.getSelection();
    if (sel.toString().length && editorRef.current) {
      const range = sel.getRangeAt(0);
      const serialized = serializeRange(range, editorRef.current);
      setSavedSelection(serialized);
    }
  };

  // 点击按钮时阻止默认行为,避免编辑器失焦
  const onButtonMouseDown = (e) => {
    e.preventDefault();
  };

  const onClick = () => {
    if (savedSelection && editorRef.current) {
      // 先重建当前选区
      const range = deserializeRange(savedSelection, editorRef.current);
      if (!range) return;
      const sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);

      // 包裹选中文本为加粗span
      const newNode = document.createElement("span");
      newNode.className = "text-bold";
      newNode.textContent = sel.toString();
      range.deleteContents();
      range.insertNode(newNode);

      // 更新内容,同时保存新的选区(选中刚插入的粗体文本)
      const newHtml = editorRef.current.innerHTML;
      const newRange = document.createRange();
      newRange.selectNodeContents(newNode);
      setSavedSelection(serializeRange(newRange, editorRef.current));
      setHtmlContent(newHtml);
    }
  };

  return (
    <div className="container">
      <div className="editor">
        <div
          ref={editorRef}
          className="content"
          contentEditable="true"
          dangerouslySetInnerHTML={{ __html: htmlContent }}
          onBlur={storeSelectionRange}
        ></div>
        <button className="btn" onMouseDown={onButtonMouseDown} onClick={onClick}>
          Bold
        </button>
      </div>
      <div className="selected-text">{htmlContent}</div>
    </div>
  );
}

对应styles.css补充样式:

.text-bold {
  font-weight: 700;
}
.content {
  border: 1px solid #ccc;
  padding: 8px;
  min-height: 100px;
  margin-bottom: 8px;
}

关键优化点

  • 用useRef绑定编辑器节点,避免每次用querySelector查找
  • 点击按钮时通过onMouseDown阻止默认行为,避免编辑器主动失焦
  • 加粗完成后自动保存新的选区为刚插入的粗体文本,方便后续连续加样式

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 10:36:09