# 变换(Transforms)

slate的数据结构是不可变的(immutable)的,所以你不能直接修改或者删除节点. 但是, slate提供了一系列叫做"变换(transform)"的函数使你能够修改编辑器的值

为了能表示所有你可能需要对编辑器进行的修改, slate的transform 函数被设计得十分灵活. 然而, 这种灵活性可能使你在一开始的时候很难去理解.

通常情况下, 你会对若干节点执行同一个操作. 例如, 通过对每个block element的父节点执行 unwrapNodes 来扁平化语法树

Transforms.unwrapNodes(editor, {
  at: [], // Path of Editor
  match: node =>
    !Editor.isEditor(node) &&
    node.children?.every(child => Editor.isBlock(editor, child)),
  mode: 'all', // also the Editor's children
})
1
2
3
4
5
6
7

对于非标准操作(以及需要debug或trace哪些节点会被一组NodeOptions影响)可能需要使用 Editor.nodes 来创建一个NodeEntries的javascript 迭代器和一个用于执行迭代器的for..of循环, 比如说, 将所有图像节点替换为他们对应的alt text

const imageElmnts = Editor.nodes(editor, {
  at: [], // Path of Editor
  match: (node, path) => 'image' === node.type,
  // mode defaults to "all", so this also searches the Editor's children
})
for (const nodeEntry of imageElmnts) {
  const altText =
    nodeEntry[0].alt ||
    nodeEntry[0].title ||
    /\/([^/]+)$/.exec(nodeEntry[0].url)?.[1] ||
    '☹︎'
  Transforms.select(editor, nodeEntry[1])
  Editor.insertFragment(editor, [{ text: altText }])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

🤖 查看Transforms获取slate的transforms的完整参考资料

# 选区变换(Selection Transforms)

关于选区(selection)的变换(transforms)方法则更加简单. 比如说, 将选区设置为一个新的范围(range)

Transforms.select(editor, {
  anchor: { path: [0, 0], offset: 0 },
  focus: { path: [1, 0], offset: 2 },
})
1
2
3
4

但同时他们也可以变得更为复杂

比如说, 将光标往前或者往后移动几个字母, 几个单词或者几行是一个常见的需求. 下面展示了如何将光标完后移动三个单词

Transforms.move(editor, {
  distance: 3,
  unit: 'word',
  reverse: true,
})
1
2
3
4
5

🤖 查看Selection Transforms API Reference获取更多信息

# 文本变换(Text Transforms)

Text transforms对编辑器的文本内容进行操作, 比如向特定的point插入一段文本

Transforms.insertText(editor, 'some words', {
  at: { path: [0, 0], offset: 3 },
})
1
2
3

你也可以删除整个范围(range)中的所有内容

Transforms.delete(editor, {
  at: {
    anchor: { path: [0, 0], offset: 0 },
    focus: { path: [1, 0], offset: 2 },
  },
})
1
2
3
4
5
6

🤖 查看Text Transforms API Reference获取更多信息

# 节点变换(Node Transforms)

节点变换(Node transforms)对单个element node或者text node进行操作. 比如在特定的路径(path)插入一个text node

Transforms.insertNodes(
  editor,
  {
    text: 'A new string of text.',
  },
  {
    at: [0, 1],
  }
)
1
2
3
4
5
6
7
8
9

又比如把一些node从一个path移动到另一个path

Transforms.moveNodes(editor, {
  at: [0, 0],
  to: [0, 1],
})
1
2
3
4

🤖 查看Node Transforms API Reference获取更多信息

# at 选项(The at Option)

很多transforms操作会在文档的特定位置进行. 默认情况下, 他们会对用户的当前选区(selection)进行操作, 但是, 这个默认值是可以使用 at 选项进行覆盖的

比如说, 下面的方法会在用户当前光标所在位置插入一段文本

Transforms.insertText(editor, 'some words')
1

然而, 修改成下面这样就可以在特定的位置插入了

Transforms.insertText(editor, 'some words', {
  at: { path: [0, 0], offset: 3 },
})
1
2
3

at 选项的用途非常广泛, 可以用它很方便的实现一些复杂的transforms, 由于 at 选项传入的值是一个 Location(位置), 包括 Path, Point 或者 Range, 且每种类型的location会导致稍有不同的transformations 比如说, 在下面这个插入文本的例子中, 如果你设定一个 Range 类型的位置, 这个range最开始会被删除, collapse为一个point, 你的文本会插入在这个point所在的位置

所以, 如果想要将 range所包含的文本替换成新的文本你可以这样做

Transforms.insertText(editor, 'some words', {
  at: {
    anchor: { path: [0, 0], offset: 0 },
    focus: { path: [0, 0], offset: 3 },
  },
})
1
2
3
4
5
6

如果你设置一个Path类型的location, 方法所作用的范围会扩展到该path对应的整个node, 然后, 使用基于range的方法会删除node的所有内容, 并替代为你想要插入的文本

所以如果想要将node的所有内容替换为一个新的字符串你可以

Transforms.insertText(editor, 'some words', {
  at: [0, 0],
})
1
2
3

凭借 at 选项, 这些基于location的操作对所有的transforms都能产生作用. 这可能最开始让你难以理解, 但正是它使得这个API强大且能够表达许许多多有细微差别的transforms

# match 选项(The match Option)

很多基于node的transforms会使用 match 选项, 这给选项使得transforms只会作用于match选项处的函数返回值为 true 的节点. 当我们将 atmatch 选项结合起来使用的时候竟会非常强大

比如说, 下面是一个基础的transform方法, 用于将一个node从一个path移动到另一个path

Transforms.moveNodes(editor, {
  at: [2],
  to: [5],
})
1
2
3
4

尽管它看上去只是简单的从一个path移动到另一个path, 背后其实发生了两件事...

第一步, at 选项会拓展为一个包含 [2] 位置所在的整个node的range

at: {
  anchor: { path: [2, 0], offset: 0 },
  focus: { path: [2, 2], offset: 19 }
}
1
2
3
4

第二步, match 选项默认情况下是一个只会匹配特定path的函数(matcher), 在这个例子中匹配的结果就是 [2] ;

match: (node, path) => Path.equals(path, [2])
1

然后slate会在 at 拓展后的range上迭代并把任意通过了match函数(matcher)的节点移动到新的位置. 在这个例子中, 由于match默认情况下只会精确的匹配 path 为 [2] 的, 因此将移动该节点

但如果你想要移动 [2] 对应的子节点呢?

你可能会考虑遍历子节点并且把一次移动一个, 但这会让问题变得非常复杂因为当你移动节点的时候你引用的path会过时

相反, 你可以充分利用 atmatch 选项的优势来匹配所有的children

Transforms.moveNodes(editor, {
  // This will again be expanded to a range of the entire node at `[2]`.
  at: [2],
  // Matches nodes with a longer path, which are the children.
  match: (node, path) => path.length === 2,
  to: [5],
})
1
2
3
4
5
6
7

在这里, 我们使用了相同的 at path(已经扩展为range了), 但是除了让他匹配默认的path, 我们提供了我们自己的match函数使得transform只对子节点起作用

使用 match 可以让复杂的逻辑变得简单

比如说, 想要给不是italic的text添加bold

Transform.setNodes(
  editor,
  { bold: true },
  {
    // This path references the editor, and is expanded to a range that
    // will encompass all the content of the editor.
    at: [],
    // This only matches text nodes that are not already italic.
    match: (node, path) => Text.isText(node) && node.italic !== true,
  }
)
1
2
3
4
5
6
7
8
9
10
11

当我们执行transforms的时候, 如果你曾经遍历节点并一个个对他们进行transform操作, 考虑试试match, 看看能不能解决你的情况, 并把操作循环时的复杂对转嫁给Slate. match 函数会用node.children检查node的children, 或者使用Node.parent来检查他们的parent

# Transforms and Normalization

如果节点树(node tree)应该在transform操作之间被normalized(标准化), 一系列的transforms操作需要被包裹在 Editor.withoutNormalizing (opens new window) 中. 参见 Normalization - Implications for Other Code (opens new window)