9Sky 九天 / 博客 / 从零开发富文本编辑器(2)-selection与range

从零开发富文本编辑器(2)-selection与range

2024 年 10 月 30 日 05:59


文章目录

在开始实现简单的富文本编辑器之前,要先了解几个富文本中涉及到的概念。

  1. 选区 Selection
  2. 范围 Range

1、选区 Selection

在实现富文本编辑器过程中,有个重要概念:选区。

在原生浏览器中,选区由 Selection 对象表示。

Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。

有两个需要明白的点:
锚点 anchor:选择内容时,按下鼠标/键盘开始选择的位置
焦点 focus:选择内容结束时,鼠标/键盘处于的位置

这两个点与选择方向有关

从左至右选择内容:anchor在左边,focus在右边

如图示:

lr.gif
从右至左选择内容:focus在左边,anchor在右边
如图示:

rl.gif

而从anchor到focus中间的内容,就是被选中的内容,也就是选择范围Range,不同的浏览器对于Range的限制不同。

  • Firefox:一个选区可以有多个Range。
  • 其他浏览器:一个选区Selection最多只能有一个Range。

2、Selection 对象

在浏览器中,可以通过 getSelection 获取选区对象。

const selection = window.getSelection();

被选中的文本都不直接在 Selection 中,而是存在Range 中。

属性

rangeCount

selection.rangeCount 属性可以返回该选区所包含的连续范围的数量,通过该属性可以获取选区 (Selection) 中包含多少个Range,通过 selection.getRangeAt(index) 可以获取到选区中第 index 个被选中的块。

const selection = window.getSelection();
console.log(selection.rangeCount); 
// 在firefox浏览器中显示多个选区具体数量,在其他浏览器中只会显示一个
const firstRange = selection.getRangeAt(0); // 被选中的内容,即上方粉红色的部分

Chrome浏览器:

Jietu20241030-121058.jpg

FireFox浏览器:

firefox.jpg

anchor

在 Selection 中,anchor 锚点相关的属性有 anchorNodeanchorOffset,只读。

  • anchorNode只读

    • 返回该选区起点所在的节点
  • anchorOffset

    • 返回一个数字,其表示的是选区起点在anchorNode中的位置偏移量。
    1. 如果 anchorNode 是文本节点,那么返回的就是从该文字节点的第一个字开始,直到被选中的第一个字之间的字数(如果第一个字就被选中,那么偏移量为零)。
    2. 如果 anchorNode 是一个元素,那么返回的就是在选区第一个节点之前的同级节点总数。(这些节点都是 anchorNode 的子节点)

focus

在 Selection 中,focus 焦点相关的属性有 focusNodefocusOffset,只读。

  • focusNode
    • 返回该选区终点所在的节点。
  • focusOffset
    • 返回一个数字,其表示的是选区终点在focusNode中的位置偏移量。
    1. 如果 focusNode 是文本节点,那么选区末尾未被选中的第一个字,在该文字节点中是第几个字(从 0 开始计),就返回它。
    2. 如果 focusNode 是一个元素,那么返回的就是在选区末尾之后第一个节点之前的同级节点总数。

举个例子:
当选中职场生存中的生存的时候,通过控制台输出结果,可以看到如下结果:

doc_1.jpg
根据结果列出已知条件:

∵ focusOffset = 2 锚点偏移值是2
∵ anchorOffset = 4 焦点偏移值是4
∵ anchorOffset>focusOffset
∴ 选区是从右到左的方向

anchorNode节点中,可以看到data是职场生存,也就是锚点所在的TextNode节点的值,同理在focusNode中的data是职场生存,也就是焦点所在的TextNode节点的值。

doc_2.jpg
以上的例子是当Range和Selection是在同一个TextNode节点的情况下。

那当Range是多个Node节点组合的情况是什么样呢,继续看例子:
这次选中了二级标题和内容,属于多个Node节点,然后再输出selection查看结果:

doc_3.jpg
可以看到

anchorOffset = 0 锚点偏移值是0
focusOffset = 5 焦点偏移值是5

发现焦点偏移值是5,不急,先看看anchorNode和focusNode是什么情况
anchorNode:
Pasted image 20241030172744.png

focusNode:

Pasted image 20241030172801.png

可以发现anchorNode和focusNode不再是同一个节点,因为选区横跨了两个节点区域
因此在anchorNode中,anchorOffset是0,从0开始
而在focusNode中,focusOffset是5,在focusNode中5结束
这样我们就可以获取选区选择的内容

接下来继续发散思维,如果横跨3+个区域呢,会是什么情况?
继续实验:

