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

contentEditable富文本如何实现仅移除选中文本的格式

问题根因

原有移除格式的逻辑存在缺陷:当检测到选中文本位于目标格式标签(如<strong>、<em>)内时,直接选中整个格式标签节点替换为纯文本,没有校验选中范围是否仅覆盖格式节点的部分内容,导致同节点下未被选中的文本也被一并清除格式。

修复方案

核心调整点:

  • 移除格式前先拆分格式节点,仅将选中范围内的内容转为纯文本,范围外的内容保留原有格式包裹
  • 精准向上遍历定位匹配的格式父节点,避免因选中容器是文本节点导致的判断失效
  • 覆盖选中范围在格式节点头部、尾部、中间、全选四种场景的处理逻辑

修复后完整代码如下:

document.getElementById('bold').addEventListener('click', () => edit('STRONG'));
document.getElementById('italic').addEventListener('click', () => edit('EM'));

function edit(format) {
  const selection = document.getSelection();
  if (!selection.rangeCount) return;
  const range = selection.getRangeAt(0);
  let formatNode = range.commonAncestorContainer;

  // 向上遍历查找匹配的格式标签
  while (formatNode && formatNode.tagName !== format) {
    formatNode = formatNode.parentElement;
  }

  // 未命中格式标签,执行添加格式逻辑
  if (!formatNode) {
    const newFormatNode = document.createElement(format);
    const fragment = range.extractContents();
    newFormatNode.appendChild(fragment);
    range.insertNode(newFormatNode);
    // 重新选中新插入节点内容,方便后续连续操作
    const newRange = document.createRange();
    newRange.selectNodeContents(newFormatNode);
    selection.removeAllRanges();
    selection.addRange(newRange);
    return;
  }

  // 命中格式标签,执行移除格式逻辑
  const nodeRange = document.createRange();
  nodeRange.selectNodeContents(formatNode);

  // 场景1:选中范围覆盖整个格式节点,直接替换为纯文本
  const isFullSelected = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) === 0 
    && range.compareBoundaryPoints(Range.END_TO_END, nodeRange) === 0;
  if (isFullSelected) {
    const textNode = document.createTextNode(formatNode.textContent);
    formatNode.replaceWith(textNode);
    const newRange = document.createRange();
    newRange.selectNode(textNode);
    selection.removeAllRanges();
    selection.addRange(newRange);
    return;
  }

  // 场景2:仅选中部分内容,拆分原格式节点
  // 计算选中文本在整个格式节点内的起止偏移
  const startPoint = range.cloneRange();
  startPoint.selectNodeContents(formatNode);
  startPoint.setEnd(range.startContainer, range.startOffset);
  const start = startPoint.toString().length;
  const end = start + range.toString().length;
  const fullText = formatNode.textContent;

  // 拆分三段:选中前内容(保留格式)、选中内容(去格式)、选中后内容(保留格式)
  const fragment = document.createDocumentFragment();
  const beforeText = fullText.slice(0, start);
  const selectedText = fullText.slice(start, end);
  const afterText = fullText.slice(end);

  if (beforeText) {
    const beforeNode = document.createElement(format);
    beforeNode.textContent = beforeText;
    fragment.appendChild(beforeNode);
  }
  const plainTextNode = document.createTextNode(selectedText);
  fragment.appendChild(plainTextNode);
  if (afterText) {
    const afterNode = document.createElement(format);
    afterNode.textContent = afterText;
    fragment.appendChild(afterNode);
  }

  // 替换原格式节点
  formatNode.replaceWith(fragment);
  // 重新选中刚解除格式的文本
  const newRange = document.createRange();
  newRange.selectNode(plainTextNode);
  selection.removeAllRanges();
  selection.addRange(newRange);
  // 合并相邻同类型文本节点
  plainTextNode.parentElement.normalize();
}

配套HTML结构无需调整:

<button id="bold">B</button>
<button id="italic">I</button>
<div contentEditable="true">Lorem</div>
效果验证

按复现步骤测试可达到预期:

  1. 选中「Lorem」点击加粗,整段变为粗体
  2. 在Lorem后输入「ipsum」,新内容自动继承粗体格式
  3. 仅选中「ipsum」点击加粗取消格式,最终效果为 Lorem(粗体)+ ipsum(常规),未选中的Lorem保留粗体格式。

上述代码覆盖了单格式节点内部分选中的核心场景,如果需要支持跨多节点选中、多层嵌套格式剥离等复杂富文本能力,还需要补充节点遍历、嵌套标签清洗的逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:15:36