使用命令

到目前为止,我们所学习的内容都是在编写为特定的 Slate 编辑器定制的一次性业务逻辑。不过,Slate 最强大的地方之一在于它的插件系统,它能够让你少些写这样的一次性业务代码。

在之前的指南中我们已经编写了一些有用的代码用来处理代码块和加粗格式。我们使用 onKeyDown 事件处理函数来调用这些代码。但是我们总是直接使用 Editor 内置的帮助器(helpers)来完成它们,而不是使用命令 (commands)。

Slate允许您扩展内置 Editor 对象以处理您自己的自定义富文本命令。你甚至可以使用预先打包的插件(plugins) 来添加一组给定的功能。

让我们看看如何编写吧。

我们还是从之前的应用程序继续:

  1. const App = () => {
  2. const editor = useMemo(() => withReact(createEditor()), [])
  3. const [value, setValue] = useState([
  4. {
  5. type: 'paragraph',
  6. children: [{ text: 'A line of text in a paragraph.' }],
  7. },
  8. ])
  9. const renderElement = useCallback(props => {
  10. switch (props.element.type) {
  11. case 'code':
  12. return <CodeElement {...props} />
  13. default:
  14. return <DefaultElement {...props} />
  15. }
  16. }, [])
  17. const renderLeaf = useCallback(props => {
  18. return <Leaf {...props} />
  19. }, [])
  20. return (
  21. <Slate editor={editor} value={value} onChange={value => setValue(value)}>
  22. <Editable
  23. renderElement={renderElement}
  24. renderLeaf={renderLeaf}
  25. onKeyDown={event => {
  26. if (!event.ctrlKey) {
  27. return
  28. }
  29. switch (event.key) {
  30. case '`': {
  31. event.preventDefault()
  32. const [match] = Editor.nodes(editor, {
  33. match: n => n.type === 'code',
  34. })
  35. Transforms.setNodes(
  36. editor,
  37. { type: match ? null : 'code' },
  38. { match: n => Editor.isBlock(editor, n) }
  39. )
  40. break
  41. }
  42. case 'b': {
  43. event.preventDefault()
  44. Transforms.setNodes(
  45. editor,
  46. { bold: true },
  47. { match: n => Text.isText(n), split: true }
  48. )
  49. break
  50. }
  51. }
  52. }}
  53. />
  54. </Slate>
  55. )
  56. }

它已经有了代码块加粗格式的概念。但是这些都是在 onKeyDown 事件处理程序中一次性定义的。如果你想要在其他地方重用这些逻辑,你需要把它们提取出来。

我们可以通过创建自定义的帮助函数来实现这些特定于域的概念:

  1. // Define our own custom set of helpers.
  2. const CustomEditor = {
  3. isBoldMarkActive(editor) {
  4. const [match] = Editor.nodes(editor, {
  5. match: n => n.bold === true,
  6. universal: true,
  7. })
  8. return !!match
  9. },
  10. isCodeBlockActive(editor) {
  11. const [match] = Editor.nodes(editor, {
  12. match: n => n.type === 'code',
  13. })
  14. return !!match
  15. },
  16. toggleBoldMark(editor) {
  17. const isActive = CustomEditor.isBoldMarkActive(editor)
  18. Transforms.setNodes(
  19. editor,
  20. { bold: isActive ? null : true },
  21. { match: n => Text.isText(n), split: true }
  22. )
  23. },
  24. toggleCodeBlock(editor) {
  25. const isActive = CustomEditor.isCodeBlockActive(editor)
  26. Transforms.setNodes(
  27. editor,
  28. { type: isActive ? null : 'code' },
  29. { match: n => Editor.isBlock(editor, n) }
  30. )
  31. },
  32. }
  33. const App = () => {
  34. const editor = useMemo(() => withReact(createEditor()), [])
  35. const [value, setValue] = useState([
  36. {
  37. type: 'paragraph',
  38. children: [{ text: 'A line of text in a paragraph.' }],
  39. },
  40. ])
  41. const renderElement = useCallback(props => {
  42. switch (props.element.type) {
  43. case 'code':
  44. return <CodeElement {...props} />
  45. default:
  46. return <DefaultElement {...props} />
  47. }
  48. }, [])
  49. const renderLeaf = useCallback(props => {
  50. return <Leaf {...props} />
  51. }, [])
  52. return (
  53. <Slate editor={editor} value={value} onChange={value => setValue(value)}>
  54. <Editable
  55. renderElement={renderElement}
  56. renderLeaf={renderLeaf}
  57. onKeyDown={event => {
  58. if (!event.ctrlKey) {
  59. return
  60. }
  61. // 使用我们新编写的命令来替代 onKeyDown 中的逻辑
  62. switch (event.key) {
  63. case '`': {
  64. event.preventDefault()
  65. CustomEditor.toggleCodeBlock(editor)
  66. break
  67. }
  68. case 'b': {
  69. event.preventDefault()
  70. CustomEditor.toggleBoldMark(editor)
  71. break
  72. }
  73. }
  74. }}
  75. />
  76. </Slate>
  77. )
  78. }

现在我们已经明确定义了命令,所以在任何可以访问到 editor 对象的地方都可以调用它们。例如,从假设的工具栏按钮:

  1. const App = () => {
  2. const editor = useMemo(() => withReact(createEditor()), [])
  3. const [value, setValue] = useState([
  4. {
  5. type: 'paragraph',
  6. children: [{ text: 'A line of text in a paragraph.' }],
  7. },
  8. ])
  9. const renderElement = useCallback(props => {
  10. switch (props.element.type) {
  11. case 'code':
  12. return <CodeElement {...props} />
  13. default:
  14. return <DefaultElement {...props} />
  15. }
  16. }, [])
  17. const renderLeaf = useCallback(props => {
  18. return <Leaf {...props} />
  19. }, [])
  20. return (
  21. // 添加一个工具栏按钮来调用同一个方法
  22. <Slate editor={editor} value={value} onChange={value => setValue(value)}>
  23. <div>
  24. <button
  25. onMouseDown={event => {
  26. event.preventDefault()
  27. CustomEditor.toggleBoldMark(editor)
  28. }}
  29. >
  30. Bold
  31. </button>
  32. <button
  33. onMouseDown={event => {
  34. event.preventDefault()
  35. CustomEditor.toggleCodeBlock(editor)
  36. }}
  37. >
  38. Code Block
  39. </button>
  40. </div>
  41. <Editable
  42. editor={editor}
  43. renderElement={renderElement}
  44. renderLeaf={renderLeaf}
  45. onKeyDown={event => {
  46. if (!event.ctrlKey) {
  47. return
  48. }
  49. switch (event.key) {
  50. case '`': {
  51. event.preventDefault()
  52. CustomEditor.toggleCodeBlock(editor)
  53. break
  54. }
  55. case 'b': {
  56. event.preventDefault()
  57. CustomEditor.toggleBoldMark(editor)
  58. break
  59. }
  60. }
  61. }}
  62. />
  63. </Slate>
  64. )
  65. }

这就是提取逻辑的好处。

我们再一次完成了编码工作!我们仅仅做了非常少量的工作就为编辑器添加了大量的功能。并且我们可以把命令逻辑放在一个单独的地方进行测试和隔离,从而使代码更容易进行维护。