getselection() 返回的选区对象精确描述 dom 节点边界位置,anchornode/focusnode 为起止节点,anchoroffset/focusoffset 为对应偏移量(text 节点是字符索引,element 是子节点序号)。

getSelection() 返回的选区对象在 HTML 中到底选中了什么
HTML 里的选区(Selection)不是简单“高亮了一段文字”,而是精确到 DOM 节点边界的位置描述。调用 getSelection() 得到的对象,其 anchorNode 和 focusNode 是实际被选中的起止节点(可能是 Text 节点,也可能是 Element),而 anchorOffset/focusOffset 表示在该节点内的偏移量——对 Text 节点是字符索引,对 Element 节点则是子节点序号。
这意味着:如果你在 <p>Hello <strong>world</strong>!</p> 中选中 “world!”,anchorNode 可能是 <strong></strong> 元素,anchorOffset 是 0(表示从第一个子节点开始),而 focusNode 可能是 <p></p> 的最后一个 Text 节点,focusOffset 是 1(“!” 字符位置)。这种混合结构让直接读取或修改文本内容容易出错。
用 range.toString() 获取纯文本 vs range.cloneContents() 提取 HTML 片段
想提取用户选中的内容?别直接读 selection.toString() —— 它只返回纯文本,丢失所有结构;也别盲目用 selection.getRangeAt(0).cloneContents() —— 它返回 DocumentFragment,但若选区跨多个不连续元素(比如表格中隔行选中单元格),getRangeAt(0) 可能只返回第一个范围,漏掉其余部分。
- 要纯文本:用
selection.toString().trim()最安全,兼容所有浏览器,且自动处理换行/空格归一化 - 要保留 HTML 结构:先确认
selection.rangeCount > 0,再遍历selection.getRangeAt(i),对每个range调用range.cloneContents(),最后合并到一个临时容器里再序列化为字符串 - 注意:
range.commonAncestorContainer可帮你快速判断选区是否集中在某一段落内,避免无谓的 DOM 遍历
insertHTML() 在选区位置插入内容时的 DOM 污染风险
document.execCommand('insertHTML', false, '<em>hi</em>') 已废弃,且在现代浏览器中行为不一致:Chrome 可能将 <em></em> 插入到当前光标处,但 Safari 可能强制包裹在 <p></p> 或 <div> 中,Firefox 则可能拒绝解析含属性的标签。更严重的是,如果当前选区在 <code><pre class="brush:php;toolbar:false;"></pre> 或 <textarea></textarea> 内,该命令直接失效。
推荐改用原生方式:
- 先
const range = selection.getRangeAt(0) - 用
range.deleteContents()清除原有选区内容(防止残留) - 创建新节点:
const el = document.createElement('em'); el.textContent = 'hi'; - 用
range.insertNode(el)插入,它严格按 range 边界操作,不触发重排或格式猜测 - 如果需插入富文本 HTML 字符串,先用
DOMParser解析:new DOMParser().parseFromString(htmlStr, 'text/html').body.firstChild,再insertNode
监听选区变化时,selectionchange 事件的触发时机很微妙
document.addEventListener('selectionchange', handler) 确实能捕获鼠标拖选、键盘 Shift+方向键、甚至双击词选中等行为,但它**不会在 focus 切换时触发**(比如点击另一个 input 后,旧页面的选区已清空,但事件不发),也不保证每次 getSelection() 调用后都更新——尤其在 iframe 或 Shadow DOM 场景下,选区可能属于子上下文。
- 不要依赖单次事件回调就认为选区“稳定”:用户松开鼠标瞬间,选区可能还在动画过渡中(如 Safari 的平滑选中效果),建议加
setTimeout(() => { /* 重新 getSelection */ }, 0)延迟读取 - 跨 iframe 操作必须显式访问目标 frame 的
contentDocument.getSelection(),主文档的getSelection()对它无效 - 移动端 Safari 在输入法弹出时会临时清空选区,此时
selection.rangeCount === 0是正常现象,不必报错或重置状态
选区看似简单,但涉及 DOM 树遍历、文本节点切分、跨上下文边界和浏览器私有行为,最易出问题的地方往往不是逻辑,而是默认假设“选区一定落在 Text 节点里”或“一次事件就能拿到最终结果”。











