跳到主要内容

自定义功能

提示

为避免理解和翻译误差,特此说明,extensions指的是编辑器配置对象中的配置项extensions,此配置项支持 节点、标记和扩展,一般我将其称为三大件或功能,并不是单纯的指扩展一项。

介绍

Tiptap 的优势之一是它的可扩展性,您可以根据您的喜好扩展编辑器(的功能)。

Tiptap 内容有三大法宝构成:节点(node)、标记(mark)和扩展(extension),您都可去扩展丰富它们。

本章的所有内容 都是均可用于 这三大法宝上。

继承现有的功能

每个 Tiptap 的功能三大件都有一个extend()方法,它接受一个对象,其中包含您想要更改或添加的所有内容。

比方说,您想更改无序列表扩展的键盘快捷键,那如下就会把将普通段落转为无须列表的快捷键Control+Shift+8改为了Control+l

import BulletList from "@tiptap/extension-bullet-list";

// 重写该扩展里定义的快捷键设置
const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
"Mod-l": () => this.editor.commands.toggleBulletList(),
};
},
});

// 在你的编辑器中尝试一些你的新扩展吧
new Editor({
extensions: [
CustomBulletList(),
// …
],
});

名称

扩展名称无法(通过 extend)修改,换个思路,如果可以把整个扩展代码赋值一份 其中改写名字字段。然后再使用这个自定义的扩展 不就行了?!

设置项

通过addOptions,可以为三大件添加一些设置项(翻译为设置项目,其实不太合适,它算是定义的一些插件内部常量吧),这些设置项你可以在后边的renderHtml或parseHTML中用到。
如果你想更改默认设置,但是不完全重写,还想保持父级原有的内容,你可以这样

import Heading from "@tiptap/extension-heading";

const CustomHeading = Heading.extend({
addOptions() {
return {
...this.parent?.(),
levels: [1, 2, 3],
};
},
});

如上 CustomHeading 扩展 除了 levels 不一样,其它都和原 Heading 扩展保持一致!

创建新的功能

除了继承之外,我们还允许您头开始构建自己的功能扩展。

创建扩展

import { Extension } from "@tiptap/core";

const CustomExtension = Extension.create({
name: "customExtension",
// ...
});

创建节点

您可以将文档视为一棵树,那么节点只是该树中的一种内容。您可以学习Paragraph、Heading 或 CodeBlock。

import { Node } from "@tiptap/core";

const CustomNode = Node.create({
name: "customNode",
// ...
});

创建标记

标记可以应用于节点,例如添加内联格式:加粗、斜体。您可以学习Bold,Italic 和 Highlight。

import { Mark } from "@tiptap/core";

const CustomMark = Mark.create({
name: "customMark",
// ...
});

加载优先级

优先级定义了扩展注册的顺序。默认优先级是 100,这是大多数扩展所具有的。具有更高优先级的扩展将被更早地加载。

import Link from "@tiptap/extension-link";

const CustomLink = Link.extend({
priority: 1000,
});

除此之外,更高的优先级的扩展 还会影响到最终的渲染效果,例如 Link 标记具有更高的优先级。这意味着它将呈现为<a href="…"><strong>Example</strong></a>而不是<strong><a href="…">Example</a></strong>

扩展的内部存储

在某些时候,您可能希望在扩展实例中保存一些数据变量等。您可以在 下的扩展中访问它storage

import { Extension } from "@tiptap/core";

const CustomExtension = Extension.create({
name: "customExtension",

addStorage() {
return {
awesomeness: 100,
};
},

onUpdate() {
this.storage.awesomeness += 1;
},
});

在扩展外部,您可以通过 editor.storage 来访问

const editor = new Editor({
extensions: [CustomExtension],
});

const awesomeness = editor.storage.customExtension.awesomeness;

架构

