-
为什么需要 Ref?
Slate 中的节点路径会因为应用到文档的 op
而随时发生改变。比如[2]
路径,会因为执行了一个 removeNode [1]
的操作路径从[2]
变为[1]
。 Ref
作用是会根据 op
自动保持住正确的路径,从而确保拿到的路径不会错误。
// 对 [2] 使用 ref
const pathRef = Editor.PathRef(editor, [2]);
// 删除 [1]
Transform.removeNode(editor, [1]);
// ref 会根据 op 操作自动保持住正确的路径
console.log(pathRef.current); // [1]
之所以节点路径能够保持正确,是因为内部 transfrom 来就是处理这种事情。分别有 path.transfrom
, range.transfrom
, point.transfrom
。下面我们就来看一下各种 transfrom 是如何处理的。
-
Path.Transform 纠正 path
pathTransform 的参数主要有三个:
path
:需要被纠正的path
op
:当前执行的operation
options.affinity
:affinity
表示的是方向,
/** * Transform a point by an operation. */
transform(
path: Path,
op: Operation,
options: { affinity?: 'forward' | 'backward' | null } = {}
): Point | null {
return produce(path, p => {
const { affinity = 'forward' } = options
const { path, offset } = p
// ... Path.transform implementation
});
}
-
insert_node
以 { type: 'insert_node', path: [0, 1], node: Text }
这个op 为例子受到插入节点影响的有三种:
- 完全相同的路徑 (对应右图的红色节点) 。
- 位于 op 右边的节点 (对应右图紫色节点) 。
- 以 op 为祖先的路径 (对应右图的绿色节点) 。
在 op.path
的位置插入一个新节点,上诉的节点相当于都向右挪动,对应的 path
改变了,需要在op.path.length - 1
的地方都需要 +1
。
case 'insert_node': {
const { path: op } = operation
if (
Path.equals(op, p) ||
Path.endsBefore(op, p) ||
Path.isAncestor(op, p)
) {
p[op.length - 1] += 1
}
break
}
-
remove_node
以 { type: 'remove_node', path: [0, 1], node: Element }
为例子受到插入节点影响的有俩种:
-
完全相同的路徑 & 以 op 为祖先的路径 (对应左图的红色节点) 。
- 因为删除了 Element 其子节点也会跟着一起删除。在最新的视图找不到了所以直接返回 null。
-
位于 op 右边的节点 (对应右图紫色节点) 。
- 删除一个节点,紫色节点相当于都向左前进,需要在
op.path.length - 1
的地方-1
。
- 删除一个节点,紫色节点相当于都向左前进,需要在
case 'remove_node': {
if (Path.equals(op.path, p) || Path.isAncestor(op.path, p)) {
return null
} else if (Path.endsBefore(op.path, p)) {
p[op.path.length - 1] -= 1;
}
break;
}
-
split_node
以 { type: 'split_node', path: [0], position: 1 }
为例子受到合并节点影响的有三种:
-
完全相同的路徑 (对应左图的黑色节点) 。这里受到會受到
options.affinity
的影响,forward
表示向前, 需要将原始路徑的index + 1
。指向新增的element
节点。backward
我理解就是保持不动,则不需要处理。
case 'split_node': { const { path: op, position } = operation if (Path.equals(op, p)) { if (affinity === 'forward') { p[p.length - 1] += 1 } else if (affinity === 'backward') { // Nothing, because it still refers to the right path. } else { return null } } // xxx break } ```
-
位于 op 右边的节点 (对应右图紫色节点) 。因为 splitNode 的时候会在 [1] 这个地方插入新节点,对于紫色节点来说相当于后退了。所以需要在
op.path.length - 1
的地方+1
。else if (Path.endsBefore(op, p)) { p[op.length - 1] += 1 } ```
-
以 op 为祖先的路径并且是在 position 位置之后 (对应右图的橘色节点以及左图的橘色框区域) 。这里需要分为2步理解
- 原节点会移动到后一个节点,相当于后退了。所以需要在
op.path.length - 1
的地方+1
。 - 原节点位置在其前面有
position
个节点。移动之后前面相当于前面没有节点了,所以需要在op.length
地方-position
。
else if (Path.isAncestor(op, p) && path[op.length] >= position) { p[op.length - 1] += 1 p[op.length] -= position } ```
- 原节点会移动到后一个节点,相当于后退了。所以需要在
-
merge_node
以 { type: 'merge_node', path: [1], position: 1 }
为例子受到合并节点影响的有两种:
-
完全相同的路徑 (对应左图的绿色节点) 和位于 op 右边的节点 (对应右图紫色节点) 。
- 对于紫色节点来说相当于前面少一个节点,紫色节点相当于都向左前进,需要在
op.path.length - 1
的地方-1
。 - 对于绿色节点,其实在最新的model已经找不到了,但是 slate 处理方式也是在
op.path.length - 1
的地方-1
。
- 对于紫色节点来说相当于前面少一个节点,紫色节点相当于都向左前进,需要在
-
以 op 为祖先的路径 (对应右图的红色节点) 。这里需要分为2步理解
- 节点会合并到前一个节点,相当于前进了。所以需要在
op.path.length - 1
的地方-1
。 - 节点是在前一个节点的
position
插入。相当于前面有position
个节点,所以需要在op.length
地方+position
- 节点会合并到前一个节点,相当于前进了。所以需要在
case 'merge_node': {
const { path: op, position } = operation
if (Path.equals(op, p) || Path.endsBefore(op, p)) {
p[op.length - 1] -= 1
} else if (Path.isAncestor(op, p)) {
p[op.length - 1] -= 1
p[op.length] += position
}
break
}
-
move_node
move_node 情况比较复杂,我花了很长时间才理解这段代码😓😓😓😓😓😓,需要分3种大情况讨论:
- 自己移动到自己
- 同层级移动
- 非同层级的移动
-
自己移动到自己
为了避免死循环,这种情况直接 return;
case 'move_node': {
const { path, newPath } = op;
if (Path.equals(path, newPath)) {
return;
}
}
-
同层级移动
{ type: 'move_node', path: [1], newPath: [3] }
为例子,受影响的主要是下面4部分:
-
和
path
完全相同的路徑 & 以path
为祖先的路径 (对应左图/右图的红色节点) 。-
移动的时候相当于
path
整体移动到newPath
。所以只需要在p
中属于path
替换成newPath
即可。比如[1,0]
属于path
的部分就是[1]
, 所以只需要将[1]
替换成[3]
变成[3, 1]
。-
if (Path.isAncestor(path, p) || Path.equals(path, p)) { const copyPath = newPath.slice(); // 非同层级移动逻辑 // newPath + 原来其子节点的位置 return copyPath.concat(p.slice(path.length)); }
-
-
-
和
newPath
完全相同的路徑 & 以newPath
为祖先的路径 (对应左图/右图的绿色节点) 。- 同层节点的移动如果
path
是newPath
前面的节点,移动到newPath
位置,那么相当于前面少了一个节点,绿色节点前进了。所以需要在path.length - 1
地方-1
。 (从左到右观察绿色节点) - 同层节点的移动如果
path
是newPath
后面的节点,移动到newPath
位置,那么相当于前面多了一个节点,绿色节点后退了。所以需要在path.length - 1
地方+1
。 (从右到左观察绿色节点)
else if (Path.isAncestor(newPath, p) || Path.equals(newPath, p)) { if (Path.isSibling(newPath, path)) { if (Path.endsBefore(path, p)) { p[path.length - 1] -= 1 } else { p[path.length - 1] += 1 } } else { // 非同层级移动逻辑 } } ```
- 同层节点的移动如果
-
位于 newPath 右边的节点 (对应左图/右图橘色节点) 。
-
同层级移动 newPath 右边的节点位置不会变化
-
-
位于
path
右边的节点 (对应左图/右图紫色节点) 。相当于前面的节点移动了,紫色节点前进,所以需要在path.length - 1
地方-1
else if (Path.endsBefore(path, p)) { p[path.length - 1] -= 1; } ```
-
非同层级移动
{ type: 'move_node', path: [1], newPath: [3] }
为例子,受影响的主要是下面4部分:
-
path
完全相同的路徑 & 以path
为祖先的路径 (对应左图/右图的红色节点) 。-
因为跨层级的时候如果
path
在newPath
前面,那么从path
移动到newPath
相当于前面少了一个节点,所以需要在path.length - 1
地方-1
。 -
移动的时候相当于
path
整体移动到newPath
。所以只需要在p
中属于path
的部分替换成newPath
即可 -
if (Path.isAncestor(path, p) || Path.equals(path, p)) { const copyPath = newPath.slice(); // path.length < newPath.length 表示跨层级,跨层级删除了一个节点,需要改变位置需要 -1 if (Path.endsBefore(path, newPath) && path.length < newPath.length) { copyPath[path.length - 1] -= 1; } // newPath + 原来其子节点的位置 return copyPath.concat(p.slice(path.length)); }
-
-
newPath
完全相同的路徑 & 以newPath
为祖先的路径 (对应左图/右图的绿色节点) 。-
如果 path 是 newPath 前面的节点,移动到 newPath 位置,那么相当于前面少了一个节点,绿色节点前进了。所以需要在
path.length - 1
地方-1
。 (从左到右/从右到左观察绿色节点) -
path
节点移动到newPath
位置,那么相当于前面多了一个节点,绿色节点后退了。所以需要在newPath.length - 1
地方+1
。 -
else if (Path.isAncestor(newPath, p) || Path.equals(newPath, p)) { if (Path.isSibling(newPath, path)) { if (Path.endsBefore(path, p)) { p[path.length - 1] -= 1 } else { p[path.length - 1] += 1 } } else { if (Path.endsBefore(path, p)) { p[path.length - 1] -= 1 } p[newPath.length - 1] += 1 } }
-
-
位于
newPath
右边的节点 (对应左图/右图橘色节点) 。- 如果
path
是newPath
前面的节点,移动到newPath
位置,那么相当于前面少了一个节点,绿色节点前进了。所以需要在path.length - 1
地方-1
。 path
节点移动到newPath
位置,那么相当于前面多了一个节点,绿色节点后退了。所以需要在newPath.length - 1
地方+1
。-
else if (Path.endsBefore(newPath, p)) { if (Path.endsBefore(path, p)) { p[path.length - 1] -= 1 } p[newPath.length - 1] += 1 }
- 如果
-
位于
path
右边的节点 (对应左图/右图紫色节点) 。相当于前面的节点移动到别的地方,紫色节点前进,所以需要在path.length - 1
地方-1
-
else if (Path.endsBefore(path, p)) { p[path.length - 1] -= 1; }
-
-
point.Transfrom 纠正 point
pointTransform 的参数主要有三个:
point
:需要被纠正的 pointop
:当前执行的 operationoptions.affinity
:affinity 表示的是方向,
/** * Transform a point by an operation. */
transform(
point: Point,
op: Operation,
options: { affinity?: 'forward' | 'backward' | null } = {}
): Point | null {
return produce(point, p => {
const { affinity = 'forward' } = options
const { path, offset } = p
// ... Point.transform implementation
});
}
-
insert_text
假设在红色区域插入0
文本,只有 path
与 op.path
为同一路径才需要纠正:
- 如果
op.offset
小于offset
(如下图蓝色point
),表示插入的文字位于需要纠正的point
之前,因为在其前面插入了文字的关系,因此p.offset
需要增加op.text
的长度。 - 如果
op.offset
等于offset
(如下图红色point
),则表示插入的文字位于需要纠正的point
的位置。根据affinity
做处理,如果是forward
才会向前移动op.text
长度,否则不处理。
case 'insert_text': {
if (Path.equals(op.path, path)) {
if (
op.offset < offset ||
(op.offset === offset && affinity === 'forward')
) {
p.offset += op.text.length
}
}
}
-
remove_text
假设在红色区域删除34
文本,此时的 op 为 { path: xxx, type: 'remove_text' , offset: 2, text: '34' }
。只有 path
与 op.path
为同一路径才需要纠正:
-
如果
op.offset
小于等于offset
(如下图蓝色和黑色point
),表示删除的文字位于需要纠正的point
之前,因为在其前面删除了文字的关系,- 对于蓝色
point
来说p.offset
需要扣除text
的长度。 - 红色
point
来说,就需要扣除offset - op.offset
。其实就是回到op.offset
的位置。
- 对于蓝色
case 'remove_text': {
if (Path.equals(op.path, path) && op.offset <= offset) {
p.offset -= Math.min(offset - op.offset, op.text.length)
}
break
}
-
split_node
以{ type: 'split_node', position: 1, path: [0,0,0] }
为例子:因为按照 1 分割文本节点,
- 如果此时
offset > 1
,光标是要跟随到下一个节点的,下一个节点的是从0
开始,所以需要p.offset -= op.position
; - 如果位置在
offset = position
,那就根据affinity
做处理,forward
就跟随到下一个节点。
case 'split_node': {
// 自身的 op 需要改变 offset
if (Path.equals(op.path, path)) {
if (
op.position < p.offset ||
(op.position === p.offset && affinity === 'forward')
) {
p.offset -= op.position;
p.path = Path.transform(path, op, { affinity: 'forward' })!;
}
} else {
p.path = Path.transform(path, op, options)!;
}
break;
}
-
insert_node
不会修改到 offset
的值,所以实际上只需要修改 path 即可。
case 'insert_node': {
// 指向 insertNode 的尾巴
p.path = Path.transform(path, op)!;
break;
}
-
remove_node
remove_node 本身也不会修改到 offset 的值,所以实际上只需要修改 path 即可。但是因为删除了 Element 其子节点也会跟着一起删除,所以如果 op.path
与 path
为相同路径或者 op.path
是 path
的祖先直接返回 null。原因是在最新的视图找不到了。
case 'remove_node': {
/**
* 如果 point 是本身或者在其父节点,
*/
if (Path.equals(op.path, path) || Path.isAncestor(op.path, path)) {
return null;
}
p.path = Path.transform(path, op, options)!;
break;
}
-
merge_node
case 'merge_node': {
/**
* A,B 两个节点,B mergeTo A,
* 此时 B 的 offset 应该变为 A.length+B.length,
* B 的 path 应该变为 A 的 path
*/
if (Path.equals(op.path, path)) {
p.offset += op.position;
}
p.path = Path.transform(path, op)!;
break;
}
-
move_node
不会修改到 offset
的值,所以实际上只需要修改 path 即可。
case 'move_node': {
p.path = Path.transform(path, op)!;
break;
}
-
range.Transfrom 纠正 range
Range 本质是由两个 point 组成的,所以对 range 的transfrom 其实就是对 point 的transfrom。这里主要是针对 affinity
参数的控制,最後將 range 的 anchor
与 focus
丟入 Point.transform
进行转换
/** * Transform a range by an operation. */
transform(
range: Range,
op: Operation,
options: {
affinity?: 'forward' | 'backward' | 'outward' | 'inward' | null
} = {}
): Range | null {
// 对 affinity 的处理
const anchor = Point.transform(r.anchor, op, { affinity: affinityAnchor })
const focus = Point.transform(r.focus, op, { affinity: affinityFocus })
if (!anchor || !focus) {
return null
}
r.anchor = anchor
r.focus = focus
})
},
-
forward
:split_node 的时候,节点位置指向下一个节点。 -
backward
:split_node 的时候,节点位置保持不变。 -
inward
(内向):- 如果 range 是从前到后,进行 split_node 的时候,开始节点指向下一个节点, 结束节点保持不变。
- 如果 range 是从后到前,进行 split_node 的时候,开始节点保持不变,结束节点指向下一个。
- 如果 range 是闭合的,那就指向下一个。
split_node 是按照光标尾部 focus split,随后按照光标头部 anchor split
-
outward
(外向):- 如果 range 是从后到前,进行 split_node 的时候,开始节点指向下一个节点, 结束节点保持不变。
- 如果 range 是从前到后,进行 split_node 的时候,开始节点保持不变,结束节点指向下一个。
split_node 是按照光标尾部 focus split,随后按照光标头部 anchor split
const { affinity = 'inward' } = options
let affinityAnchor: 'forward' | 'backward' | null
let affinityFocus: 'forward' | 'backward' | null
if (affinity === 'inward') {
const isCollapsed = Range.isCollapsed(r)
if (Range.isForward(r)) { // range 从前到后
affinityAnchor = 'forward' // 开始节点指向下一个节点
affinityFocus = isCollapsed ? affinityAnchor : 'backward' // 结束节点保持不变
} else {
affinityAnchor = 'backward' // 开始节点不变
affinityFocus = isCollapsed ? affinityAnchor : 'forward' // 结束节点指向下一个
}
}
else if (affinity === 'outward') {
if (Range.isForward(r)) {
affinityAnchor = 'backward' // 开始节点不变
affinityFocus = 'forward' // 结束节点指向下一个
} else {
affinityAnchor = 'forward' // 开始节点指向下一个节点
affinityFocus = 'backward' // 结束节点保持不变
}
}
else {
affinityAnchor = affinity
affinityFocus = affinity
}
-
写在最后 & 参考资料
欢迎 star:github.com/JokerLHF/mi…