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

不使用系统剪贴板实现选中内容克隆并保留指定格式的方案咨询

内部剪贴板选区内容提取实现方案

前置预处理:统一选区起始方向

先矫正用户反选场景下的节点顺序,确保起始节点永远在结束节点之前:

const selection = window.getSelection();
if (selection.rangeCount === 0) return;

let startNode = selection.anchorNode;
let startOffset = selection.anchorOffset;
let endNode = selection.focusNode;
let endOffset = selection.focusOffset;

// 矫正选区顺序,避免用户从下往上反选导致的顺序颠倒
const position = startNode.compareDocumentPosition(endNode);
if (position & Node.DOCUMENT_POSITION_PRECEDING || (position === 0 && startOffset > endOffset)) {
  [startNode, endNode] = [endNode, startNode];
  [startOffset, endOffset] = [endOffset, startOffset];
}

核心逻辑:遍历选区节点并过滤标签

通过递归遍历节点,仅保留允许的格式化标签,过滤span、div、h1等非预期标签,同时保留文本和合法格式:

  • 仅保留白名单内的格式标签:b/strong(粗体)、i/em(斜体)、s/del(删除线)
  • 非白名单元素直接跳过标签本身,仅递归处理子节点内容
  • 文本节点根据选区偏移量截取对应内容
// 定义允许保留的格式标签白名单
const ALLOWED_TAGS = new Set(['b', 'strong', 'i', 'em', 's', 'del']);
const resultFragment = document.createDocumentFragment();
let isInSelectionRange = false;

function traverseNode(node) {
  // 处理文本节点
  if (node.nodeType === Node.TEXT_NODE) {
    if (node === startNode && node === endNode) {
      // 选区完全在同一个文本节点内
      resultFragment.appendChild(document.createTextNode(node.textContent.slice(startOffset, endOffset)));
      isInSelectionRange = false;
      return;
    }
    if (node === startNode) {
      // 到达选区起始节点
      resultFragment.appendChild(document.createTextNode(node.textContent.slice(startOffset)));
      isInSelectionRange = true;
      return;
    }
    if (node === endNode) {
      // 到达选区结束节点
      resultFragment.appendChild(document.createTextNode(node.textContent.slice(0, endOffset)));
      isInSelectionRange = false;
      return;
    }
    if (isInSelectionRange) {
      resultFragment.appendChild(node.cloneNode(false));
    }
    return;
  }

  // 处理元素节点
  if (node.nodeType === Node.ELEMENT_NODE) {
    let formatWrapper = null;
    // 如果是允许保留的格式标签,创建对应标签作为容器
    if (ALLOWED_TAGS.has(node.tagName.toLowerCase()) && (isInSelectionRange || node.contains(startNode))) {
      formatWrapper = document.createElement(node.tagName.toLowerCase());
    }
    // 递归处理当前元素的所有子节点
    Array.from(node.childNodes).forEach(child => traverseNode(child));
    // 把处理后的内容塞进格式标签,再加入结果片段
    if (formatWrapper && formatWrapper.childNodes.length > 0) {
      while (resultFragment.lastChild && formatWrapper.parentNode !== resultFragment) {
        formatWrapper.prepend(resultFragment.lastChild);
      }
      resultFragment.appendChild(formatWrapper);
    }
  }
}

// 可替换为你的应用内容容器节点,缩小遍历范围提升性能
traverseNode(document.body);

// 最终得到的处理后HTML字符串,可存入自定义剪贴板历史数组
const filteredHtml = resultFragment.firstChild ? new XMLSerializer().serializeToString(resultFragment) : '';

copy事件绑定逻辑

你已经拦截了copy事件,直接在自定义回调中执行上述逻辑即可:

  • 调用e.preventDefault()阻止默认复制行为
  • 执行上述代码拿到处理后的HTML字符串
  • 写入你自己维护的剪贴板历史存储中,后续粘贴时直接将HTML插入目标容器即可

边界情况优化

  • 若选中内容跨多个块级元素,过滤块级标签后内容会自动拼接,不会保留多余的块级样式
  • 可限制遍历范围为你的应用内容容器,不用遍历整个document,大幅提升性能
  • 空选区直接跳过处理,避免报错

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 22:18:02