架构,Tiptap 工作在严格的 schema 中。
schema 规定了标记、节点和扩展,这些共同组成了编辑器强大丰富的功能。
标记决定了样式,节点可以定义您的富文本内容标签支持 和嵌套规范等,而扩展则为您的编辑器提供了额外的功能。

属性

属性可以让您在(节点、标记、扩展)内容中存储附加信息。
假设您想扩展默认 Paragraph 节点以具有不同的颜色:

const CustomParagraph = Paragraph.extend({
addAttributes() {
// 返回一个属性配置对象
return {
color: {
default: "pink",
},
};
},
});

// 渲染结果如下
// <p color="pink">示例文本</p>

让我们继续使用颜色示例,并假设您想要添加一个内联样式来实际为文本着色。使用该 renderHTML 函数,您可以返回将在输出中呈现的 HTML 属性。

此示例根据以下值添加样式 HTML 属性 color:

const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
// 返回 如何呈现属性的字符串
renderHTML: (attributes) => {
// … 返回一个属性配置对象
return {
style: `color: ${attributes.color}`,
};
},
},
};
},
});

// 渲染结果如下
// <p style="color: pink">示例文本</p>

您还可以控制如何从 HTML 解析属性。
也许您想将颜色存储在一个名为 data-color(而不仅仅是 color)的属性中,下面是您将如何做到这一点:

🌰 举个例子
exampleImg
collapsePng
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import React from "react";
import Paragraph from '@tiptap/extension-paragraph'

const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
// 自定义HTML解析
parseHTML: (element) => element.getAttribute("data-color"),
// … 自定义HTML渲染
renderHTML: (attributes) => {
// 您还可以使用 rendered: false, 这样此属性就完全不会在呈现出来了
return {
"data-color": attributes.color,
style: `color: ${attributes.color}`,
};
},
}

// 配置如上color属性后 渲染结果将会如下
// <p data-color="pink" style="color: pink">示例文本</p>
};
},
});

export default () => {
const editor = useEditor({
extensions: [StarterKit, CustomParagraph],
content: '<p data-color="pink">示例文本</p>',
});

return (
<>
<EditorContent editor={editor} />
</>
);
};


扩展现有属性

如果要向三大件添加属性并保留现有属性,您可以通过 访问它们this.parent()

const CustomTableCell = TableCell.extend({
addAttributes() {
return {
...this.parent?.(),
myCustomAttribute: {
// …
},
}
},
})

全局属性

属性可以一次应用于多个三大件。
这对于文本对齐、行高、颜色、字体系列和其他与样式相关的属性很有用。

以TextAlign扩展来举例

const TextAlign = Extension.create({
addGlobalAttributes() {
return [
{
// 继承(即应用在)以下extensions(节点、标记、描述)
types: [
'heading',
'paragraph',
],
// … 拥有这些属性
attributes: {
textAlign: {
default: 'left',
renderHTML: attributes => ({
style: `text-align: ${attributes.textAlign}`,
}),
parseHTML: element => element.style.textAlign || 'left',
},
},
},
]
},
})

如果有时间,您可仔细查看TextAlign扩展的完整源代码

呈现 HTML

使用该renderHTML函数,您可以控制三大件(节点、标记、描述)如何呈现为 HTML。
renderHTML函数有一个参数,其中包含所有本地属性、全局属性和配置的 CSS 类。

这是Bold扩展的一个例子

renderHTML({ HTMLAttributes }) {
return ['strong', HTMLAttributes, 0]
},

返回是个数组中:

  • 第一个值应该是 HTML 标签的名称。
  • 第二个元素是一个对象或一个数组,若是对象它被解释为一组属性。
  • 第三个是一个数字,用于指示应该插入内容的位置。

若第二个元素为数组,则表示之后的任何元素都呈现为子元素。
让我们看一下带有两个嵌套标签的扩展的渲染 以CodeBlock为例:

renderHTML({ HTMLAttributes }) {
return ['pre', ['code', HTMLAttributes, 0]]
},

