数据模型
引言
与许多其他编辑器不同,Tiptap 是基于一个 数据模型 来构建的,它定义了内容的结构。这样,你可以定义文档中可能出现的节点类型、它们的属性以及它们如何嵌套。
这个数据模型非常严格。你不能使用数据模型中未定义的任何 HTML 元素或属性。
让我举个例子:如果你在 Tiptap 中粘贴像 This is <strong>important</strong>
这样的内容,但没有处理 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",
});
节点和标记
区别
节点就像内容块,比如段落、标题、代码块、引言等。
标记可以应用于节点的特定部分,如粗体、斜体 或~~删除线~~ 文本。链接 也是标记。
节点数据模型
内容
content
属性精确地定义了节点可以包含的特定内容。ProseMirror 对此非常严格,意味着不符合数据模型的内容将被丢弃。它期望一个名称或组名作为字符串。这里有一些例子:
Node.create({
// 必须有一个或多个块
content: "block+",
// 必须有零个或多个块
content: "block*",
// 允许所有类型的 'inline' 内容(文本或硬换行)
content: "inline*",
// 只能有 'text' 的内容
content: "text*",
// 可以有一个或多个段落,或者如果有列表的话,也可以有列表
content: "(paragraph|list?)+",
// 必须有一个顶级标题,下面有一个或多个块
content: "heading block+",
});
标记
你可以使用 marks
属性在数据模型中定义哪些标记可以在节点内部使用。添加一个或多个名称或标记组,允许所有、不允许所有标记,如下所示:
Node.create({
// 只允许 'bold' 标记
marks: "bold",
// 只允许 'bold' 和 'italic' 标记
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*",
});
原生不可编辑
节点带有 atom: true
时,它们不是直接可编辑的,应被视为单个单元。在编辑器上下文中不太可能使用这种方式,但它看起来像这样:
Node.create({
atom: true,
});
一个例子是 Mention
扩展,它看起来像文本,但实际上更像是一个整体单元。由于它没有可编辑的文本内容,复制这样的节点时会为空。不过,好消息是你可以控制这一点。这是 Mention
扩展的一个示例:
// 将原子节点转换为纯文本
renderText({ node }) {
return `@${node.attrs.id}`
},
可选选择
除了可见的文本选择外,还有一个不可见的节点选择。如果你想让你的节点可选中,可以这样配置:
Node.create({
selectable: true,
});
可拖动
所有节点都可以通过 draggable
设置为可拖动(默认不拖动):
Node.create({
draggable: true,
});
代码
用户期望代码的行为非常特殊。对于包含代码的所有节点,你可以设置 code: true
来考虑这一点。
Node.create({
code: true,
});
空白处理
控制节点中的空白如何解析。
Node.create({
whitespace: "pre",
});
替代
当节点的内容被完全替换(例如,粘贴新内容)时,节点会被替换掉。如果一个节点在这种替换操作中应该保留,可以将其配置为 defining
。
通常适用于 Blockquote
、CodeBlock
、Heading
和 ListItem
。
Node.create({
defining: true,
});
隔离
对于希望在常规编辑操作(如退格)时隔离光标的节点,例如表格单元格,设置 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({
// 不得与 'bold' 标记共存
excludes: "bold",
// 排除所有其他标记
excludes: "_",
});
可退出
默认情况下,标记会 “捕获” 光标,意味着光标不能离开标记,除非将光标从左向右移动到没有标记的文本中。如果设置为 true
,则当标记位于节点末尾时,标记将变为可退出的。这对于使用代码标记尤其方便。
Mark.create({
// 将此标记设置为可退出 - 默认为 false
exitable: true,
});
分组
将此标记添加到一组扩展中,该组可以在内容属性的 schema 中引用。
Mark.create({
// 将此标记添加到 '基本' 组
group: "基本",
// 将此标记添加到 '基本' 和 'foobar' 组
group: "基本 foobar",
});
代码模式
用户期望代码的行为有所不同。对于包含代码的所有标记,可以设置 code: true
来考虑这一点。
Mark.create({
code: true,
});
跨节点
默认情况下,渲染为 HTML 时,标记可以跨越多个节点。设置 spanning: false
表示标记不应跨越多个节点。
Mark.create({
spanning: false,
});
获取底层的 ProseMirror 协议
在某些情况下,您需要与底层协议进行交互。如果您正在使用 Tiptap 的协作文本编辑功能,或者想手动将内容渲染为 HTML,则需要这个。
方案 1:通过 Editor
如果您需要在客户端使用,并且已经有一个 Editor 实例,可以通过 Editor 获取:
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:不使用 Editor
如果您只想获得 schema,但不需要初始化实际的编辑器,可以使用 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,
// 这里添加更多扩展
]);