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>
效果验证
按复现步骤测试可达到预期:
- 选中「Lorem」点击加粗,整段变为粗体
- 在Lorem后输入「ipsum」,新内容自动继承粗体格式
- 仅选中「ipsum」点击加粗取消格式,最终效果为 Lorem(粗体)+ ipsum(常规),未选中的Lorem保留粗体格式。
上述代码覆盖了单格式节点内部分选中的核心场景,如果需要支持跨多节点选中、多层嵌套格式剥离等复杂富文本能力,还需要补充节点遍历、嵌套标签清洗的逻辑。
内容的提问来源于stack exchange,提问作者xRay
相关产品推荐
相关产品推荐

