• React富文本编辑器开发(十一)命令与编辑器


    命令

    在编辑富文本内容时,您的用户将执行诸如插入文本、删除文本、拆分段落、添加格式等操作。在底层,这些编辑操作使用转换和操作来表达。但在高级别上,我们将它们称为 “命令”。

    命令是表示用户特定意图的高级别操作。它们表示为编辑器接口上的辅助函数。核心中包含了一些常见富文本行为的帮助程序,但鼓励您编写自己的命令来模拟您特定领域的行为。

    例如,以下是一些内置命令:

    Editor.insertText(editor, '要插入的新文本字符串。')
    
    Editor.deleteBackward(editor, { unit: 'word' })
    
    Editor.insertBreak(editor)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但您可以(而且应该!)定义自己的自定义命令,以模拟您的领域。例如,您可能想要定义一个 formatQuote 命令,或者一个 insertImage 命令,或者一个 toggleBold 命令,具体取决于您允许的内容类型。

    命令总是描述要执行的操作,就好像用户正在执行操作一样。因此,它们永远不需要定义执行命令的位置,因为它们始终作用于用户当前的选择。

    命令的概念基于DOM的内置 execCommand API。但是,Slate定义了自己更简单(且可扩展!)的API版本,因为DOM的版本过于主观和不一致。

    在底层,Slate负责将每个命令转换为一组低级别的 “操作”,这些操作被应用以产生新值。这就是协同编辑实现成为可能的原因。但是您不必担心这一点,因为它会自动发生。

    自定义命令

    在定义自定义命令时,您可以创建自己的命名空间:

    
    const MyEditor = {
      ...Editor,
    
      insertParagraph(editor) {
        // ...
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在编写自己的命令时,您经常会使用Slate提供的Transforms帮助程序。

    转换

    转换是一组特定的帮助程序,允许您对文档执行各种特定的更改,例如:

    // 在范围内的所有文本节点上设置 "bold" 格式。
    // 通常,您会使用Editor.addMark()命令应用类似粗体的样式。
    // addMark()命令执行类似的setNodes转换,但它使用了更复杂的匹配函数,以便在markableVoid元素中应用标记。
    Transforms.setNodes(
      editor,
      { bold: true },
      {
        at: range,
        match: node => Text.isText(node),
        split: true,
      }
    )
    
    // 在文档中某一点的最低块外面包装引用块。
    Transforms.wrapNodes(
      editor,
      { type: 'quote', children: [] },
      {
        at: point,
        match: node => Editor.isBlock(editor, node),
        mode: 'lowest',
      }
    )
    
    // 插入新文本以替换特定路径处节点中的文本。
    Transforms.insertText(editor, '要插入的新文本字符串。', { at: path })
    
    // ... 还有许多其他转换!
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    转换帮助程序设计成可以一起组合使用。因此,您可能会为每个命令使用一些帮助程序。

    编辑器 Editor

    Slate编辑器的所有行为、内容和状态都被整合到一个单独的顶级 Editor 对象中。它的接口如下:

    interface Editor {
      // 当前编辑器状态
      children: Node[]
      selection: Range | null
      operations: Operation[]
      marks: Omit<Text, 'text'> | null
      // 模式特定的节点行为。
      isInline: (element: Element) => boolean
      isVoid: (element: Element) => boolean
      markableVoid: (element: Element) => boolean
      normalizeNode: (entry: NodeEntry) => void
      onChange: (options?: { operation?: Operation }) => void
      // 可重写的核心操作。
      addMark: (key: string, value: any) => void
      apply: (operation: Operation) => void
      deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void
      deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void
      deleteFragment: () => void
      insertBreak: () => void
      insertSoftBreak: () => void
      insertFragment: (fragment: Node[]) => void
      insertNode: (node: Node) => void
      insertText: (text: string) => void
      removeMark: (key: string) => void
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    它比其他对象稍微复杂一些,因为它包含了定义您自定义的、特定领域行为的所有顶级函数。

    • children 属性包含组成编辑器内容的节点文档树。

    • selection 属性包含用户当前的选择,如果有的话。不要直接设置它;使用 Transforms.select

    • operations 属性包含自上次 "change" 被刷新以来应用的所有操作。(因为 Slate 将操作批处理成事件循环的时刻。)

    • marks 属性存储在编辑器插入文本时要应用的格式。如果 marksnull,则格式将从当前选择中获取。不要直接设置它;使用 Editor.addMark 和 Editor.removeMark。

    覆盖行为

    在之前的指南中我们已经提到过,但是您可以通过覆盖其函数属性来覆盖编辑器的任何行为。

    例如,如果您想要定义内联节点的链接元素:

    const { isInline } = editor
    
    editor.isInline = element => {
      return element.type === 'link' ? true : isInline(element)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    或者也许您想要覆盖 insertText 行为以将 URL “链接化”:

    const { insertText } = editor
    
    editor.insertText = text => {
      if (isUrl(text)) {
        // ...
        return
      }
    
      insertText(text)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果您有可以接受粗体或斜体等标记的空 “mention” 元素:

    const { isVoid, markableVoid } = editor
    
    editor.isVoid = element => {
      return element.type === 'mention' ? true : isVoid(element)
    }
    
    editor.markableVoid = element => {
      return element.type === 'mention' || markableVoid(element)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    或者您甚至可以定义自定义的 “normalizations” 来确保链接遵循某些约束:

    const { normalizeNode } = editor
    
    editor.normalizeNode = entry => {
      const [node, path] = entry
    
      if (Element.isElement(node) && node.type === 'link') {
        // ...
        return
      }
    
      normalizeNode(entry)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    每当您覆盖行为时,请确保调用现有的函数作为默认行为的后备机制。除非您确实想要完全删除默认行为(这很少是一个好主意)。

    辅助函数

    Editor 接口像所有 Slate 接口一样,公开了在实现某些行为时有用的辅助函数。有许多,许多与编辑器相关的辅助函数。例如:

    // 获取指定路径节点的起始点。
    const point = Editor.start(editor, [0, 0])
    
    // 在范围内获取片段(文档的一部分)。
    const fragment = Editor.fragment(editor, range)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    还有许多基于迭代器的辅助函数,例如:

    // 遍历范围内的每个节点。
    for (const [node, path] of Editor.nodes(editor, { at: range })) {
      // ...
    }
    
    // 遍历当前选择中每个文本节点中的每个点。
    for (const point of Editor.positions(editor)) {
      // ...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    Pycharm在进行debug时出现collecting data如何解决?
    业务脚本pytest封装
    JAVA计算机毕业设计在线课程教学大纲系统Mybatis+系统+数据库+调试部署
    【无人机】基于自适应无人机的湍流下发动机故障不确定性自动着陆问题(Matlab代码实现)
    系统架构设计:11 论湖仓一体架构及其应用
    Spring Cloud Zuul 基本原理
    树状数组——逆序对(范围较小)
    Java面试八股之Java中==和equals()的区别
    尚硅谷axios笔记——入门学习
    视觉slam论文、代码汇总
  • 原文地址:https://blog.csdn.net/zxmatline/article/details/136519009