假设你在做知识库,需要一种“提示组”:里面可以有多个段落,用户能输入、选中、复制,并把它和正文一起保存。这已经超出了给段落换个颜色的范围——提示组是文档中一种有名称、有子内容的新结构。
本章把这种结构命名为 docsNote。先理解内容和命令,再看 Canvas/SVG 额外需要的显示与选区配置。
1. 定义它保存什么
在 feature/node.ts 中通过 Node.create() 定义提示组。下面是决定文档结构的部分:
const DocsNote = Node.create({
name: 'docsNote',
group: 'block',
content: 'block+',
isolating: true,
})
name 是保存到 JSON 中的节点名。group: 'block' 让它像段落一样出现在正文中。content: 'block+' 表示里面至少有一个块级节点,例如段落;isolating 则让通用编辑操作尊重这个容器的边界。
完整文件还定义了 HTML 的读取与输出规则:提示组对应 aside[data-docs-note],子内容放在其中。这样复制为 HTML 时保留它的结构。以上片段只解释结构字段,完整定义在本页末尾按文件查阅。
2. 提供一个插入命令
界面应当调用“插入提示组”,不需要知道它内部如何拼接 JSON。在 feature/extension.ts 中提供 insertDocsNote(text),命令的关键部分是:
insertDocsNote: (text: string) => ({ commands }) =>
commands.insertContent({
type: 'docsNote',
content: [{
type: 'paragraph',
content: [{ type: 'text', text }],
}],
})
它复用 insertContent(),所以仍经过 Core 的事务与撤销历史。完整文件为命令补充 TypeScript 声明,并通过 addExtensions() 安装节点。安装 DocsNoteFeature 后,按钮可以这样调用:
editor.chain().focus().insertDocsNote('这里是提示内容').run()
现在,“新增节点”和“通过界面插入它”已经连起来了。can().insertDocsNote(...) 也可以用来检查按钮是否可用。
3. 告诉 Canvas/SVG 怎样排列子内容
DOM 可以根据 HTML 生成页面元素;Canvas/SVG 使用 QuireKit 的排版结果,所以仅有 renderHTML() 还不够。提示组还需要一个布局提供者(layout provider)。
feature/layout.ts 使用引擎已有的子块排版服务:先在可用宽度内排列子段落,再把这些段落放进提示组的区域。它不自己测量浏览器 DOM,也不重新实现文字排版。addLayoutProviders() 把这项能力交给编辑器。
这一步使提示组在 Canvas 和 SVG 中都能显示。布局接口涉及尺寸、文档范围和分页,完整参数在 扩展 API中查阅;第一次接入可以先沿用此例的普通容器布局。
4. 让选区理解容器边界
用户在组内拖选时,应该像选择普通文字一样工作;从组外拖到组内时,编辑器还要知道哪些子节点属于这次选择。
feature/selection.ts 定义这两种行为:组内交给文字选区,跨越边界时按子节点处理。addSelectionAdapters() 注册它。显示与选择分开定义,才能让复制、替换和跨容器选择使用相同的内容范围。
试用并检查结果
点击“插入提示组”,在里面输入文字,拖选组内和组外的内容,再撤销。切到 SVG 后内容应继续存在;下载 JSON 可以看到 docsNote 和它包含的段落。
按职责查阅完整文件
| 文件 | 什么时候看 | 它负责什么 |
|---|---|---|
feature/node.ts | 要增加字段或修改允许的子内容 | 节点结构、HTML 读取与输出 |
feature/extension.ts | 要安装功能或调整插入动作 | 命令类型、命令实现、能力注册 |
feature/layout.ts | 要改变容器的排列方式 | Canvas/SVG 中子内容的尺寸和位置 |
feature/selection.ts | 要改变跨容器选择行为 | 容器内部与边界的选区规则 |
下面一次只显示你选择的文件。先读 node.ts 和 extension.ts,需要调整排版时再读另外两个。
import { Extension } from "@quirekit/core";
import { DocsNote } from "./node";
import { noteLayout } from "./layout";
import { noteSelection } from "./selection";
declare module "@quirekit/core" {
/** The same typed command registry used by direct, chain and can modes. */
interface Commands<ReturnType> {
/** Feature-owned commands. */
docsNote: {
/** @param text - Paragraph text inside a new note. @returns Whether insertion can run or was handled. */
insertDocsNote: (text: string) => ReturnType;
};
}
}
/** One installable feature owns schema, command, retained layout and structured selection. */
export const DocsNoteFeature = Extension.create({
name: "docsNoteFeature",
/** @returns Schema owner; no second document is maintained. */
addExtensions() { return [DocsNote]; },
/** @returns Typed raw command factories, also safe in can() dry runs. */
addCommands() {
return {
insertDocsNote: (text: string) => ({ commands }) => commands.insertContent({ type: "docsNote",
content: [{ type: "paragraph", content: [{ type: "text", text }] }] }),
};
},
/** @returns Renderer-neutral geometry owner. */
addLayoutProviders() { return [noteLayout]; },
/** @returns Structured crossing semantics, independently of hit testing. */
addSelectionAdapters() { return [noteSelection]; },
});
保存与其他格式
提示组的 JSON 与正文一起保存,恢复时必须再次安装此扩展。本例没有实现 DOCX 转换或 AI 能力;需要这些功能时,要分别通过 addDocx() 和 addAiCapabilities() 补充规则。能保存 JSON 并不代表其他文件格式已经认识这种节点。
如果你的需求是增加工具栏或侧栏,而不是新的正文结构,继续阅读产品插件。