如果您想添加一些额外的属性,请使用mergeAttributes

import { mergeAttributes } from '@tiptap/core'

renderHTML({ HTMLAttributes }) {
return ['a', mergeAttributes(HTMLAttributes, { rel: this.options.rel }), 0]
},
🌰 举个例子
exampleImg
collapsePng
import { Mark, mergeAttributes } from "@tiptap/core";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import React from "react";

const CustomMark = Mark.create({
name: "test",
// 通过设置项 配置一些下边用到的 常量
addOptions() {
return {
herf: '/',
target: 'blank'
}
},
renderHTML({ HTMLAttributes }) {
// 通过mergeAttributes来合并 回显的内容中接收到的标签带的属性(这个
// 例子中没有,因为解析HTML需要用到下节的parseHTML)和额外添加的属性
return ["abc", mergeAttributes(HTMLAttributes, { ...this.options }), 0];
},
addCommands() {
return {
toggleDot:
() =>
({ commands }) => {
return commands.toggleMark(this.name);
},
};
},
});


export default () => {
const editor = useEditor({
extensions: [StarterKit, CustomMark],
content: "这是一些普通文本,标记完后按下f12审查下我吧",
});

if (!editor) {
return null;
}
return (
<>
<button
className={editor.isActive("test") ? "is-active" : ""}
onClick={() => editor.chain().focus().toggleDot().run()}
>
test标记
</button>
<EditorContent editor={editor} />
</>
);
};

解析HTML

此函数定义了 三大件的解析规则,遇到那些匹配到的标签会触发解析(进而执行renderHTML)。 下边是一个Bold的解析简化示例

parseHTML() {
return [
{
//意思是只有遇到strong 标签的时候才会触发本扩展/节点/标记
tag: 'strong',
},
]
},

使用 getAttrs

getAttrs根据您返回布尔值,来决定是否会触发解析(进而执行renderHTML)。

相比tag,他更加灵活。因为它是个函数,通过形参 您可以获取到当前的dom节点信息,进而根据节点信息 做出一些逻辑性的决定

parseHTML() {
return [
{
tag: 'span',
getAttrs: element => {
// 检查是否包含这个属性: element.hasAttribute('style')
// 检查行内样式: element.style.color
// 检查一个特殊属性: element.getAttribute('data-color')
// 比如 这个意思是:只有当tag为span,并且其span标签颜色为red的时候 才会匹配上 并解析
return element.style.color==='red';
},
},
]
}

我们甚至还可以用在属性选项中,这样设置属性的值将更加灵活。

addAttributes() {
return {
color: {
// 从`data-color` 属性获取颜色 来设置属性color
parseHTML: element => element.getAttribute('data-color'),
}
}
},

命令

命令是扩展/标记/节点内部暴漏的方法。
定义形式如下

import Paragraph from '@tiptap/extension-paragraph'

const CustomParagraph = Paragraph.extend({
addCommands() {
return {
// 要访问内部的其他命令,这里结构出commands参数就可以用
setP: () => ({ commands }) => {
return commands.setNode('paragraph')
},
}
},
})

使用如下

editor.commands.setP()

热键

大多数核心extensions都默认带有合理的键盘快捷键。
比方说,您想更改无序列表扩展的键盘快捷键,那如下就会把将普通段落转为无须列表的快捷键Control+Shift+8改为了Control+l

// 改变设定为无序列表 的默认快捷键
import BulletList from '@tiptap/extension-bullet-list'

const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
'Mod-l': () => this.editor.commands.toggleBulletList(),
}
},
})

输入规则

您可以定义正则表达式来侦听用户输入,然后做出修改。就像您常用的markdown编辑器那样。

对节点使用nodeInputRule,对于标记使用markInputRule,举个例子:将删除标记的输入激活规则修改下

