探索 Tiptap V3 的最新功能

Tiptap 模式

与许多其他编辑器不同,Tiptap 基于一个 模式,定义了内容的结构。这使您能够定义文档中可能出现的节点类型、其属性以及如何嵌套它们。

这个模式是 非常 严格的。您不能使用未在您的模式中定义的任何 HTML 元素或属性。

让我给你一个例子:如果您将类似 This is <strong>important</strong> 的内容粘贴到 Tiptap 中,而没有处理 strong 标签的扩展,您将只会看到 This is important — 没有强标签。

如果您想知道何时发生这种情况,可以在启用 enableContentCheck 选项后监听 contentError 事件。

模式是什么样子的

当您只使用提供的扩展时,您不必过于关心模式。如果您正在构建自己的扩展,理解模式的工作方式可能会有所帮助。让我们看看一个典型 ProseMirror 编辑器的最简单模式:

// 底层 ProseMirror 模式
{
  nodes: {
    doc: {
      content: 'block+',
    },
    paragraph: {
      content: 'inline*',
      group: 'block',
      parseDOM: [{ tag: 'p' }],
      toDOM: () => ['p', 0],
    },
    text: {
      group: 'inline',
    },
  },
}

我们在这里注册了三个节点。docparagraphtextdoc 是根节点,允许一个或多个块节点作为子节点(content: 'block+')。因为 paragraph 在块节点组中(group: 'block'),所以我们的文档只能包含段落。我们的段落允许零个或多个内联节点作为子节点(content: 'inline*'),所以其中只能有 textparseDOM 定义了如何从粘贴的 HTML 中解析节点。toDOM 定义了它将如何在 DOM 中呈现。

在 Tiptap 中,每个节点、标记和扩展都生活在自己的文件中。这使我们可以将逻辑分开。在底层,整个模式将被合并在一起:

// Tiptap 模式 API
import { Node } from '@tiptap/core'

const Document = Node.create({
  name: 'doc',
  topNode: true,
  content: 'block+',
})

