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',
},
},
}我们在这里注册了三个节点。doc、paragraph 和 text。doc 是根节点,允许一个或多个块节点作为子节点(content: 'block+')。因为 paragraph 在块节点组中(group: 'block'),所以我们的文档只能包含段落。我们的段落允许零个或多个内联节点作为子节点(content: 'inline*'),所以其中只能有 text。parseDOM 定义了如何从粘贴的 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。
通常,这适用于 Blockquote、CodeBlock、Heading 和 ListItem。
Node.create({
defining: true,
})隔离
对于应在常规编辑操作(如退格)中封闭光标的节点,例如 TableCell,设置 isolating: true。
Node.create({
isolating: true,
})允许间隙光标
Gapcursor 扩展注册了一个新模式属性,用于控制该节点的各个地方是否允许间隙光标。
Node.create({
allowGapCursor: false,
})表格角色
Table 扩展注册了一个新模式属性,以配置节点的角色。允许的值为 table、row、cell 和 header_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)
// 可能向用户显示通知,告知他们需要刷新应用程序
}