Pasted image 20241030183330.png
anchorNode显示是开始的部分,看起来没有问题

接下来看看focusNode
Pasted image 20241030183355.png

会发现,focusNode并不是我们之前预想的会包括上面选中全部的内容,而是最后一个Range节点区域。
问题来了,中间的内容哪里去了?Selection.toString()又是如何获取完整的被选中的内容呢?

前面提到过被选中的文本都不直接在 Selection 中,而是存在Range 中。,后面我们谈Range对象的时候会来解释这个问题。

isCollapsed

isCollapsed 用于判断选区的起始点和终点是否在同一个位置,即当前选区是光标所在的位置。

方法:

方法说明
collapse(parentNode: Node, offset?: number)设置光标位置
setPosition(parentNode, offset?)与collapse相同
collapseToStart()将光标移动到当前选区的开头
collapseToEnd()将光标移动到当前选区的结尾
toString()返回选区文本字符串
containsNode(node, aPartlyContained?)判断传入的DOM节点是否处于选区中
deleteFromDocument()将选区内容从文档中删除
(注:该方法执行后无法通过撤销恢复内容)
extend(node, offset)修改选区,以原来选区的 anchorNode 为基础,设置 focusNode,达到修改选区的效果。
addRange(range)向选区添加一个 Range
getRangeAt(index)获取Range,通过rangeCount可以获取到当前选区有多少个Range,通过当前方法可以获取对应的Range对象。
addRange(range)向选区添加一个 Range
removeAllRanges()删除所有的选区
removeRange(range)移除指定 Range
selectAllChildren(parentNode)选中 parentNode 的所有子元素,parentNode 本身不会被选中
(parentNode为TextNode时无效)
setBaseAndExtent(anchorNode,anchorOffset,focusNode,focusOffset)指定 anchorNode 与 focusNode 以及对应的offset设置选区

3、范围 Range对象

Range 的创建是通过 document 上的 createRange 创建的,创建之后,通过 range 的方法对 range 对象进行设置。

const range = document.createRange()

也可以通过selection中获取Range对象

const range = selection.getRangeAt(0)

属性

collapsed

  • 表示 Range 的起始位置和终止位置是否相同,与selection.isCollapsed作用类似
  • selection 中isCollapsed是选区内实时的光标状态
  • range
    • 有可能是新创建的Range,还没有生效
    • 或者从selection中提取出来的旧的属性
    • 不一定能直接代表当前状态,但可以代表之前或未来的状态(过去完成时或将来完成时)

startContainer

返回包含 Range 开始的节点。

endContainer

返回包含 Range 终点的节点。

startOffset

表示 Range 终点在 endContainer 中的位置。

endOffset

表示 Range 开始在 startContainer 中的位置。

commonAncestorContainer

startContainer 与 endContainer 所在的公共父节点。

  • range与selection不同的地方在于range没有左右之分,方向均为从左至右。

主要方法:

方法说明
cloneRange()克隆Range,返回一个不会影响原有Range的对象(深拷贝)
cloneContents()返回一个克隆 Range 中所有节点的HTML片段
collapse(toStart: boolean)设置光标到当前选择范围的端点(折叠范围)
insertNode(newNode)向 range 首部插入新的节点
selectNode(referenceNode)Range置为包含整个referenceNode及其内容。
selectNodeContents(referenceNode)设置 Range 包含referenceNode节点的内容,此节点中的内容会被Range选中
setStart(startNode, startOffset)设置 Range 的起点
setEnd(endNode, endOffset)设置 Range 的终点
setStartBefore(refenceNode, offset)refenceNode为基准,设置 Range 在refenceNode节点前的起点位置。
setStartAfter(refenceNode, offset)refenceNode为基准,设置 Range 在refenceNode节点后的起点位置。
surroundContents(newParent)Range 中的内容移动到一个新的节点
注:如果是跨节点会报错

接下来继续讨论上面的问题

当跨区选择范围后,selection只有锚点和焦点的节点的值,中间的跨区内容则存储在范围range中,在range中,可以通过cloneContents()获取范围内的HTML片段

image.png
可以看到通过cloneContents()就能获取到选择范围内的元素,之后再进行进一步的处理。

以上就是Selection和Range的概念了,虽然在现代的富文本编辑器中很少直接涉及这些Selection和Range,都是框架封装后暴露API提供给开发者使用,但是其底层原理依然和Selection与Range有关系,后面的从0实现LO级富文本编辑器也会调用Selection和Range,从而直观的去了解富文本的操作。