-
什么是 operation?
假设用户A和用户B同时对一篇文档进行编辑,文档内容是 12345。
-
A 调用
insertText
指令在4
后面插入了文本a
,生成了新的文档1234a5
-
B 调用
removeText
指令删除了5
,生成了新的文档1234
在多人协作的场景下,期望的结果是每个用户「彼时彼刻都看到同样的内容」。也就是说用户A需要知道用户B做的操作并将操作应用到本地修改Model,同理用户B需要知道用户A做的操作并将操作应用到本地修改Model。这样 A、B 才能看到一样结果的内容。(当然实际协同编辑并不是这么简单,协同过程中存在冲突。)
Operation 俗称 op,表示的就是你做了什么操作。Slate op 大体可以分为3种类型。分别是对 节点/选区/文本 的操作: NodeOperation | SelectionOperation | TextOperation
。
-
slate operation 的设计
op 的设计上还需要特别注意 undo/redo,本质上 undo/redo 就是将之前的 op 取反并重新应用到文档模型上。举个例子:insertText
的 undo
就是 removeText
:
-
在
offset
位置insertText
了一个a
文本。 -
与其对应的
undo
: 在offset
位置removeText
了一个a
文本。
在 slate 中大部分操作都有与其对应的相反操作。而 setNode/setSelection/moveNode
取反则是在 op 携带的属性上做文章。
insertText <=> removeText
insertNode <=> removeNode
splitNode <=> mergeNode
setNode <=> setNode
setSelection <=> setSelection
moveNode <=> moveNode
-
op 数据结构
-
insertText Operation
export type BaseInsertTextOperation = {
type: 'insert_text'
path: Path
offset: number
text: string
}
-
通过
type: insert_text
表示是一个插入文本操作的op
-
通过
path
找到对应的slateNode
节点 -
通过
offset
找到在哪一个位置插入text
文本
举个🌰:
下面 op 表示在[0,0,0]
节点的第1
个位置插入一个2
文本
{
type: 'insert_text'
path: [0,0,0]
offset: 1
text: 2
}
-
removeText Operation
export type BaseRemoveTextOperation = {
type: 'remove_text'
path: Path
offset: number
text: string
}
-
通过
type: insert_text
表示是一个删除文本操作的op
-
通过
path
找到对应的slateNode
节点 -
通过
offset
找到在哪一个位置删除text
文本
举个🌰:
下面 op 表示在[0,0,0]
节点的第1
个位置删除一个长度的文本,也就是2
这个文本。
{
type: 'remove_text'
path: [0,0,0]
offset: 1
text: 2
}
这里的 text
字段设计就是为了适应 undo
****。设想一下如果不考虑 undo
这里是不是设计成下面比较符合直觉:
export type BaseRemoveTextOperation = {
type: 'remove_text'
path: Path
offset: number
length: number
}
-
通过
type: insert_text
表示是一个插入操作的op
-
通过
path
找到对应的slateNode
节点 -
通过
offset
找到在哪一个位置删除length
个文本
那用 length
会有什么问题呢?假设有一个文档内容是 12345
,用户执行一个 op
删除了5
这个文本,现在内容就是 1234
。此时执行 undo
删除操作对应的 undo
是 insertText
:
length
的设计能够的到的信息就是:通过offset
找到在哪一个位置插入length
个文本。text
的设计能够的到的信息就是:通过offset
找到在哪一个位置插入text
文本。
很明显 length
字段不符合。
-
insertNode Operation
export type BaseInsertNodeOperation = {
type: 'insert_node'
path: Path
node: Node
}
-
通过
type: insert_node
表示是一个插入节点操作的op
-
通过
path
找到要插入的位置,并且插入node
节点
举个🌰:
下面 op 表示在[0,1]
位置下插入一个 node
节点。原先在 [0,1] 位置节点就会被挤到右边。
{
type: 'insert_node'
path: [0,1]
node: { text: 4 }
}
-
removeNode Operation
export type BaseRemoveNodeOperation = {
type: 'remove_node'
path: Path
node: Node
}
- 通过
type: remove_node
表示是一个删除节点操作的op
- 通过
path
找到对应的slateNode
节点,并将其删除。
这里的 node 字段设计就是为了适应 undo
。具体原因可以参考 removeText,这里不再赘述了。
举个🌰:
下面 op 表示在删除[0,1]
位置的slateNode
节点。
{
type: 'remove_node'
path: [0,1]
node: { text: 4 }
}
-
splitNode operation
export type BaseSplitNodeOperation = {
type: 'split_node'
path: Path
position: number
properties: Partial<Node>
}
-
通过
type: split_node
表示是一个拆分节点操作的op
-
通过
path
找到对应的slateNode
-
position
需要分情况:- 如果
slateNode
是一个文本节点:position
表示需要分割的文本节点的位置。从position
位置将slateTextNode
拆分为两个slateTextNode
。 - 如果
slateNode
不是文本节点:position
表示slateNode
的第几个子节点。从position
位置将slateNode
的子节点拆分为两部分。
- 如果
-
properties
是节点的一些额外信息,比如 加粗/斜体 等
举个🌰(文本)
下面 op 表示从 [0,0,0]
的position
位置将 slateTextNode
拆分为两个 slateTextNode
。原本 [0,0,0]
节点的properties
存在加粗属性,拆分出来新的节点也会带上 properties
属性。
{
type: 'split_node'
path: [0,0,0]
position: 1
properties: { bold: true }
}
举个🌰(非文本)
下面 op 表示从 [1]
的第1
子节点的位置拆分为2部分。并且将其移动到[1]
的右边。原本 [1]
节点的properties
存在{ test: true }
属性,拆分出来新的节点也会带上 properties
属性。
{
type: 'split_node'
path: [1]
position: 1
properties: { test: true }
}
-
mergeNode operation
export type BaseMergeNodeOperation = {
type: 'merge_node'
path: Path
position: number
properties: Partial<Node>
}
-
通过
type: merge_node
表示是一个合并节点操作的op
-
通过
path
找到对应的slateNode
,并将slateNode
合并到prevPath
的节点中。(prevPath 表示 path 的前一个节点)- 如果 slateNode 是一个文本节点,则直接将当前节点文本合并到 prevPathSlateNode
- 如果 slateNode 是非文本节点,则直接将当前节点的子节点合并到 prevPathSlateNode,
- 最终都会删除当前节点。
-
position
需要分情况。设计就是为了适应undo
。- 如果
prevPath
对应的节点是一个文本节点,拿position
则是prevNode
的文本长度。 - 如果
prevPath
对应的节点非文本节点,拿position
则是prevNode
的孩子节点个数。
- 如果
-
properties
就是path
节点的属性 字段设计就是为了适应undo
。
case 'merge_node': {
const { path } = op
const node = Node.get(editor, path)
const prevPath = Path.previous(path)
const prev = Node.get(editor, prevPath)
const parent = Node.parent(editor, path)
const index = path[path.length - 1]
if (Text.isText(node) && Text.isText(prev)) {
prev.text += node.text
} else if (!Text.isText(node) && !Text.isText(prev)) {
prev.children.push(...node.children)
} else {
throw new Error('xxx')
}
// 删除当前节点
parent.children.splice(index, 1)
break
}
举个🌰(文本)
下面 op 表示从 [0,0,1]
合并到 [0,0,0]
中。其中 position
表示的就是 [0,0,0]
的长度,properties
就是 [0,0,1]
的properties
。这两个字段都是为了 undo
。
{
type: 'merge_node'
path: [0,0,1]
position: 1
properties: { bold: true }
}
举个🌰(非文本)
下面 op 表示从 [2]
合并到[1]
中。其中 position
表示[1]
子节点的个数。properties
是[2]
的properties
。这两个字段设计是为了 undo
。
{
type: 'merge_node'
path: [2]
position: 1
properties: { test: true }
}
-
setSelection operation
export type BaseSetSelectionOperation =
| {
type: 'set_selection'
properties: null
newProperties: Range
}
| {
type: 'set_selection'
properties: Partial<Range>
newProperties: Partial<Range>
}
| {
type: 'set_selection'
properties: Range
newProperties: null
}
-
通过
type: set_selection
表示是一个修改选区操作的op
-
properties 表示需要修改的属性原来的值,newProperties 表示需要修改的属性新的值。
-
通过
path
找到对应的slateNode
,通过 newProperties 设置/修改 新的属性 -
setNode operation
export type BaseSetNodeOperation = {
type: 'set_node'
path: Path
properties: Partial<Node>
newProperties: Partial<Node>
}
-
通过
type: set_node
表示是一个修改节点属性操作的op
-
properties 表示需要修改的属性原来的值,newProperties 表示需要修改的属性新的值。
-
通过
path
找到对应的slateNode
,通过 newProperties 设置/修改 新的属性
举个🌰:
下面 op 表示修改[0,0,0]
位置的slateNode
节点的属性。bold 从 true 变为 false,只记录前后不同的属性。
{
type: 'set_node'
path: [0,0,0]
properties: { bold: true }
newProperties: { bold: false }
}
-
moveNode operation
export type BaseMoveNodeOperation = {
type: 'move_node'
path: Path
newPath: Path
}
-
通过
type: move_node
表示是一个移动节点属性操作的op
-
通过
path
找到对应的slateNode
,保存下来 -
删除 slateNode
-
slateNode 增加到
newPath
上
举个🌰:
下面 op 表示移动[1]
位置的slateNode
节点到[2,2]
位置上
{
type: 'move_node'
path: [1]
newPath: [2,2]
}
-
写在最后 & 参考资料
原文链接:slate operation 如何修改 model
欢迎 star:github.com/JokerLHF/mi…