Tiptap 是一个基于 ProseMirror 构建的富文本编辑器,它是一个灵活、可扩展的富文本编辑器,同时适用于 Vue.js 和 React。所以,无论你的技术栈是Vue,还是React,使用Tiptap都不用太过于在选型上纠结。Tiptap 的核心思路是通过插件系统提供丰富的功能,使得开发者可以根据需求定制编辑器的功能和样式。
Tiptap 的主要有5大部分组成:
首先,为了了解这些模块之间的关联关系,我们可以看看下面这系统架构幅图。
Tiptap 作为主要的入口,连接了 Core、Extensions、Commands、Schema 和 Vue/React components。Extensions 又包括了多个功能模块,如 Bold、Italic、List 和 Link。这样的架构使得 Tiptap 可以根据需求灵活地扩展功能和样式。
Tiptap 的 Core 模块是基于 ProseMirror 构建的,它负责处理编辑器的基本功能,如文本输入、选择、撤销和重做等。ProseMirror 是一个用于构建富文本编辑器的 JavaScript 库,提供了强大的文档模型和编辑功能,我们在上篇文章中有简单的介绍过,Tiptap实际上就是扩展了ProseMirror的 Nodes,Marks等等,所有的包装相关的源码,我们可以参考 https://github.com/ueberdosis/tiptap/tree/develop/packages/pm。正如这个库的readme文件所说,
那么,整个Core 实际上层对 ProseMirror 的更加方便的使用的封装。说是封装其实就是间接导出,并没有做什么实质性的导出,所以,完完全全可以理解为 TipTap的底层就是 ProseMirror,那么为何不直接依赖 ProseMirror呢?这里极有可能是为了后续更好做扩展或者解耦。
整个Tiptap的架构图,我们可以参考如下
基本上,可以理解为 是 ProseMirror的那套把戏。
在 Tiptap 中,插件的各种能力(如快捷键、命令等)是通过扩展(Extension)的 API 实现的。当你将扩展添加到编辑器时,编辑器会自动加载和应用这些 API。以下是一些主要的 API 和它们的原理:
inputRules
或 keymap
属性,可以添加快捷键。inputRules
是一种基于输入模式的快捷键,例如在输入 *
和空格时自动创建一个列表。keymap
是一种基于按键组合的快捷键,例如按 Ctrl+B
时切换加粗样式。当用户输入或按下快捷键时,编辑器会自动调用相应的命令。commands
方法,可以添加命令。命令是一个函数,接受一个参数 params
,并返回一个处理函数。处理函数接受两个参数:state
和 dispatch
。state
是当前的编辑器状态,dispatch
是一个用于分发事务的函数。你可以在处理函数中执行一些操作,如修改文档模型、更新视图和触发事件等。menuItems
属性,可以添加菜单项。菜单项是一个对象,包含一些属性,如 command
、icon
和 title
等。当用户点击菜单项时,编辑器会自动调用相应的命令。plugins
属性,可以添加 ProseMirror 插件。ProseMirror 插件是一个对象,通常包含一个或多个处理函数,如 handleDOMEvents
、appendTransaction
和 filterTransaction
等。这些处理函数用于处理编辑器的事件和事务。以下是用户操作时,扩展Extension于编辑器Editor的交互序列图,当然隐藏了诸多细节,但是不妨碍我们理解一个扩展在整个编辑过程中扮演的角色。
在Tiptap上实现一个扩展最简单的办法莫过于基于他的模板了:npm init tiptap-extension@latest
为了极大的降低理解难度,我选择直接使用加粗扩展来了解下一个扩展的主要逻辑。
import {
Mark,
markInputRule,
markPasteRule,
mergeAttributes,
} from '@tiptap/core'
export interface BoldOptions {
HTMLAttributes: Record<string, any>,
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
bold: {
/**
* Set a bold mark
*/
setBold: () => ReturnType,
/**
* Toggle a bold mark
*/
toggleBold: () => ReturnType,
/**
* Unset a bold mark
*/
unsetBold: () => ReturnType,
}
}
}
export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))$/
export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^*]+))(?:\*\*))/g
export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/
export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/g
export const Bold = Mark.create<BoldOptions>({
name: 'bold',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [
{
tag: 'strong',
},
{
tag: 'b',
getAttrs: node => (node as HTMLElement).style.fontWeight !== 'normal' && null,
},
{
style: 'font-weight',
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
},
]
},
renderHTML({ HTMLAttributes }) {
return ['strong', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addCommands() {
return {
setBold: () => ({ commands }) => {
return commands.setMark(this.name)
},
toggleBold: () => ({ commands }) => {
return commands.toggleMark(this.name)
},
unsetBold: () => ({ commands }) => {
return commands.unsetMark(this.name)
},
}
},
addKeyboardShortcuts() {
return {
'Mod-b': () => this.editor.commands.toggleBold(),
'Mod-B': () => this.editor.commands.toggleBold(),
}
},
addInputRules() {
return [
markInputRule({
find: starInputRegex,
type: this.type,
}),
markInputRule({
find: underscoreInputRegex,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: starPasteRegex,
type: this.type,
}),
markPasteRule({
find: underscorePasteRegex,
type: this.type,
}),
]
},
})
他的代码不多,也就100行左右,也就实现了加粗的功能,我们可以看到,这里面,定义了一些正则,还定义了一些命令,以及快捷键。
可以看到主要的逻辑是,当触发快捷键,时,会给选择的文本增加 ** **,再次触发,会去掉选中。实际上渲染的样式是会表现为 html结构插入到dom中,而renderHtml 就是干这个事情的,但是,加粗的方式不一,所以,多种形式都可以被解析为是 文本加粗。
想必,大家都基本上体验过Notion那种Ai赋能的写作之爽了吧,总之开始用的时候是惊艳到我了,那么,像NotionAI那种输入 / ,就呼出菜单的扩展,该如何实现呢?实际上,这种就就需要用到addProseMirrorPlugins的方式。
const SlashExtensions = Extension.create({
name: 'slash-command',
addOptions() {
return {
suggestion
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion
})
];
}
});
然后具体的逻辑无非就是通过editor读取上下文,然后调用 ChatGPT 接口,拿到响应,在通过editor的接口写到到文档即可。当然我有实现一个成品的NotionAi 的平替,在utools上搜索 aition即可体验。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。