const Paragraph = Node.create({
  name: 'paragraph',
  group: 'block',
  content: 'inline*',
  parseHTML() {
    return [{ tag: 'p' }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['p', HTMLAttributes, 0]
  },
})

const Text = Node.create({
  name: 'text',
  group: 'inline',
})

节点和标记

区别

节点就像内容块,例如段落、标题、代码块、引用等等。

标记可以应用于节点的特定部分。这适用于 粗体斜体删除 文本。链接 也是标记。

节点模式

内容

内容属性精确地定义了节点可以包含什么样的内容。ProseMirror 对此非常严格。这意味着,不符合模式的内容将被抛弃。它需要一个字符串形式的名称或组。以下是一些示例:

Node.create({
  // 必须有一个或多个块
  content: 'block+',

  // 必须有零个或多个块
  content: 'block*',

  // 允许所有类型的“内联”内容(文本或硬换行)
  content: 'inline*',

  // 不能有除“文本”之外的其他内容
  content: 'text*',

  // 可以有一个或多个段落,或列表(如果使用列表)
  content: '(paragraph|list?)+',

  // 必须在顶部有一个确切的标题,并在下面有一个或多个块
  content: 'heading block+',
})

标记

您可以通过模式的 marks 设置定义允许在节点内部的标记。添加一个或多个标记名称或组,允许所有或不允许所有标记,如下所示:

Node.create({
  // 只允许“粗体”标记
  marks: 'bold',

  // 只允许“粗体”和“斜体”标记
  marks: 'bold italic',

  // 允许所有标记
  marks: '_',

  // 不允许所有标记
  marks: '',
})

将该节点添加到一组扩展中,可以在模式的 content 属性中引用。

Node.create({
  // 添加到“block”组
  group: 'block',

  // 添加到“inline”组
  group: 'inline',

  // 添加到“block”和“list”组
  group: 'block list',
})

内联

节点也可以被渲染为内联。当设置 inline: true 时,节点与文本内联渲染。这适用于提及。结果更像是一个标记,但具有节点的功能。一个区别是生成的 JSON 文档。多个标记同时应用,内联节点将导致嵌套结构。

Node.create({
  // 使节点与文本内联渲染,例如
  inline: true,
})

对于某些需要标记中不可用的功能的情况,例如节点视图,尝试看看内联节点是否可行:

Node.create({
  name: 'customInlineNode',
  group: 'inline',
  inline: true,
  content: 'text*',
})

内联节点可能很难选择,特别是在行边缘。快速修复:在使用 CSS 后,元素后面添加一个零宽空格:

.customInlineNode::after {
  content: "\200B";
}

原子

设置 atom: true 的节点不可直接编辑,应视为一个单元。在编辑器上下文中使用这种情况并不常见,但这将是其样子:

Node.create({
  atom: true,
})

一个例子是 Mention 扩展,它看起来像文本,但更像一个单一单元。由于它没有可编辑的文本内容,因此在复制此类节点时它是空的。不过,好消息是您可以控制这一点。以下是 Mention 扩展的示例:

// 用于将原子节点转换为纯文本
renderText({ node }) {
  return `@${node.attrs.id}`
},

可选择

除了已经可见的文本选择外,还有一个不可见的节点选择。如果您想使您的节点可选择,可以这样配置:

Node.create({
  selectable: true,
})

可拖动

所有节点都可以通过此设置配置为可拖动(默认情况下不是):

Node.create({
  draggable: true,
})

代码

用户期望代码的行为非常不同。对于所有包含代码的节点,您可以设置 code: true 以考虑这一点。

Node.create({
  code: true,
})

空格

控制此节点中的空格解析方式。

Node.create({
  whitespace: 'pre',
})

定义

当节点的完整内容被替换(例如,粘贴新内容)时,节点会默认被丢弃。如果节点在替换操作中应保留,请将其配置为 defining

通常,这适用于 BlockquoteCodeBlockHeadingListItem

Node.create({
  defining: true,
})

隔离

对于应在常规编辑操作(如退格)中封闭光标的节点,例如 TableCell,设置 isolating: true

Node.create({
  isolating: true,
})

允许间隙光标

Gapcursor 扩展注册了一个新模式属性,用于控制该节点的各个地方是否允许间隙光标。

Node.create({
  allowGapCursor: false,
})

表格角色

Table 扩展注册了一个新模式属性,以配置节点的角色。允许的值为 tablerowcellheader_cell

Node.create({
  tableRole: 'cell',
})

标记模式

包含

如果您不希望标记在光标处于末尾时处于活动状态,请设置 inclusive 为 false。例如,这就是 Link 标记的配置方式:

Mark.create({
  inclusive: false,
})

排除

默认情况下,所有标记可以同时应用。通过 excludes 属性,您可以定义哪些标记不得与标记共存。例如,内联代码标记排除了任何其他标记(粗体、斜体及所有其他)。

Mark.create({
  // 不得与粗体标记共存
  excludes: 'bold',
  // 排除任何其他标记
  excludes: '_',
})

可退出

默认情况下,标记会“困住”光标,这意味着光标无法从标记中退出,除非通过从左到右移动光标到没有标记的文本中。 如果将此设置为 true,则标记在节点末尾时将是可退出的。这对于使用代码标记非常方便。

Mark.create({
  // 使此标记可退出 - 默认值为 false
  exitable: true,
})

将此标记添加到一组扩展中,可以在模式的 content 属性中引用。

Mark.create({
  // 将此标记添加到“基本”组
  group: 'basic',
  // 将此标记添加到“基本”和“foobar”组
  group: 'basic foobar',
})

代码

用户期望代码的行为非常不同。对于所有包含代码的标记,您可以设置 code: true 以考虑这一点。

Mark.create({
  code: true,
})

跨越

默认情况下,标记在作为 HTML 渲染时可以跨越多个节点。设置 spanning: false 以指示标记不得跨越多个节点。

Mark.create({
  spanning: false,
})

获取底层 ProseMirror 模式

有几个用例需要处理底层模式。如果您使用 Tiptap 的协作文本编辑功能,或者希望手动将内容呈现为 HTML,则需要此。

选项 1:使用编辑器

如果您需要在客户端并且无论如何都需要编辑器实例,它可以通过编辑器访问:

import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'

const editor = new Editor({
  extensions: [
    Document,
    Paragraph,
    Text,
    // 在这里添加更多扩展
  ]
})

const schema = editor.schema

选项 2:不使用编辑器

如果您只想获得模式 而不 初始化实际编辑器,可以使用 getSchema 辅助函数。它需要一个可用扩展的数组,并且方便地为您生成 ProseMirror 模式:

import { getSchema } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'

const schema = getSchema([
  Document,
  Paragraph,
  Text,
  // 在这里添加更多扩展
])

无效模式处理

为了跟踪和响应内容错误,Tiptap 支持检查提供的内容是否与从注册的扩展派生的模式相匹配。 要使用此功能,请将 enableContentCheck 选项设置为 true,这会激活内容检查并发出 contentError 事件。 这些事件可以通过 onContentError 回调来监听。 默认情况下,此标志设置为 false,以保持与以前版本的兼容性。

注意

Tiptap 进行的内容检查在 JSON 内容类型上是 100% 准确的。但是,如果您将内容提供为 HTML,我们尽力警告缺少的节点,但在某些情况下可能会遗漏标记,因此,默认情况下会恢复到删除未识别内容的默认行为。

contentError 事件

当在编辑器设置期间提供的初始 content 与模式不兼容时,会发出 contentError 事件。

作为错误上下文的一部分,您将获得一个 disableCollaboration 函数。调用此函数会重新初始化编辑器,而不使用协作扩展,确保任何已删除的内容不会与其他用户同步。

此事件可以通过 onContentError 直接处理,如下所示:

new Editor({
  enableContentCheck: true,
  content: invalidContent,
  onContentError({ editor, error, disableCollaboration }) {
    // 在这里处理
  },
  ...options,
})

或者,通过将监听器附加到编辑器实例上的 contentError 事件。

const editor = new Editor({
  enableContentCheck: true,
  content: invalidContent,
  ...options,
})

editor.on('contentError', ({ editor, error, disableCollaboration }) => {
  // 在这里处理
})

有关更多实现示例,请参考 events 部分。

在不启用内容检查的情况下监听 contentError 事件

如果您想在不启用内容检查的情况下监听 contentError 事件,请在初始化 Tiptap 编辑器时将 emitContentError 设置为 true

new Editor({
  enableContentCheck: false,
  emitContentError: true,
  ...options,
})

此设置允许您在编辑器中拥有无效内容,但仍会在内容无效时收到通知。

推荐处理

您处理模式错误的方式将取决于您的应用程序和要求,但以下是我们的建议:

不使用协作编辑

根据您的用例,删除未知内容的默认行为使您的内容保持在已知有效状态,以供将来编辑。

使用协作编辑

根据您的用例,您可能希望设置 enableContentCheck 标志,并监听 contentError 事件。当收到此事件时,您可能希望类似于以下示例进行响应:

onContentError({ editor, error, disableCollaboration }) {
  // 删除协作扩展。
  disableCollaboration()

  // 由于内容无效,我们不想发出更新
  // 防止与其他编辑器或服务器同步
  const emitUpdate = false

  // 禁用编辑器以防止进一步的用户输入
  editor.setEditable(false, emitUpdate)

  // 可能向用户显示通知,告知他们需要刷新应用程序
}