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

React文本高亮功能异常:嵌套标签、Range错误及取消高亮求助

React文本高亮功能的问题修复方案

问题梳理

  • 已高亮文本“quick fox jumps”后,再次选中“fox”会生成嵌套<mark>标签,期望保持为<mark>quick fox jumps</mark>
  • 选中“jumps over”时抛出错误:Uncaught DOMException: Failed to execute 'surroundContents' on 'Range': The Range has partially selected a non-Text node.
  • 缺少取消已高亮文本的功能

解决方案

1. 避免嵌套<mark>标签

处理选区前先检查范围内是否存在已有的<mark>元素,若有则先将这些元素的内容还原为普通文本,再统一重新包裹,杜绝嵌套情况。

2. 解决跨节点选区的surroundContents错误

surroundContents要求选区必须完全包含非文本节点,不能部分选中。改用extractContents提取选区内容为文档片段,再将片段包裹进<mark>后插入原位置,兼容跨节点的选区场景。

3. 实现取消高亮功能

判断当前选中文本是否已被<mark>包裹:若是,则移除<mark>标签并还原文本;若否则执行高亮操作。

修复后的完整代码

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

export default function App() {
  const selectedRef = useRef(null);

  const handleSelection = () => {
    const selection = window.getSelection();
    if (selection.rangeCount === 0 || selection.isCollapsed) return;

    const range = selection.getRangeAt(0);
    const selectedText = range.toString().trim();
    if (!selectedText) return;

    // 检查选区是否包含已高亮的mark元素
    const markElements = Array.from(range.commonAncestorContainer.querySelectorAll('mark'));
    const isHighlighted = markElements.some(mark => range.intersectsNode(mark));

    if (isHighlighted) {
      // 取消高亮:移除mark标签,合并文本节点
      markElements.forEach(mark => {
        const parent = mark.parentNode;
        while (mark.firstChild) {
          parent.insertBefore(mark.firstChild, mark);
        }
        parent.removeChild(mark);
      });
      // 合并相邻文本节点,避免冗余空白
      const parent = selectedRef.current;
      const childNodes = Array.from(parent.childNodes);
      childNodes.forEach((node, index) => {
        if (node.nodeType === Node.TEXT_NODE && index > 0) {
          const prevNode = childNodes[index - 1];
          if (prevNode.nodeType === Node.TEXT_NODE) {
            prevNode.textContent += node.textContent;
            parent.removeChild(node);
          }
        }
      });
    } else {
      // 处理跨节点选区,替代surroundContents避免报错
      const fragment = range.extractContents();
      const mark = document.createElement("mark");
      mark.appendChild(fragment);
      range.insertNode(mark);
    }

    // 清除选区,避免重复触发
    selection.removeAllRanges();
  };

  return (
    <>
      <p onMouseUp={handleSelection} ref={selectedRef}>
        A quick fox jumps over a lazy dog.
      </p>
    </>
  );
}

代码说明

  • 防嵌套高亮:通过range.intersectsNode检测选区与已有<mark>的重叠情况,已高亮则先还原再处理。
  • 跨节点兼容:用extractContents提取选区内容为片段,再包裹<mark>插入,避开surroundContents的限制。
  • 取消高亮逻辑:遍历目标<mark>元素,将内部内容移至父节点后删除标签,同时合并相邻文本节点保持结构整洁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 12:32:18