插件机制

插件作为 Slate 一等公民,具有能够完全修改编辑器行为的能力。

这里我们通过官方提供的 slate-history 来介绍 slate 的插件原理。

插件原理

实现插件

  1. /**
  2. * withHistory 插件使用 撤消(redo) 和 重做(undo) 堆栈来跟踪对 Slate编辑器 执行操作时的操作历史记录。
  3. */
  4. export const withHistory = <T extends Editor>(editor: T) => {
  5. const e = editor as T & HistoryEditor;
  6. const {apply} = e;
  7. // ...
  8. e.redo = () => {
  9. // ...
  10. };
  11. e.undo = () => {
  12. // ...
  13. };
  14. e.apply = (op: Operation) => {
  15. // ...
  16. apply(op);
  17. };
  18. return e;
  19. };

使用插件

  1. import React, {useState, useCallback, useMemo} from 'react';
  2. import {Slate, Editable, withReact} from 'slate-react';
  3. import {Editor, Range, Point, Node, createEditor} from 'slate';
  4. import {withHistory} from 'slate-history';
  5. const TablesExample = () => {
  6. const [value, setValue] = useState<Node[]>(initialValue);
  7. const renderElement = useCallback(props => <Element {...props} />, []);
  8. const renderLeaf = useCallback(props => <Leaf {...props} />, []);
  9. const editor = useMemo(
  10. () => withTables(
  11. withHistory(
  12. withReact(createEditor())
  13. )
  14. ),
  15. []
  16. );
  17. return (
  18. <Slate
  19. editor={editor}
  20. value={value}
  21. onChange={value => setValue(value)}
  22. >
  23. <Editable
  24. renderElement={renderElement}
  25. renderLeaf={renderLeaf}
  26. />
  27. </Slate>
  28. );
  29. };

Slate 插件是一个返回 editor 实例的函数,在这个插件函数中我们可以调用编辑器自身的能力,这样就能完全控制编辑器行为,所以我们可以使用插件来定制我们想要的编辑器。

人间惨案

由于目前我们使用的版本还是slate@0.47.x, 它的插件原理是基于 middleware 的设计,绑定在editor上。

  1. function registerPlugin(editor, plugin) {
  2. if (Array.isArray(plugin)) {
  3. plugin.forEach(p => registerPlugin(editor, p));
  4. return;
  5. }
  6. if (plugin == null) {
  7. return;
  8. }
  9. const {commands, queries, schema, ...rest} = plugin;
  10. if (commands) {
  11. const commandsPlugin = CommandsPlugin(commands);
  12. registerPlugin(editor, commandsPlugin);
  13. }
  14. if (queries) {
  15. const queriesPlugin = QueriesPlugin(queries);
  16. registerPlugin(editor, queriesPlugin);
  17. }
  18. if (schema) {
  19. const schemaPlugin = SchemaPlugin(schema);
  20. registerPlugin(editor, schemaPlugin);
  21. }
  22. for (const key in rest) {
  23. const fn = rest[key];
  24. const middleware = (editor.middleware[key] = editor.middleware[key] || []);
  25. middleware.push(fn);
  26. }
  27. }
  28. class Editor {
  29. constructor(attrs = {}, options = {}) {
  30. const {controller = this, construct = true} = options;
  31. const {plugins = []} = attrs;
  32. this.middleware = {};
  33. // ...
  34. this.registerPlugins(plugins);
  35. // ...
  36. }
  37. registerPlugins(plugins) {
  38. const core = CorePlugin({plugins});
  39. registerPlugin(this, core);
  40. }
  41. run(key, ...args) {
  42. const {controller, middleware} = this;
  43. const fns = middleware[key] || [];
  44. let i = 0;
  45. function next(...overrides) {
  46. const fn = fns[i++];
  47. if (!fn) {
  48. return;
  49. }
  50. if (overrides.length) {
  51. args = overrides;
  52. }
  53. const ret = fn(...args, controller, next);
  54. return ret;
  55. }
  56. // debug warning
  57. return next();
  58. }
  59. }

实现

  1. function MyPlugin(options) {
  2. return {
  3. onKeyDown(t) {},
  4. onClick() {},
  5. renderBlock() {},
  6. renderInline() {},
  7. commands: {},
  8. queries: {},
  9. schema: {},
  10. };
  11. }

使用

  1. import {Value} from 'slate';
  2. import {Editor} from 'slate-react';
  3. import * as React from 'react';
  4. const initialValue = {
  5. object: 'value',
  6. document: {
  7. object: 'document',
  8. nodes: [
  9. {
  10. object: 'block',
  11. type: 'paragraph',
  12. nodes: [
  13. {
  14. object: 'text',
  15. text:
  16. 'Slate editors save all changes to an internal "history" automatically, so you don\'t need to implement undo/redo yourself. And the editor automatically binds to the browser\'s default undo/redo keyboard shortcuts.',
  17. },
  18. ],
  19. },
  20. {
  21. object: 'block',
  22. type: 'paragraph',
  23. nodes: [
  24. {
  25. object: 'text',
  26. text:
  27. 'Try it out for yourself! Make any changes you\'d like then press "cmd+z".',
  28. },
  29. ],
  30. },
  31. ],
  32. },
  33. };
  34. class History extends React.Component {
  35. state = {
  36. value: Value.fromJSON(initialValue),
  37. };
  38. ref = editor => {
  39. this.editor = editor;
  40. };
  41. render() {
  42. const {value} = this.state;
  43. const {data} = value;
  44. const plugins = [MyPlugin];
  45. return (
  46. <div>
  47. <Editor
  48. placeholder='Enter some text...'
  49. ref={this.ref}
  50. plugins={plugins}
  51. value={this.state.value}
  52. onChange={this.onChange}
  53. />
  54. </div>
  55. );
  56. }
  57. onChange = change => {
  58. this.setState({value: change.value});
  59. };
  60. }

如何注册自定义插件

lastest

  1. export const withHistory = <T extends Editor>(editor: T) => {
  2. const e = editor as T & HistoryEditor;
  3. const {apply} = e;
  4. e.apply = (op: Operation) => {
  5. // ...
  6. apply(op);
  7. };
  8. return e;
  9. };

0.47.x

  1. <Editor
  2. plugins={[
  3. {
  4. custom: (editor, next) => {
  5. console.log(1);
  6. next();
  7. }
  8. },
  9. {
  10. custom: (editor, next) => {
  11. console.log(2);
  12. }
  13. },
  14. {
  15. custom: (editor, next) => {
  16. console.log(3);
  17. }
  18. }
  19. ]}
  20. />;
  21. // 使用
  22. editor.run('custom'); // 输出 1 2