这一小节我们聊聊选区,拖更了两年了好像。
什么是选区
在富文本编辑器领域,选区是一个非常重要的概念。mdn
的描述是这样的,选区表达了用户选择的文本范围 或者 当前插入位置。
A
Selection
object represents the range of text selected by the user or the current position of the caret.
比如在掘金的页面中,点击红色文字区域后,看起来什么都没有发生。
实际上,浏览器选区已经发生了变化,通过 window.getSelection()
就可以获得当前浏览器的选区。
可以看到,选区包含以下字段
- 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 级别的富文本编辑器都会抽象出自己的选区表达方式。
总结下来有以下几种
[number, number]
这个可能有的人一开始就很奇怪了,一个数字怎么表达位置,我们正常使用的编辑器是有行的概念,一个数字是不是顶多只能表达第几行。这里我们就可以回头看看 前面提到的 easysync
这种数据模型,它用了字符串来表达一篇文档。那么一个数字就能表达文档中所有的位置,两个数字就能表达任意一个范围。
当然,往往这个形式是不符合直觉的,影响开发者体验。
[{ line: number; offset: number }, { line: number; offset: number }]
一些编辑器就会在此基础上做一个转换,转换成这种形式,带来行的概念。通过表达第xx行的第xx个位置来描述位置。
这里我们简单描述一下一般编辑器是怎么实现 【浏览器选区】<-> 【编辑器选区】这个转换的
【浏览器选区】->【编辑器选区】
- 监听
selectionchange
事件 - 基于
window.getSelection()
取得Range
,计算得到编辑器选区
【编辑器选区】->【浏览器选区】
- 基于编辑器选区计算得到对应的浏览器选区,即
Range
- 调用
window.getSelection().addRange(range)
设置到浏览器中
块选区
如果有使用过 notion
或者 了解过相关概念 的人就会知道,notion
是可以选中块的,这在选区的描述中就会是更复杂的场景。
notion
使用了数组来描述块选区
受控选区
像金山文档、腾讯文档、Google Docs均属于 L2
但以上这种不是一个终极的形态,上面讲的都是 L0 L1 的富文本编辑器。特点是选区行为由浏览器控制。
- 选区位置可能影响输入行为。举个例子,在图中 link 的后面输入,新输入的内容脱离了 link ,实际的业务需求中,可能会要求内容跟随 link。
- 选区绘制可能不符合预期。如图,存在空白
那么 一个 L2 的富文本编辑器会怎么做?L2 富文本编辑器最核心的一个点就是受控,需要抛弃 contenteditable
,自己实现输入,从而隔离掉浏览器选区的影响。
一般这种输入方式会在页面上放置一个隐藏的输入框,用户的输入会输入到这个输入框内,再经过js获取输入内容,渲染到页面上。
那么就出现一个悖论,如果需要显示选区,浏览器选区势必要在文本上,此时用户输入,无法触发隐藏输入框的输入事件,那么编辑器无法响应输入事件。如果需要响应输入事件,那么光标需要在隐藏输入框内,但浏览器不支持多个range,此时选区的蓝色矩形就无法出现。
因此,一般的L2编辑器会选择自己计算选区的位置信息,通过 dom 或者 svg 手动绘制出蓝色的矩形。
更极致的编辑器,如 腾讯文档、Google Docs,使用了 canvas 绘制富文本,同时选区也使用 canvas 绘制选区。
曾经看到过一个编辑器 TextBus,做了一个特别的操作,它用 iframe 来渲染文档,然后隐藏输入框放在 iframe外面,那么很自然就支持多个range了,这样既能实现受控输入,也不用手动画选区。
选区大致上就写到这里,上面提到了一个名词,叫做受控输入,相对应的有 非受控输入,这是下小节会分享的内容。