我们在使用web编辑器时往往需要extensions自定义扩展,因为如何一款web编辑器自带的扩展都不可能满足场景的需求,tiptap可以这种个性化的需求有两种方法,一是按照官方方法实现扩展,一种是自己写vue组件调用tiptap命令,这样也可以非常方便的集成到tiptap。
tiptap提供了非常多的扩展,当这些扩展无法满足你的需求时,你可以对它进行再次扩展,每个扩展都有一个extend()方法,该方法接受一个object,其中object包含您想要更改或添加的所有内容。下面代码演示了扩展BulletList列扩展,实现按下Ctrl+L快捷键把普通文本转成列
// 1. 导入扩展
import BulletList from '@tiptap/extension-bullet-list'
// 2. 重写快捷键
const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
'Mod-l': () => this.editor.commands.toggleBulletList(),
}
},
})
// 3. 注册扩展
new Editor({
extensions: [
CustomBulletList(),
// …
],
})
扩展名在很多地方被使用,使用不太容易改变名称,如果要更改现有扩展的名称,可以复制整个扩展并在所有情况下更改名称。扩展名也是JSON的一部分,如果你将编辑器内容存储为JSON,则也需要更改它的名称。
priority 定义了注册扩展的顺序,扩展的默认优先级是100,这是大多数扩展的优先级,值越大越先注册。
import Link from '@tiptap/extension-link'
const CustomLink = Link.extend({
priority: 1000,
})
扩展的注册顺序会影响,Plugin order和Schema order
Link扩展拥有更高的优先级,将把<strong><a href="…">Example</a></strong> 呈现为<a href="…"><strong>Example</strong></a>
对已有扩展进行再次封装时,你可以重新进行重写配置项,实现代码如下
import Heading from '@tiptap/extension-heading'
const CustomHeading = Heading.extend({
addOptions() {
return {
...this.parent?.(),
levels: [1, 2, 3],
}
},
})
有些情况下你可能希望在扩展中存储一下数据,你可以通过this.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 配置内容的结构、嵌套、行为等。让我们来看看几个常见的用例,默认的Blockquote扩展可以包裹其他节点,比如标题,如果你想只允许段落,相应地设置content属性:
// Blockquotes must only include paragraphs
import Blockquote from '@tiptap/extension-blockquote'
const CustomBlockquote = Blockquote.extend({
content: 'paragraph*',
})
Schema 允许你的节点是可拖拽的,配置draggable选项的即可,默认为false,但你可以重写它。
// Draggable paragraphs
import Paragraph from '@tiptap/extension-paragraph'
const CustomParagraph = Paragraph.extend({
draggable: true,
})
你可以使用attributes属性在内容中存储其他信息,比如你想扩展Paragraph节点,让它有不同的颜色:
const CustomParagraph = Paragraph.extend({
addAttributes() {
// Return an object with attribute configuration
return {
color: {
default: 'pink',
},
},
},
})
// 输出结果:
// <p color="pink">Example Text</p>
我们也可以使用renderHTML实现内联样式属性。
const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
// Take the attribute values
renderHTML: attributes => {
// … and return an object with HTML attributes.
return {
style: `color: ${attributes.color}`,
}
},
},
}
},
})
// 输出结果:
// <p style="color: pink">Example Text</p>
您还可以控制从HTML中解析属性的方式,你可以将颜色存储在一个名为data-color的属性中(而不仅仅是color)。
const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
// Customize the HTML parsing (for example, to load the initial content)
parseHTML: element => element.getAttribute('data-color'),
// … and customize the HTML rendering.
renderHTML: attributes => {
return {
'data-color': attributes.color,
style: `color: ${attributes.color}`,
}
},
},
}
},
})
// Result:
// <p data-color="pink" style="color: pink">Example Text</p>
你可以使用render: false完全禁用属性的呈现,如果你想添加一个属性到一个扩展,并保留现有的属性,你可以通过this.parent()访问它们。
const CustomTableCell = TableCell.extend({
addAttributes() {
return {
...this.parent?.(),
myCustomAttribute: {
// …
},
}
},
})
属性可以同时应用到多个扩展。这对于文本对齐、行高、颜色、字体系列和其他样式相关属性非常有用。
import { Extension } from '@tiptap/core'
const TextAlign = Extension.create({
addGlobalAttributes() {
return [
{
// Extend the following extensions
types: [
'heading',
'paragraph',
],
// … with those attributes
attributes: {
textAlign: {
default: 'left',
renderHTML: attributes => ({
style: `text-align: ${attributes.textAlign}`,
}),
parseHTML: element => element.style.textAlign || 'left',
},
},
},
]
},
})
使用renderHTML函数你可以将扩展渲染为HTML。我们将一个属性对象传递给它,其中包含所有本地属性、全局属性和配置的CSS类。下面是一个来自Bold扩展的例子:
renderHTML({ HTMLAttributes }) {
return ['strong', HTMLAttributes, 0]
}
数组中的第一个值我HTML标签的名称。如果第2个元素是一个对象,它将被解释为一组属性,在此之后的任何元素都呈现为子元素。
数字0用来指示内容应该插入的位置,让我们看看带有两个嵌套标记的CodeBlock扩展的呈现:
renderHTML({ HTMLAttributes }) {
return ['pre', ['code', HTMLAttributes, 0]]
}
如果你想在添加一些特定的属性,可以从@tiptap/core导入mergeAttributes:
import { mergeAttributes } from '@tiptap/core'
// ...
renderHTML({ HTMLAttributes }) {
return ['a', mergeAttributes(HTMLAttributes, { rel: this.options.rel }), 0]
}
使用parseHTML()函数尝试从编辑器加载html文档,下面是 Bold mark 的简化示例:
parseHTML() {
return [
{
tag: 'strong',
},
]
}
这定义了一个将所有<strong>标签转换为Bold标签的规则,但你可以使用更高级的用法,下面是完整的例子:
parseHTML() {
return [
// <strong>
{
tag: 'strong',
},
// <b>
{
tag: 'b',
getAttrs: node => node.style.fontWeight !== 'normal' && null,
},
// <span style="font-weight: bold"> and <span style="font-weight: 700">
{
style: 'font-weight',
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
},
]
}
这将检查<strong>和<b>标记。如你所见,你可以选择性地传递一个getAttrs回调,以添加更复杂的检查。
在这个例子中,你可能已经注意到getAttrs函数有两个目的:
检查HTML属性以确定规则是否匹配
获取DOM Element并使用HTML属性来设置标记或节点属性
parseHTML() {
return [
{
tag: 'span',
getAttrs: element => {
// Check if the element has an attribute
element.hasAttribute('style')
// Get an inline style
element.style.color
// Get a specific attribute
element.getAttribute('data-color')
},
},
]
}
你可以返回一个对象,其中属性作为键,解析值用于设置mark或node属性。不过,我们建议在addAttributes()中使用parseHTML,更详细的信息请访问 https://prosemirror.net/docs/ref/#model.ParseRule。
在扩展中使用命令或添加一个Commands命令:
import Paragraph from '@tiptap/extension-paragraph'
const CustomParagraph = Paragraph.extend({
addCommands() {
return {
paragraph: () => ({ commands }) => {
return commands.setNode('paragraph')
},
}
},
})
大多数核心的扩展都带有快捷键,你可以使用addKeyboardShortcuts()方法,覆盖预定义的快捷键:
// Change the bullet list keyboard shortcut
import BulletList from '@tiptap/extension-bullet-list'
const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
'Mod-l': () => this.editor.commands.toggleBulletList(),
}
}
})
使用输入规则Input rules,你可以定义正则表达式来侦听用户输入,比如将文本(c)转换为©字符。mark标记使用markInputRule函数,node节点使用nodeInputRule函数。
// Use the ~single tilde~ markdown shortcut
import Strike from '@tiptap/extension-strike'
import { markInputRule } from '@tiptap/core'
// Default:
// const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/
// New:
const inputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/
const CustomStrike = Strike.extend({
addInputRules() {
return [
markInputRule({
find: inputRegex,
type: this.type,
}),
]
}
})
粘贴规则的工作原理与输入规则类似(见上文),应用于粘贴的内容。 正则表达式中有一个微小的区别。输入规则通常以$ 符号结束(这意味着“断言在一行末尾的位置”),粘贴规则通常会查看所有内容,并且没有$ 符号,如下面的示例所示。
// Check pasted content for the ~single tilde~ markdown syntax
import Strike from '@tiptap/extension-strike'
import { markPasteRule } from '@tiptap/core'
// Default:
// const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/g
// New:
const pasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))/g
const CustomStrike = Strike.extend({
addPasteRules() {
return [
markPasteRule({
find: pasteRegex,
type: this.type,
}),
]
}
})
你可以将事件监听器移动到单独的扩展中,下面代码是一个所有事件监听器的例子:
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
onCreate() {
// 编辑器初始化完成
},
onUpdate() {
// 文档内容更新事件
},
onSelectionUpdate({ editor }) {
// 选中内容改变
},
onTransaction({ transaction }) {
// 编辑器状态更改事件 The editor state has changed.
},
onFocus({ event }) {
// 编辑器获得焦点事件
},
onBlur({ event }) {
// 编辑器失去焦点事件
},
onDestroy() {
// 编辑器销毁事件
},
})
下面代码是 this 对象的属性列表。
// 扩展名称 'bulletList'
this.name
// 编辑器实例
this.editor
// ProseMirror type
this.type
// 所有的设置
this.options
// Everything that’s in the extended extension
this.parent
因为tiptap建立在ProseMirror之上所以我们可以用addProseMirrorPlugins去封装ProseMirror插件。
Existing plugins 封装已存在的ProseMirror插件
import { history } from '@tiptap/pm/history'
const History = Extension.create({
addProseMirrorPlugins() {
return [
history(),
// …
]
},
})
访问ProseMirror API
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
export const EventHandler = Extension.create({
name: 'eventHandler',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('eventHandler'),
props: {
handleClick(view, pos, event) { /* … */ },
handleDoubleClick(view, pos, event) { /* … */ },
handlePaste(view, event, slice) { /* … */ },
// … and many, many more.
// Here is the full list: https://prosemirror.net/docs/ref/#view.EditorProps
},
}),
]
},
})
对于高级用例,你可能需要在节点内执行JavaScript,例如围绕图片呈现复杂的界面,它很强大,但也很复杂。简而言之,你需要返回一个父DOM元素和一个子DOM元素,下面个简化的例子:
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,
}
}
},
})
当现有扩展无法满足你的需求时,你可以创建自己的扩展,它与上面描述的扩展现有的扩展的语法相同。
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,
}
}
},
})
Create a node 创建节点
如果将文档看作树,那么node节点只是树中的一种内容类型。可以学习 Paragraph 、Heading 或 CodeBlock扩展
import { Node } from '@tiptap/core'
const CustomNode = Node.create({
name: 'customNode',
// Your code goes here.
})
Create a mark 创建标记
你可以对Node节点添加一个或多个标记Mark,例如添加内联样式Style,你可以学习例子 Bold、Italic 或 Highlight。
import { Mark } from '@tiptap/core'
const CustomMark = Mark.create({
name: 'customMark',
// Your code goes here.
})
Create an extension 创建新扩展
你可以通过TextAlign扩展学习如何创建一个新扩展。
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
name: 'customExtension',
// Your code goes here.
})
本教程Demo的源码点击这里下载,Tiptap Demo,下载完成后运行npm i初始化依赖包。