Slate 的插件系统是一种强大的扩展机制,允许你以模块化的方式向编辑器添加功能、样式和行为。它提供了许多钩子函数和工具,使你可以轻松地自定义编辑器的行为,以满足特定的需求。
插件系统的核心思想是使用装饰器模式,通过对 Editor 对象应用装饰器来扩展其功能。Editor 是编辑器的核心对象,它提供了一系列操作和管理编辑器内容的方法。然后我们通过重新编辑 Editor 提供的工具方法,来达到自定义编辑器功能的效果。
Editor对象
我们可以通过提供的 createEditor 来获取到 editor 对象:
const editor = createEditor()
然后通过编写装饰器函数来创建插件,比如官方提供的 withReact 和 withHistory 。
在官方例子中:
// site/examples/richtext.tsx
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
每一个with函数,通过接受一个初始 editor 对象,在函数中编辑重写 editor 对象提供的各种工具方法,最后再返回一个修改过后的 editor 对象。
比如编辑editor提供的 insertText 方法,达到我们输入字符为 'a' 的时候,控制台就打印一个 'hello world' :
const myPlugin = (editor: Editor) => {
const { insertText } = editor
editor.insertText = (text: string) => {
if (text === 'a') {
console.log('hello world')
} insertText(text)
}
return editor
}
const editor = useMemo(() => myPlugin(withHistory(withReact(createEditor()))), [])
其中我们在insertText中添加了自己需要的操作,最后再调用insertText函数来运行原先的功能。
下面总结了一些 etitor 对象提供的常用的功能模块:
// 用于向选中的文本范围中添加一个标记
addMark: (...args) => addMark(editor, ...args)
// 用于删除操作
deleteBackward: (...args) => deleteBackward(editor, ...args)
deleteForward: (...args) => deleteForward(editor, ...args)
deleteFragment: (...args) => deleteFragment(editor, ...args)
// 用于插入操作
insertBreak: (...args) => insertBreak(editor, ...args)
insertSoftBreak: (...args) => insertSoftBreak(editor, ...args)
insertFragment: (...args) => insertFragment(editor, ...args)
insertNode: (...args) => insertNode(editor, ...args)
insertText: (...args) => insertText(editor, ...args)
// 用于判断编辑器的状态或者选区位置
isBlock: (...args) => isBlock(editor, ...args)
isEdge: (...args) => isEdge(editor, ...args)
isEmpty: (...args) => isEmpty(editor, ...args)
isEnd: (...args) => isEnd(editor, ...args)
isNormalizing: (...args) => isNormalizing(editor, ...args)
isStart: (...args) => isStart(editor, ...args)
// 这是Slate编辑器的核心操作方法,传入对应的 operations 来实现对内容的修改、插入、删除等操作
apply: (...args) => apply(editor, ...args),
// ......
Slate-history解析
在第一章《slate源码解析(一):总体概览》里介绍了结构目录,源码使用了 monorepo 的管理方式,各个包存于 /packages 文件下。其中 Slate-history 就是以插件的形式提供给Slate 历史记录功能的模块,实现了编辑时候撤销和还原的操作。下面我们来对这个官方提供的插件做一个源码解析。
Slate-history 提供了一个 withHistory 的装饰器函数:
export const withHistory = <T extends Editor>(editor: T) => {
const e = editor as T & HistoryEditor
const { apply } = e
e.history = { undos: [], redos: [] }
e.redo = () => { //... }
e.undo = () => { //... }
e.apply = (op: Operation) => {
// ...
apply(op)
}
e.writeHistory = (stack: 'undos' | 'redos', batch: any) => {
e.history[stack].push(batch)
}
return e
}
从传入参数里获取到上一个函数返回的 editor 对象,然后给 editor 新增了一个 history 对象,其中包括 undos 的操作栈和 redos 的操作栈。新增两个方法,redo 和 undo ,用来实现撤销和还原历史操作栈。修改了 apply 方法,使得每次slate操作时,都会由 history 去记录。
在apply的写入中,有一个合并操作:
const shouldMerge = (op: Operation, prev: Operation | undefined): boolean => {
if (
prev &&
op.type === 'insert_text' &&
prev.type === 'insert_text' &&
op.offset === prev.offset + prev.text.length &&
Path.equals(op.path, prev.path)
) {
return true
}
if (
prev &&
op.type === 'remove_text' &&
prev.type === 'remove_text' &&
op.offset + op.text.length === prev.offset &&
Path.equals(op.path, prev.path)
) {
return true
}
return false
}
就是每次我们键入文本或者删除的时候,会通过这个合并策略,将该次操作op保存到同一个操作批次中,这样每次撤销和还原,就是这一整个批次(这点其实有些反操作习惯,比如我们连续打一大段文字的时候,每次撤销会把这一大段文字一并撤销掉。其实可以加一个类似防抖的操作,根据时间节点来优化这个合并策略。)
下面是撤销操作时的实现:
const batch = undos[undos.length - 1]
const inverseOps = batch.operations.map(Operation.inverse).reverse()
for (const op of inverseOps) {
e.apply(op)
}
if (batch.selectionBefore) {
Transforms.setSelection(e, batch.selectionBefore)
}
获取到上一次的编辑历史,然后从 Operation.inverse 中获取到该次编辑的反向操作:
// /slate/packages/slate/src/interfaces/operation.ts
inverse(op: Operation): Operation {
switch (op.type) {
case 'insert_node': {
return { ...op, type: 'remove_node' }
}
case 'insert_text': {
return { ...op, type: 'remove_text' }
}
case 'merge_node': {
return { ...op, type: 'split_node', path: Path.previous(op.path) }
}
case 'move_node': {
const { newPath, path } = op
if (Path.equals(newPath, path)) {
return op
}
if (Path.isSibling(path, newPath)) {
return { ...op, path: newPath, newPath: path }
}
const inversePath = Path.transform(path, op)!
const inverseNewPath = Path.transform(Path.next(path), op)!
return { ...op, path: inversePath, newPath: inverseNewPath }
}
case 'remove_node': {
return { ...op, type: 'insert_node' }
}
case 'remove_text': {
return { ...op, type: 'insert_text' }
}
case 'set_node': {
const { properties, newProperties } = op
return { ...op, properties: newProperties, newProperties: properties }
}
case 'set_selection': {
const { properties, newProperties } = op
if (properties == null) {
return {
...op,
properties: newProperties as Range,
newProperties: null,
}
} else if (newProperties == null) {
return {
...op,
properties: null,
newProperties: properties as Range,
}
} else {
return { ...op, properties: newProperties, newProperties: properties }
}
}
case 'split_node': {
return { ...op, type: 'merge_node', path: Path.next(op.path) }
}
}
},
如操作为 'insert_text' 则获取到的撤销操作是 'remove_text'。
并循环 reverse 数组,将获取到的 op 一个一个通过 apply 去执行。
在withHistory函数里,还新增了一个 writeHistory 方法。用于直接将操作批次(batch)写入历史记录。在每次 redo 和 undo 操作时都会被调用。比如每次我运行一个撤销操作,那这次的历史操作就会被写入到还原栈中,反之每次运行还原操作时,也会被写入到撤销栈。