🌰 举个例子
exampleImg
collapsePng
import StarterKit from "@tiptap/starter-kit";
import { markInputRule } from "@tiptap/core";
import Strike from "@tiptap/extension-strike";
import { EditorContent, useEditor } from "@tiptap/react";
import React from "react";

export default () => {
// 修改默认激活删除线规则: 由需要两边各输入两条波浪线 改为两边各输入一条即可
const CustomStrike = Strike.extend({
addInputRules() {
return [
markInputRule({
find: /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/,
type: this.type,
}),
];
},
});

const editor = useEditor({
extensions: [StarterKit, CustomStrike],
content: "在我两边各输入一条波浪线试试",
});

return <EditorContent editor={editor} />;
};

粘贴规则

粘贴规则就像输入规则(见上文)一样工作。但它们不是听用户输入的内容,而是监听用户的粘贴的内容操作。

import Strike from '@tiptap/extension-strike'
import { markPasteRule } from '@tiptap/core'

const CustomStrike = Strike.extend({
addPasteRules() {
return [
markPasteRule({
find: /(?:^|\s)((?:~)((?:[^~]+))(?:~))/g,
type: this.type,
}),
]
},
})

事件

extensions内部提供了各种钩子函数。

import { Extension } from '@tiptap/core'

const CustomExtension = Extension.create({
onCreate() {
// 编辑器已准备就绪.
},
onUpdate() {
// 内容发生变化
},
onSelectionUpdate({ editor }) {
// 选中的内容发生改变
},
onTransaction({ transaction }) {
// 编辑器状态改变
},
onFocus({ event }) {
// 编辑器被聚焦
},
onBlur({ event }) {
// 编辑器失焦
},
onDestroy() {
// 编辑器销毁
},
})

extensions中的this

尽管extensions不是类,但是this也会被经常使用。

// extensions(节点、标记、扩展)的名字, 比如 'bulletList(无序列表)'
this.name

// 编辑器当前实例
this.editor

// ProseMirror 类型
this.type

// 所有的设置对象
this.options

// 父级 节点、标记、扩展
this.parent

ProseMirror 插件(高级)

在ProseMirror 扩展的功能叫做 plugins,但是在我们Tiptap中 叫它extensions,其作用是一样的,不用太纠结。

毕竟,Tiptap 是建立在 ProseMirror 之上的,而 ProseMirror 也有一个非常强大的插件 API。要直接访问它,请使用addProseMirrorPlugins()。

现有插件

您可以将现有的 ProseMirror 插件包装在 Tiptap 的extensions中,如下例所示。

import { history } from '@tiptap/pm/history'

const History = Extension.create({
addProseMirrorPlugins() {
return [
history(),
// …
]
},
})

访问 ProseMirror API

您可以通过editor-props事件传递到ProseMirror中,也可以直接在Tiptap 扩展定义ProseMirror Plugin

import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'

export const EventHandler = Extension.create({
name: 'eventHandler',

addProseMirrorPlugins() {
return [
new Plugin({ //自定义一个ProseMirror 插件
key: new PluginKey('eventHandler'),
props: {
handleClick(view, pos, event) { /* … */ },
handleDoubleClick(view, pos, event) { /* … */ },
handlePaste(view, event, slice) { /* … */ },
// … 还有更多
// 这里是所有的事件列表: https://prosemirror.net/docs/ref/#view.EditorProps
},
}),
]
},
})

节点视图(高级)

renderHTML渲染还是过于简单了,比如您的节点和复杂您可以使用节点视图addNodeView

比如 您需要在您自定义的节点中添加点击事件,以及处理各种程序。

import Image from '@tiptap/extension-image'

const CustomImage = Image.extend({
addNodeView() {
return () => {
const container = document.createElement('div')

container.addEventListener('click', event => {
alert('clicked on the container')
})

const content = document.createElement('div')
container.append(content)

return {
dom: container,
contentDOM: content,
}
}
},
})

这里有个更具体的例子,TaskItem
建议您看我们专门写的节点视图教程文档