Skip to content

富文本编辑器的基础 - 光标与选区小结 #33

Description

@p2227

富文本编辑器的基础 (document.getSelection && range)

0x01 Selection 和 range之间的关系

document.getSelection 方法会返回一个selection对象,对象中会有0个或者1个range,当网页中有了光标,或者选中了某个区域,selection中就会有1个range,否则0个range

0x02 selection

selection的属性和方法有很多,需要注意的有以下几个

  • anchorNode 表示选区开始的节点,可以是一个文本节点。
  • anchorOffset 表示选区在anchorNode内的偏移
  • focusNode 表示选区结束的节点。
  • focusOffset 表示选区在focusNode内的偏移
  • isCollapsed 表示选区是否闭合,简单说来,如果只有一个光标,就是闭合的,如果选择了一段文字,则不闭合
  • getRangeAt() 方法,从selection 中获取range的方法

比如以下例子
输入框

在一个区域内有一个图片表情和左右两边的文本区域,从左到右记为text1, img, text2
假如你从左往右选,那么anchorNode是img, focusNode是text2
假如你从右往左选,那么focusNode是text2,anchorNode是img
假如是textNode, 那么offset就是里面的文字偏移,如果是一般的dom节点,则offset是其父元素中子元素的偏移,可以看下图的结果

结果

0x03 range

实际应用的时候,range比selection更加有用,range是没有方向的。
range的描述

假如我们现在的业务场景是做一个文字图片混输的移动版富文本编辑器。文字是手机自带键盘,表情框是web的一部分,当用户在这之间切换的时候,我们就需要进行保存光标还原光标手动插入表情等操作。

0x04 保存和还原光标

保存光标,到底是要保存range还是selection呢,本人一开始不太熟悉这两者关系时,是把selection对象存储起来的,但是document.getSelection 的返回值是单例的,永远指向当前网页所选择的区域。我在页面进行其他操作的时候,selection的指向就变了,于是表情就插入到奇怪的地方去了

表情乱了

所以我们应该是要保存range对象 ,并且把保存的range对象放回到selection对象中,替换掉当前selection中的range,就完成了还原光标的操作。

	const sel = document.getSelection();
	sel.removeAllRanges();
	for (let i = 0, len = savedSelection.length; i < len; i++) {
		sel.addRange(savedSelection[i]);
	}

0x05 在range中插入表情

我们如何在range中插入表情呢?扫一眼range对象的方法签名,是可以找到对应的操作方法的

[CEReactions] void insertNode(Node node);

当然插入也会分两种情况,

闭合的

闭合的光标

比如像这种场景,你可以理解了光标所在的位置也是一个dom,然后range.insertNode就是不停地往这个dom里面插入表情

1234<span class="caret"></span>56

由于是在range 的里面插入,所以当你插入了一个表情后,是会“撑大”range,像这样

1234<span class="caret"><img class="faceA"/></span>56

而且插入是在range内部的前面,当你再插入一个表情B时,页面的结构大概会是这样

1234<span class="caret"><img class="faceB"/><img class="faceA"/></span>56

插入两个表情的情况

所以当我们插入完一个表情后,应该把range的位置右移一下,此时新插入的表情就会在旧的表情后面了

 range.setStartAfter(faceNode); // 插入表情后,把光标的位置移动到表情的后面
 range.setEndAfter(faceNode);

非闭合

不闭合的选区

此时 我们要做的是,先把选区闭合 ,然后参考第一种情况

但是怎么闭合呢,扫一眼range的方法,确实是有一个闭合方法,

range.collapse(false); // 闭合选区,并且是闭合到结尾的位置

但是正常的人的输入习惯,如果我选中了一块区域,再输入一个表情,应该是想用这个表情替换掉这片区域。所以单纯地闭合是不行的,我们其实是要先删除range的内容

    if (!range.collapsed) {
      range.deleteContents(); 
    }

删除后,range就又变成闭合而且是在删除内容的后面,刚合符合前面一种情况,此时再走之前的逻辑即可。

最终的效果如下,range的初始状态是这样的

不闭合的选区
连续输入时的状态

参考资料

https://www.w3.org/TR/2019/WD-selection-api-20190820/#dom-selection
https://dom.spec.whatwg.org/#concept-range
https://github.com/quilljs/quill/blob/develop/core/selection.js

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions