探索 Tiptap V3 的最新功能

编辑器命令

编辑器提供了大量命令,用于以编程方式添加或更改内容或改变选择。如果你想构建自己的编辑器,您绝对想要了解更多关于它们的信息。

执行命令

所有可用的命令通过编辑器实例访问。假设你想让用户点击按钮时文本变为粗体。实现方法如下:

editor.commands.setBold()

虽然这样完全可以将选定文本设置为粗体,但你可能希望在一次运行中链接多个命令。让我们看看这是如何工作的。

链接命令

大多数命令可以组合为一个调用。在大多数情况下,这比分别调用函数要短。以下是一个将选定文本变为粗体的示例:

editor.chain().focus().toggleBold().run()

.chain() 是启动新链所必需的,而 .run() 是实际执行所有中间命令所必需的。

在上面的示例中,同时执行了两个不同的命令。当用户点击内容外部的按钮时,编辑器不再处于焦点状态。这就是为什么你可能希望在大多数命令中添加 .focus() 调用。它将焦点返回给编辑器,以便用户能够继续输入。

所有链式命令都是排队的。它们组合成一个单一的事务。这意味着内容仅更新一次,update 事件也仅触发一次。

事务映射

默认情况下,Prosemirror 不支持链式调用,这意味着你需要通过 事务映射 来更新链式命令之间的位置。

例如,你想在一个链中链接一个 删除插入 命令,你需要在链命令中跟踪位置。以下是一个示例:

// 在此我们为编辑器添加两个自定义命令以演示两个事务步骤之间的事务映射
addCommands() {
  return {
    delete: () => ({ tr }) => {
      const { $from, $to } = tr.selection

      // 这里我们使用 tr.mapping.map 来映射事务步骤之间的位置
      const from = tr.mapping.map($from.pos)
      const to = tr.mapping.map($to.pos)

      tr.delete(from, to)

      return true
    },
    insert: (content: string) => ({ tr }) => {
      const { $from } = tr.selection

      // 这里我们使用 tr.mapping.map 来映射事务步骤之间的位置
      const pos = tr.mapping.map($from.pos)

      tr.insertText(content, pos)

      return true
    },
  }
}

现在你可以执行以下操作,而不会导致 insert 将内容插入错误的位置:

editor.chain().delete().insert('foo').run()

在自定义命令中链式调用

在链式调用命令时,事务将被暂时保留。如果你想在自定义命令中链接命令,你需要使用上述事务并进行添加。以下是如何做到这一点:

addCommands() {
  return {
    customCommand: attributes => ({ chain }) => {
      // 不起作用:
      // return editor.chain() …

      // 起作用:
      return chain()
        .insertContent('foo!')
        .insertContent('bar!')
        .run()
    },
  }
}

行内命令

在某些情况下,在命令中加入更多逻辑是有帮助的。这就是为什么你可以在命令中执行命令。我知道,这听起来很疯狂,但我们来看看一个示例:

editor
  .chain()
  .focus()
  .command(({ tr }) => {
    // 操作事务
    tr.insertText('嘿,这太酷了!')

    return true
  })
  .run()

干跑命令

有时候,你并不想实际运行命令,而只是想知道是否可以运行命令,例如在菜单中显示或隐藏按钮。这就是我们添加 .can() 的原因。所有在此方法之后的内容将被执行,而不对文档应用更改:

editor.can().toggleBold()

你也可以将其与 .chain() 一起使用。以下是一个例子,检查是否可以应用所有命令:

editor.can().chain().toggleBold().toggleItalic().run()

如果可以应用命令,则两个调用都会返回 true,否则返回 false

为了使其在你的自定义命令中工作,别忘了返回 truefalse

对于一些你自己的命令,你可能想使用原始的 事务。为了使它们与 .can() 一起工作,你应该检查事务是否应该被派遣。以下是如何创建一个简单的 .insertText() 命令:

export default (value) =>
  ({ tr, dispatch }) => {
    if (dispatch) {
      tr.insertText(value)
    }

    return true
  }

如果你只是封装另一个 Tiptap 命令,你不需要检查,我们会为你做到。

addCommands() {
  return {
    bold: () => ({ commands }) => {
      return commands.toggleMark('bold')
    },
  }
}

如果你只是封装一个普通的 ProseMirror 命令,你仍然需要传递 dispatch。这时也不需要进行检查:

import { exitCode } from '@tiptap/pm/commands'

export default () =>
  ({ state, dispatch }) => {
    return exitCode(state, dispatch)
  }

尝试命令

如果你想运行一系列命令,但只想应用第一个成功的命令,你可以使用 .first() 方法。该方法依次运行一个命令,直到遇到第一个返回 true 的命令为止。

例如,退格键首先尝试撤消输入规则。如果成功,它就会停止。如果没有应用输入规则,因此无法撤消,它将运行下一个命令并删除所选内容(如果有的话)。以下是简化的示例:

editor.commands.first(({ commands }) => [
  () => commands.undoInputRule(),
  () => commands.deleteSelection(),
  // …
])

在命令内部,你可以执行相同的操作:

export default () =>
  ({ commands }) => {
    return commands.first([
      () => commands.undoInputRule(),
      () => commands.deleteSelection(),
      // …
    ])
  }

命令列表

请查看下面列出的所有核心命令。它们应该给你一个关于可能性的良好第一印象。

内容

命令描述
clearContent()清空整个文档。
insertContent()在当前位插入一个节点或 HTML 字符串。
insertContentAt()在特定位置插入一个节点或 HTML 字符串。
setContent()用新内容替换整个文档。

节点和标记

命令描述
clearNodes()将节点标准化为简简单单的段落。
createParagraphNear()创建一个邻近的段落。
deleteNode()删除一个节点。
extendMarkRange()扩展文本选择至当前标记。
exitCode()退出代码块。
joinBackward()将两个节点向后连接。
joinForward()将两个节点向前连接。
lift()移除现有包裹。
liftEmptyBlock()如果为空,则提升块。
newlineInCode()在代码中添加换行符。
resetAttributes()将某些节点或标记的属性重置为默认值。
setMark()添加具有新属性的标记。
setNode()用节点替换给定范围。
splitBlock()从现有节点分叉出一个新节点。
toggleMark()切换标记的开关。
toggleNode()用另一个节点切换节点。
toggleWrap()在另一个节点中包裹节点,或移除现有的包裹。
undoInputRule()撤消输入规则。
unsetAllMarks()删除当前选择中的所有标记。
unsetMark()删除当前选择中的标记。
updateAttributes()更新节点或标记的属性。

列表

命令描述
liftListItem()提升列表项到一个包装列表中。
sinkListItem()将列表项下沉到一个内部列表中。
splitListItem()将一个列表项拆分为两个列表项。
toggleList()在不同类型的列表之间切换。
wrapInList()将节点包裹在一个列表中。

选择

命令描述
blur()从编辑器中移除焦点。
deleteRange()删除给定范围。
deleteSelection()删除选定内容(如果有的话)。
enter()触发回车。
focus()在给定位置将焦点放在编辑器上。
keyboardShortcut()触发键盘快捷键。
scrollIntoView()将选定内容滚动到可视区域。
selectAll()选择整个文档。
selectNodeBackward()向后选择一个节点。
selectNodeForward()向前选择一个节点。
selectParentNode()选择父节点。
setNodeSelection()创建一个 NodeSelection。
setTextSelection()创建一个 TextSelection。

编写自己的命令

所有扩展都可以添加额外的命令(大多数都有),请查看提供的节点的具体 文档标记、和 功能 来了解更多信息。 当然,您还可以 添加自定义扩展 和自定义命令。 那么如何编写这些命令呢?关于这方面还有一些内容需要学习。