如何在多段落中高亮用户选中的文本(兼容跨段落选择)

本文详解如何使用 javascript 的 range api 正确实现跨段落文本高亮,避免 `surroundcontents` 报错,并提供稳定、可复用的解决方案。

在 Web 开发中,为用户选中的文本添加高亮样式看似简单,但一旦涉及跨

或其他块级元素的多段落选择,原生 Range.surroundContents() 就会抛出 DOMException: An attempt was made to use an object that is not, or is no longer, usable 错误。根本原因在于:surroundContents() 要求选区必须完全位于

单个连续的父节点内
——而跨段落选择(例如从第一段末尾拖到第二段开头)会生成一个跨越多个独立父元素的 Range,此时无法用一个 “包裹”整个不连续的 DOM 片段。

✅ 正确解法是改用 extractContents() + insertNode() 组合:

  • extractContents() 安全地将选区内所有节点(含文本、嵌套标签)提取为文档片段(DocumentFragment),自动处理跨节点边界;
  • 然后创建 ,将该片段作为子节点插入;
  • 最后用 insertNode() 将 原位插入到选区起始位置——这保证了语义完整性与 DOM 结构合法性。

以下是生产就绪的实现代码(已增强健壮性):

document.addEventListener("DOMContentLoaded", () => {
  document.addEventListener("mouseup", function (e) {
    const selection = window.getSelection();
    // 防止无选区或选区为空时出错
    if (!selection || selection.rangeCount === 0) return;

    const range = selection.getRangeAt(0);
    // 忽略仅包含空白字符的选区(如纯换行/空格)
    if (range.toString().trim() === "") return;

    // 创建高亮 span,添加 CSS 类便于样式控制
    const highlight = document.createElement("span");
    highlight.className = "highlight";
    highlight.appendChild(range.extractContents()); // ✅ 关键:安全提取内容
    range.insertNode(highlight); // ✅ 关键:原位插入包装节点
  });
});

配套 CSS(推荐使用类名而非通配 span,避免样式污染):

.highlight {
  background-color: #E8E288;
  padding: 1px 2px; /* 可选:微调视觉呼吸感 */
  border-radius: 2px;
}

⚠️ 注意事项:

  • 不要使用 surroundContents() 处理跨节点选区——它是设计用于“单容器内完整子树”的场景;
  • extractContents() 会移除原文本并返回其副本,因此 insertNode() 是必需的后续步骤;
  • 若需支持多次高亮/撤销,建议为 添加唯一 data-highlight-id 属性,并维护高亮索引;
  • 移动端需额外监听 touchend 事件,并注意 getSelection() 在部分 iOS 版本中延迟问题;
  • 如需保留原始格式(如 、链接等),此方案天然支持——因为 extractContents() 会完整保留子节点结构。

总结:理解 Range 的核心在于区分“逻辑选区”与“物理 DOM 结构”。跨段落高亮的本质不是“强行包裹”,而是“提取→封装→归位”。掌握 extractContents() + insertNode() 这一范式,不仅能解决高亮问题,也为富文本编辑器中样式应用、引用标记、注释插入等场景打下坚实基础。