命令式加粗:不用 execCommand 手写 Bold

目标

document.execCommand('bold') 已废弃、且生成标签不可控。本节课手写 toggleBold():用 Range 把选中的内容取出来、包一层 <strong>,再放回去。

运行环境:浏览器。运行方式:在 l1 的 index.html 里,把 <script> 换成下面内容,选中文字后点"加粗"按钮。

<script>
  const editor = document.getElementById('editor')
  function toggleBold() {
    const sel = window.getSelection()
    if (!sel.rangeCount || sel.isCollapsed) return // 没选中就不处理
    const range = sel.getRangeAt(0)
    const bold = document.createElement('strong')
    bold.appendChild(range.extractContents()) // 把选中内容"抽"出来
    range.insertNode(bold)                     // 包一层 strong 放回去
    sel.removeAllRanges()                      // 收起选区,避免残影
  }
  document.getElementById('bold').addEventListener('click', toggleBold)
</script>

配套加一个按钮 <button id="bold">加粗</button>。extractContents() 会剪切选区内容,insertNode 把包裹后的节点塞回原位——这就是"命令式格式"的最小骨架。

名词解释

extractContents():Range 的方法,把选区范围内的节点"剪切"出来,返回一个 DocumentFragment。
insertNode(node):把节点插入到 Range 的起点位置。
命令式格式(Imperative Formatting):直接操作 DOM 来改格式(加粗=包一层标签),与"声明式"(改数据模型、由框架重渲染)相对。execCommand 是浏览器内置的命令式,已废弃。

课后练习

练习:仿照 toggleBold,写一个 toggleItalic() 用 <em> 包裹选中内容。

答案:

function toggleItalic() {
  const sel = window.getSelection()
  if (!sel.rangeCount || sel.isCollapsed) return
  const range = sel.getRangeAt(0)
  const em = document.createElement('em')
  em.appendChild(range.extractContents())
  range.insertNode(em)
  sel.removeAllRanges()
}

总结

手写 toggleBold 是"去 execCommand"的第一课。你会发现它比想象中微妙:extractContents() 如果跨了多个元素(比如选中跨两段的文字),会把中间结构切碎,需要处理边界。这正是生产编辑器不直接操作裸 DOM 的原因——它们先在统一的数据模型里做"标记一段加粗",再一次性算出该生成什么 DOM。但亲手写一遍的价值在于:你彻底理解了"加粗"在 DOM 层面就是"用 <strong> 把一段内容包起来"。理解了最小骨架,再看 ProseMirror 的 addMark 就不会觉得是黑魔法。