扩展 API
Tiptap 的强大之处在于其灵活性。您可以从头开始创建自己的扩展,并构建一个独特的编辑器体验,以满足您的需求。
所有扩展的基本结构都是相同的,无论您是创建节点、标记还是功能更改。而且,Tiptap 中的一切都是基于扩展的。
创建扩展
扩展为 Tiptap 添加了新功能。在文档中,您会经常看到“扩展”这个词,甚至在节点和标记中也是如此。但也有字面意义上的扩展。这些扩展不能添加到架构中(就像标记和节点那样),但它们可以添加功能或改变编辑器的行为。
一个很好的例子是监听编辑器事件并处理它们的内容。像这样:
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
name: 'customExtension',
onUpdate() {
console.log(this.editor.getJSON())
},
})您也可以使用回调函数创建扩展。这在您想封装扩展逻辑时非常有用,例如,当您想定义事件处理程序或其他自定义逻辑时。
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create(() => {
// 定义扩展内部使用的变量或函数
const customVariable = 'foo'
function onCreate() {}
function onUpdate() {}
return {
name: 'customExtension',
onCreate,
onUpdate,
// 在这里编写您的代码。
}
})此扩展监听编辑器的 update 事件,并将编辑器当前的 JSON 表示日志输出到控制台。
它这样安装到编辑器中:
import { Editor } from '@tiptap/core'
const editor = new Editor({
extensions: [CustomExtension],
})
// 或者在使用 React 或 Vue 时
const editor = useEditor({
extensions: [CustomExtension],
})此扩展数组可以包含任意数量的扩展。它们将按列出的顺序安装,或按它们的 priority 属性排序。
现在我们已经看到扩展的基本结构,让我们深入了解您可以用来创建自己扩展的所有扩展选项。
扩展选项
在创建扩展时,您可以定义一组用户可配置的选项。这些选项可以用于自定义扩展的行为,或提供额外的功能。
name
扩展的名称。这用于在编辑器的扩展管理器中识别扩展。
const CustomExtension = Extension.create({
name: 'customExtension',
})如果扩展是节点或标记,则名称用于在编辑器的架构中识别节点或标记,并因此保存在编辑器内容的 JSON 表示中。有关更多信息,请参见 存储您的内容为 JSON。
priority
优先级定义扩展注册的顺序。默认优先级为 100,大多数扩展都是这个值。具有更高优先级的扩展将更早加载。
import Link from '@tiptap/extension-link'
const CustomLink = Link.extend({
priority: 1000,
})扩展加载的顺序影响两个方面:
- 插件顺序(优先级更高的扩展的 ProseMirror 插件将先运行。)
- 架构顺序
例如,Link 标记具有更高的优先级,这意味着它将被渲染为 <a href="…"><strong>Example</strong></a> 而不是 <strong><a href="…">Example</a></strong>。
addOptions
addOptions 方法用于定义扩展的选项。此方法应返回一个对象,其中包含用户可配置的选项。
type CustomExtensionOptions = {
customOption: string
}
declare module '@tiptap/core' {
interface ExtensionOptions {
customOption: CustomExtensionOptions
}
}
const CustomExtension = Extension.create<CustomExtensionOptions>({
name: 'customExtension',
addOptions() {
return {
customOption: 'default value',
}
},
})这会暴露在安装扩展时可以设置的配置:
const editor = new Editor({
extensions: [CustomExtension.configure({ customOption: 'new value' })],
})addStorage
addStorage 方法用于定义扩展的存储(本质上是一个简单的状态管理器)。此方法应返回一个对象,其中包含扩展可以使用的存储。
type CustomExtensionStorage = {
customValue: string
}
declare module '@tiptap/core' {
interface ExtensionStorage {
customExtension: CustomExtensionStorage
}
}
const CustomExtension = Extension.create<any, CustomExtensionStorage>({
name: 'customExtension',
addStorage() {
return {
customValue: 'default value',
}
},
})这会暴露在扩展内部可以访问的存储:
const CustomExtension = Extension.create<any, CustomExtensionStorage>({
name: 'customExtension',
addStorage() {
return {
customValue: 'default value',
}
},
onUpdate() {
console.log(this.storage.customValue) // 'default value'
},
})或者通过编辑器:
const editor = new Editor({
extensions: [CustomExtension],
})
editor.storage.customExtension.customValue // 'default value'注意
editor.storage 由扩展的名称命名空间。
addCommands
addCommands 方法用于定义扩展的命令。此方法应返回一个对象,其中包含用户可以执行的命令。
declare module '@tiptap/core' {
interface Commands<ReturnType> {
customExtension: {
customCommand: () => ReturnType
}
}
}
const CustomExtension = Extension.create({
name: 'customExtension',
addCommands() {
return {
customCommand:
() =>
({ commands }) => {
return commands.setContent('Custom command executed')
},
}
},
})在 addCommands 内部使用 commands 参数
要在 addCommands 内部访问其他命令,请使用传递给它的 commands 参数。
这会暴露用户可以执行的命令:
const editor = new Editor({
extensions: [CustomExtension],
})
editor.commands.customCommand() // 'Custom command executed'
editor.chain().customCommand().run() // 'Custom command executed'addKeyboardShortcuts
addKeyboardShortcuts 方法用于定义键盘快捷键。此方法应返回一个对象,其中包含用户可以使用的键盘快捷键。
const CustomExtension = Extension.create({
name: 'customExtension',
addKeyboardShortcuts() {
return {
'Mod-k': () => {
console.log('Keyboard shortcut executed')
},
}
},
})这会暴露可供用户使用的键盘快捷键。
addInputRules
使用输入规则,您可以定义正则表达式以监听用户输入。它们用于 Markdown 快捷方式,例如将文本 (c) 转换为 ©(还有许多其他用途)与 Typography 扩展一起使用。对于标记,使用 markInputRule 辅助函数,对于节点,使用 nodeInputRule。
默认情况下,位于两侧的波浪号之间的文本会转换为 删除线文本。如果您认为在每边只需一个波浪号就足够了,可以像这样覆盖输入规则:
import { Extension } from '@tiptap/core'
import { markInputRule } from '@tiptap/core'
const CustomExtension = Extension.create({
name: 'customExtension',
addInputRules() {
return [
markInputRule({
find: /(?:~)((?:[^~]+))(?:~)$/,
type: this.editor.schema.marks.strike,
}),
]
},
})现在,当您输入 ~striked text~ 时,它将被转换为 删除线文本。
想了解更多关于输入规则的信息?请查看 输入规则 文档。
addPasteRules
粘贴规则的工作方式与输入规则(见上文)相似。但不同之处在于,它们应用于粘贴的内容,而不是监听用户输入。
正则表达式有一个微小的区别。输入规则通常以 $ 符号结束(这意味着“在行尾断言位置”),而粘贴规则通常浏览所有内容,不带该 $ 符号。
将上述示例应用于粘贴规则将如下所示:
import { Extension } from '@tiptap/core'
import { markPasteRule } from '@tiptap/core'
const CustomExtension = Extension.create({
name: 'customExtension',
addPasteRules() {
return [
markPasteRule({
find: /(?:~)((?:[^~]+))(?:~)/g,
type: this.editor.schema.marks.strike,
}),
]
},
})想了解更多关于粘贴规则的信息?请查看 粘贴规则 文档。
事件
你可以使用 transformPastedHTML 方法,在粘贴的 HTML 被解析并插入编辑器之前对其进行转换。这对于清理样式、移除危险内容或修改粘贴的 HTML 以匹配编辑器要求非常有用。
工作原理
当用户粘贴内容时:
- 扩展会按优先级排序(优先级越高,越先运行)。
- 每个扩展按顺序调用自己的
transformPastedHTML。 - 每次转换都会接收上一次转换的输出。
- 最终转换后的 HTML 会被解析并插入编辑器。
基本示例
import { Extension } from '@tiptap/core'
const CleanStylesExtension = Extension.create({
name: 'cleanStyles',
transformPastedHTML(html) {
// Remove all inline style attributes
return html.replace(/\s+style="[^"]*"/gi, '')
},
})转换链示例
多个扩展可以转换同一段粘贴内容。使用 priority 选项控制顺序:
// Extension 1: Remove scripts (runs first, priority: 110)
const SecurityExtension = Extension.create({
name: 'security',
priority: 110,
transformPastedHTML(html) {
let cleanHtml = html
// Remove script tags
cleanHtml = cleanHtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
// Remove event handler attributes (onclick, onerror, etc.)
cleanHtml = cleanHtml.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '')
// Remove javascript: protocol from href (handles double-quoted, single-quoted, and unquoted attributes)
cleanHtml = cleanHtml.replace(
/href\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*'|javascript:[^\s>]*)/gi,
'href="#"',
)
return cleanHtml
},
})
// Extension 2: Remove styles (runs second, priority: 100)
const CleanStylesExtension = Extension.create({
name: 'cleanStyles',
priority: 100,
transformPastedHTML(html) {
return html.replace(/\s+style="[^"]*"/gi, '')
},
})在此示例中:
SecurityExtension首先运行(优先级 110)并移除危险内容。CleanStylesExtension接收SecurityExtension的输出并移除样式。- 最终清理后的 HTML 会被插入编辑器。
使用扩展上下文
该方法可以通过 this 访问扩展上下文:
const ConditionalCleanup = Extension.create({
name: 'conditionalCleanup',
addOptions() {
return {
removeStyles: true,
removeClasses: false,
}
},
transformPastedHTML(html) {
let cleanHtml = html
// Access extension options
if (this.options.removeStyles) {
cleanHtml = cleanHtml.replace(/\s+style="[^"]*"/gi, '')
}
if (this.options.removeClasses) {
cleanHtml = cleanHtml.replace(/\s+class="[^"]*"/gi, '')
}
// Access editor instance
this.editor.emit('htmlTransformed', { original: html, transformed: cleanHtml })
return cleanHtml
},
})常见用例
移除 Google Docs 格式:
const GoogleDocsCleanup = Extension.create({
name: 'googleDocsCleanup',
transformPastedHTML(html) {
return (
html
// Remove Google Docs spans with inline styles
.replace(/<span[^>]*style="[^"]*"[^>]*>(.*?)<\/span>/gi, '$1')
// Remove Google Docs IDs
.replace(/\s+id="docs-internal-[^"]*"/gi, '')
// Remove empty spans
.replace(/<span>(.*?)<\/span>/gi, '$1')
)
},
})移除 Microsoft Word 格式:
const WordCleanup = Extension.create({
name: 'wordCleanup',
transformPastedHTML(html) {
return (
html
// Remove Word-specific classes
.replace(/\s+class="Mso[^"]*"/gi, '')
// Remove Word-specific tags
.replace(/<o:p>.*?<\/o:p>/gi, '')
// Remove conditional comments
.replace(/<!--\[if.*?<!\[endif\]-->/gs, '')
)
},
})事件
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
onBeforeCreate() {
// 编辑器即将被创建。
},
onCreate() {
// 编辑器已准备就绪。
},
onUpdate() {
// 内容已更改。
},
onSelectionUpdate({ editor }) {
// 选择已更改。
},
onTransaction({ transaction }) {
// 编辑器状态已更改。
},
onFocus({ event }) {
// 编辑器已获得焦点。
},
onBlur({ event }) {
// 编辑器不再获得焦点。
},
onDestroy() {
// 编辑器正在被销毁。
},
})dispatchTransaction
该钩子允许您在事务分发之前拦截和修改事务。它采用类似 Koa 或 Express 的中间件模式,每个扩展都会接收到 transaction 和 next 函数。
工作原理
- 中间件链:当事务被分发时,Tiptap 会创建一个包含所有定义了
dispatchTransaction钩子的扩展的链。 - 按优先级排序:该链按扩展优先级排序。具有更高优先级(如
1000)的扩展会更早调用,并且包装优先级较低的扩展。 - 传递调用:您必须调用
next(transaction)才能将事务传递给链中的下一个扩展。最终,最后的next()调用会将事务传递给编辑器的基础分发函数(或者如果定义了自定义的editorProps.dispatchTransaction,则调用自定义函数)。 - 阻止事务:如果您不调用
next(transaction),事务将被阻止,不会传递给编辑器。
import { Extension } from '@tiptap/core'
const LoggingExtension = Extension.create({
name: 'loggingExtension',
priority: 1000, // 提前运行
dispatchTransaction({ transaction, next }) {
`addGlobalAttributes` 允许你一次性为多个扩展添加属性。这在你想为多个节点或标记添加相同属性时非常有用。
// transaction.setMeta('customMeta', true)
// 调用 next 将其传递给下一个扩展
next(transaction)
console.log('在其他扩展之前拦截事务...')
// 你可以在此修改事务
},
})中间件生命周期
因为每个扩展都会“包裹”下一个扩展,您可以在事务被后续链和编辑器处理之前和之后执行代码。
dispatchTransaction({ transaction, next }) {
// 1. 预处理:在调用 next() 之前执行的代码
const start = performance.now()
next(transaction)
// 2. 后处理:在调用 next() 之后执行的代码
const end = performance.now()
console.log(`分发耗时 ${end - start} 毫秒`)
}addProseMirrorPlugins
您可以向扩展添加 ProseMirror 插件。如果您想使用 ProseMirror 插件扩展编辑器,这非常有用。
使用现有 ProseMirror 插件
您可以将现有的 ProseMirror 插件包装在 Tiptap 扩展中,如下面的示例所示。
import { history } from '@tiptap/pm/history'
const History = Extension.create({
addProseMirrorPlugins() {
return [
history(),
// …
]
},
})创建 ProseMirror 插件
您也可以创建自定义的 ProseMirror 插件。以下是一个将消息记录到控制台的自定义 ProseMirror 插件示例。
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
name: 'customExtension',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('customPlugin'),
view() {
return {
update() {
console.log('自定义插件已更新')
},
}
},
}),
]
},
})要了解有关 ProseMirror 插件的更多信息,请查阅 ProseMirror 文档。
addDecorations
The addDecorations method adds decorations to the editor. Use decorations for highlights, search results, and small widgets. They change what users see, but they do not change the saved document.
Build the decorations with the Decoration class imported from @tiptap/core:
import { Decoration, Extension } from '@tiptap/core'
const MyExtension = Extension.create({
name: 'myExtension',
addDecorations() {
return {
create: ({ editor, state, view }) => [Decoration.Inline(1, 5, { class: 'highlight' })],
}
},
})The Decoration class has three static methods: Decoration.Node (attributes on a node's DOM wrapper), Decoration.Inline (a styled span over a range), and Decoration.Widget (a DOM node or framework component at a position). For large documents you can gate recomputation with shouldUpdate or rebuild only edited blocks with update: 'changedRanges'. Use editor.commands.updateDecorations() to force a full rebuild at any time.
Use decorations for view-only markers
Reach for addDecorations when the marker is purely visual. When you need persisted custom
document content or editable custom blocks, use a node
view instead.
Read the Decorations guide for the three kinds and performance strategies, follow the Vanilla JS, React, or Vue 3 tutorial, and use the Decorations API reference for every signature.
addGlobalAttributes
addGlobalAttributes 方法用于为多个扩展同时添加属性。这对于诸如文本对齐、行高或其他应在许多节点和标记类型上可用的样式相关属性非常有用。
你可以通过 types 属性指定哪些扩展将接收该属性:
types: ['heading', 'paragraph']- 应用于特定类型名称types: '*'- 应用于所有节点类型(不包括内置的文本节点)和所有标记类型types: 'nodes'- 应用于所有节点类型(不包括内置的文本节点)types: 'marks'- 应用于所有标记类型
以下示例演示了如何将属性应用于特定类型:
const TextAlign = Extension.create({
name: 'textAlign',
addGlobalAttributes() {
return [
{
types: ['heading', 'paragraph'],
attributes: {
textAlign: {
default: 'left',
renderHTML: (attributes) => ({
style: `text-align: ${attributes.textAlign}`,
}),
parseHTML: (element) => element.style.textAlign || 'left',
},
},
},
]
},
})更多选项(包括字符串简写语法)请参阅应用全局属性 指南。
addExtensions
您可以向扩展添加更多扩展。如果您想创建一组属于同一类别的扩展,这非常有用。
import { Extension } from '@tiptap/core'
import CustomExtension1 from './CustomExtension1'
const CustomExtension = Extension.create({
name: 'customExtension',
addExtensions() {
return [
CustomExtension1.configure({
name: 'customExtension1',
}),
]
},
})extendNodeSchema
您可以使用 extendNodeSchema 方法扩展编辑器的 NodeConfig。如果您想向节点架构添加额外属性,这将非常有用。
import { Extension } from '@tiptap/core'
declare module '@tiptap/core' {
// 这将向 NodeConfig 添加一个新的配置选项
interface NodeConfig {
customAttribute: {
default: null
}
}
}
const CustomExtension = Extension.create({
name: 'customExtension',
extendNodeSchema() {
return {
customAttribute: {
default: null,
},
}
},
})extendMarkSchema
您可以使用 extendMarkSchema 方法扩展编辑器的 MarkConfig。如果您想向标记架构添加额外属性,这将非常有用。
import { Extension } from '@tiptap/core'
declare module '@tiptap/core' {
// 这将向 MarkConfig 添加一个新的配置选项
interface MarkConfig {
customAttribute: {
default: null
}
}
}
const CustomExtension = Extension.create({
name: 'customExtension',
extendMarkSchema() {
return {
customAttribute: {
default: null,
},
}
},
})这有什么可用的?
这些扩展不是类,但您在扩展中的 this 处仍然可以访问一些重要内容。
// 扩展的名称,例如 'bulletList'
this.name
// 编辑器实例
this.editor
// ProseMirror 类型(如果是节点或标记)
this.type
// 包含所有设置的对象
this.options
// 所有扩展的内容
this.parent
// 存储对象
this.storage