从零写一个富文本编辑器(三)—— 理解选区

2,310 阅读5分钟

这一小节我们聊聊选区,拖更了两年了好像。

什么是选区

在富文本编辑器领域,选区是一个非常重要的概念。mdn 的描述是这样的,选区表达了用户选择的文本范围 或者 当前插入位置。

Selection object represents the range of text selected by the user or the current position of the caret.

比如在掘金的页面中,点击红色文字区域后,看起来什么都没有发生。

image.png

实际上,浏览器选区已经发生了变化,通过 window.getSelection() 就可以获得当前浏览器的选区。

image.png

可以看到,选区包含以下字段

  • anchorNode。表达选区的开始节点
  • anchorOffset。表达开始位置在开始节点中的偏移,比如图中是9。9是怎么来的?我刚刚点击的字符串是 【这一小节我们聊聊选区】,在这个字符串中,第0个位置是【这】的左边,第一次位置是【一】的左边,以此类推,9的意思是【区】的左边。
  • baseNode。无需关注,可以理解为非标准属性,mdn已移除相关内容
  • baseOffset。同上
  • extentNode。同上
  • extentOffset。同上
  • focusNode。同 anchorNode,把开始换成结束即可
  • focusOffset。同 anchorOffset,把开始换成结束即可
  • isCollapsed。表达选区开始位置与结束位置是否相同,即 isCollapsed = anchorNode === focusNode && anchorOffset === focusOffset
  • rangeCount。表达当前页面中选区的数量。一般浏览器,rangeCount的最大值是1,但像 firefox,就允许有多个 range
  • type。表达当前选区的类型。比如 Caret(当前是光标态)、Range(当前是范围)

用户的选择操作映射到选区上,同时会触发 selectionchange 事件,编辑器监听该事件就可以根据选区变化执行特定的逻辑。

浏览器选区是不够的

为什么这么说,明明浏览器选区已经能够表达起始和结束的位置。

在L0级别的富文本编辑器中,几乎都是dom操作,那么浏览器选区的表达是足够的。

但dom是不可控的,在 L1 级别的富文本编辑器中,我们希望通过操作 model(数据)来改变 dom,让它更加可控。

浏览器这种表达选区的方式,局限性就非常明显,一个dom节点表达不了 model 中的位置。因此所有的 L1 级别的富文本编辑器都会抽象出自己的选区表达方式。

总结下来有以下几种

  1. [number, number]

这个可能有的人一开始就很奇怪了,一个数字怎么表达位置,我们正常使用的编辑器是有行的概念,一个数字是不是顶多只能表达第几行。这里我们就可以回头看看 前面提到的 easysync 这种数据模型,它用了字符串来表达一篇文档。那么一个数字就能表达文档中所有的位置,两个数字就能表达任意一个范围。

当然,往往这个形式是不符合直觉的,影响开发者体验。

  1. [{ line: number; offset: number }, { line: number; offset: number }]

一些编辑器就会在此基础上做一个转换,转换成这种形式,带来行的概念。通过表达第xx行的第xx个位置来描述位置。

这里我们简单描述一下一般编辑器是怎么实现 【浏览器选区】<-> 【编辑器选区】这个转换的

【浏览器选区】->【编辑器选区】

  1. 监听 selectionchange 事件
  2. 基于 window.getSelection() 取得 Range,计算得到编辑器选区

【编辑器选区】->【浏览器选区】

  1. 基于编辑器选区计算得到对应的浏览器选区,即 Range
  2. 调用 window.getSelection().addRange(range) 设置到浏览器中

块选区

如果有使用过 notion 或者 了解过相关概念 的人就会知道,notion 是可以选中块的,这在选区的描述中就会是更复杂的场景。

image.png

notion 使用了数组来描述块选区 image.png

受控选区

像金山文档、腾讯文档、Google Docs均属于 L2

但以上这种不是一个终极的形态,上面讲的都是 L0 L1 的富文本编辑器。特点是选区行为由浏览器控制。

  1. 选区位置可能影响输入行为。举个例子,在图中 link 的后面输入,新输入的内容脱离了 link ,实际的业务需求中,可能会要求内容跟随 link。

image.png

image.png

  1. 选区绘制可能不符合预期。如图,存在空白

image.png

那么 一个 L2 的富文本编辑器会怎么做?L2 富文本编辑器最核心的一个点就是受控,需要抛弃 contenteditable,自己实现输入,从而隔离掉浏览器选区的影响。

一般这种输入方式会在页面上放置一个隐藏的输入框,用户的输入会输入到这个输入框内,再经过js获取输入内容,渲染到页面上。

image.png

那么就出现一个悖论,如果需要显示选区,浏览器选区势必要在文本上,此时用户输入,无法触发隐藏输入框的输入事件,那么编辑器无法响应输入事件。如果需要响应输入事件,那么光标需要在隐藏输入框内,但浏览器不支持多个range,此时选区的蓝色矩形就无法出现。

因此,一般的L2编辑器会选择自己计算选区的位置信息,通过 dom 或者 svg 手动绘制出蓝色的矩形。

更极致的编辑器,如 腾讯文档、Google Docs,使用了 canvas 绘制富文本,同时选区也使用 canvas 绘制选区。

曾经看到过一个编辑器 TextBus,做了一个特别的操作,它用 iframe 来渲染文档,然后隐藏输入框放在 iframe外面,那么很自然就支持多个range了,这样既能实现受控输入,也不用手动画选区。

选区大致上就写到这里,上面提到了一个名词,叫做受控输入,相对应的有 非受控输入,这是下小节会分享的内容。