文章目录
在开始实现简单的富文本编辑器之前,要先了解几个富文本中涉及到的概念。
- 选区 Selection
- 范围 Range
1、选区 Selection
在实现富文本编辑器过程中,有个重要概念:选区。
在原生浏览器中,选区由 Selection 对象表示。
Selection
对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。
有两个需要明白的点:
锚点 anchor:选择内容时,按下鼠标/键盘开始选择的位置
焦点 focus:选择内容结束时,鼠标/键盘处于的位置
这两个点与选择方向有关
从左至右选择内容:anchor在左边,focus在右边
如图示:
从右至左选择内容:focus在左边,anchor在右边
如图示:
而从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浏览器:
FireFox浏览器:
anchor
在 Selection 中,anchor 锚点相关的属性有 anchorNode
与 anchorOffset
,只读。
anchorNode
只读- 返回该选区起点所在的节点
anchorOffset
- 返回一个数字,其表示的是选区起点在
anchorNode
中的位置偏移量。
- 如果
anchorNode
是文本节点,那么返回的就是从该文字节点的第一个字开始,直到被选中的第一个字之间的字数(如果第一个字就被选中,那么偏移量为零)。 - 如果
anchorNode
是一个元素,那么返回的就是在选区第一个节点之前的同级节点总数。(这些节点都是anchorNode
的子节点)
- 返回一个数字,其表示的是选区起点在
focus
在 Selection 中,focus 焦点相关的属性有 focusNode
与 focusOffset
,只读。
focusNode
- 返回该选区终点所在的节点。
focusOffset
- 返回一个数字,其表示的是选区终点在
focusNode
中的位置偏移量。
- 如果
focusNode
是文本节点,那么选区末尾未被选中的第一个字,在该文字节点中是第几个字(从 0 开始计),就返回它。 - 如果
focusNode
是一个元素,那么返回的就是在选区末尾之后第一个节点之前的同级节点总数。
- 返回一个数字,其表示的是选区终点在
举个例子:
当选中职场生存
中的生存的时候,通过控制台输出结果,可以看到如下结果:
∵ focusOffset = 2 锚点偏移值是2
∵ anchorOffset = 4 焦点偏移值是4
∵ anchorOffset>focusOffset
∴ 选区是从右到左的方向
在anchorNode
节点中,可以看到data是职场生存
,也就是锚点所在的TextNode节点的值,同理在focusNode
中的data是职场生存
,也就是焦点所在的TextNode节点的值。
以上的例子是当Range和Selection是在同一个TextNode节点的情况下。
那当Range是多个Node节点组合的情况是什么样呢,继续看例子:
这次选中了二级标题和内容,属于多个Node节点,然后再输出selection查看结果:
anchorOffset = 0 锚点偏移值是0
focusOffset = 5 焦点偏移值是5
发现焦点偏移值是5,不急,先看看anchorNode和focusNode是什么情况
anchorNode:
focusNode:
可以发现anchorNode和focusNode不再是同一个节点,因为选区横跨了两个节点区域
因此在anchorNode中,anchorOffset是0,从0开始
而在focusNode中,focusOffset是5,在focusNode中5结束
这样我们就可以获取选区选择的内容
接下来继续发散思维,如果横跨3+个区域呢,会是什么情况?
继续实验:
会发现,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片段
可以看到通过cloneContents()就能获取到选择范围内的元素,之后再进行进一步的处理。
以上就是Selection和Range的概念了,虽然在现代的富文本编辑器中很少直接涉及这些Selection和Range,都是框架封装后暴露API提供给开发者使用,但是其底层原理依然和Selection与Range有关系,后面的从0实现LO级富文本编辑器也会调用Selection和Range,从而直观的去了解富文本的操作。