如何在contenteditable元素中获取选中文本周围的标签?
现有实现的核心问题
$("#textarea-ce *:contains("+selectedText+")")匹配逻辑不可靠:如果编辑器内有多处相同文本,会匹配到错误节点;如果选中的是节点内的部分文本,也无法定位到实际选中的节点- 标签创建存在语法错误:代码里
$("<"+currTagName+"><"+currTagName+"/>")是错误的闭合写法,会生成非法DOM结构 - 没有处理跨节点选中、部分节点选中的场景,这也是
extractContents偶尔只返回文本节点的核心原因
优化实现思路
第一步:标准化选区边界,解决部分选中节点的问题
如果选区的起点/终点落在某个文本节点的中间位置,先把该文本节点拆分成两个独立节点,让选区刚好完整包裹住若干个完整的子节点,这样extractContents就能拿到完整的嵌套标签结构。
第二步:收集当前选中文本已应用的格式标签
从选区的起始容器和结束容器分别向上遍历,直到到达根编辑器容器#textarea-ce,收集所有的格式标签(b/i/u/h1/h2等),去重后得到当前的格式栈。
第三步:切换目标格式
判断目标标签是否在格式栈中:
- 存在:从格式栈中移除该标签(执行移除格式操作)
- 不存在:向格式栈中添加该标签(执行添加格式操作)
第四步:重构选中内容的DOM结构
提取选区内容后,用新的格式栈逐层包裹内容,再插回原选区位置即可。
优化后示例代码
function toggleFormat(selectedTagName) { const editor = document.getElementById('textarea-ce'); const selection = document.getSelection(); if (!selection.rangeCount) return; let range = selection.getRangeAt(0); // 1. 标准化选区边界,拆分部分选中的节点 if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) { range.startContainer.splitText(range.startOffset); range.setStart(range.startContainer.nextSibling, 0); } if (range.endContainer.nodeType === Node.TEXT_NODE && range.endOffset < range.endContainer.length) { range.endContainer.splitText(range.endOffset); range.setEnd(range.endContainer, range.endOffset); } // 2. 收集当前选中内容的所有上层格式标签 const getFormatStack = (node) => { const stack = []; // 可以自定义格式标签白名单,避免误处理div/span等容器标签 const formatTagWhitelist = ['b','i','u','h1','h2','h3','h4','h5','h6']; while (node && node !== editor) { if (node.nodeType === Node.ELEMENT_NODE) { const tag = node.tagName.toLowerCase(); if (formatTagWhitelist.includes(tag)) { stack.push(tag); } } node = node.parentNode; } return stack; }; const startStack = getFormatStack(range.startContainer); const endStack = getFormatStack(range.endContainer); // 取两个栈的交集,得到选中内容共同应用的格式 const currentFormatStack = startStack.filter(tag => endStack.includes(tag)); // 3. 切换目标格式 const newFormatStack = currentFormatStack.includes(selectedTagName) ? currentFormatStack.filter(tag => tag !== selectedTagName) : [...currentFormatStack, selectedTagName]; // 4. 重构内容DOM const content = range.extractContents(); let wrapper = document.createDocumentFragment(); wrapper.appendChild(content); // 用新的格式栈逐层包裹 newFormatStack.forEach(tag => { const el = document.createElement(tag); el.appendChild(wrapper); wrapper = el; }); // 插入回原位置,恢复选区 range.insertNode(wrapper); selection.removeAllRanges(); range.selectNodeContents(wrapper); selection.addRange(range); }
注意事项
- 你用到的设置了
display: inline的h系列标签可以正常兼容,只要把对应的标签加入格式白名单即可 - 之前使用
extractContents仅返回文本节点的问题,就是因为未拆分部分选中的文本节点,标准化选区边界后即可解决 - 如果需要保留部分选中区域的差异化格式(比如选中部分加粗部分不加粗的文本,点加粗要全部移除加粗而非统一添加),可以调整收集格式栈的逻辑,遍历
extractContents返回的文档碎片里的所有节点,逐个处理每个节点的格式标签即可
内容的提问来源于stack exchange,提问作者the_indios
相关产品推荐
相关产品推荐

