前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何实现所见即所得编辑器?tiptap的实现原理(二)

如何实现所见即所得编辑器?tiptap的实现原理(二)

原创
作者头像
brzhang
修改2023-11-14 20:29:38
2.2K0
修改2023-11-14 20:29:38
举报
文章被收录于专栏:玩转全栈玩转全栈

Tiptap 是一个基于 ProseMirror 构建的富文本编辑器,它是一个灵活、可扩展的富文本编辑器,同时适用于 Vue.js 和 React。所以,无论你的技术栈是Vue,还是React,使用Tiptap都不用太过于在选型上纠结。Tiptap 的核心思路是通过插件系统提供丰富的功能,使得开发者可以根据需求定制编辑器的功能和样式

Tiptap 的主要有5大部分组成:

  1. Core:Tiptap 的核心模块,负责处理编辑器的基本功能,如文本输入、选择、撤销和重做等。
  2. Extensions:扩展模块,提供丰富的编辑功能,如加粗、斜体、列表、链接等。开发者可以根据需求选择需要的功能,并通过插件系统轻松地添加到编辑器中,下面我们会展开说说如何自定义一个插件,例如如何将AI能力加持到编辑器上来
  3. Commands:命令模块,用于执行编辑操作,如插入、删除、修改等。开发者可以通过命令 API 对编辑器进行操作,实现自定义的功能。
  4. Schema:定义编辑器的文档结构,包括节点、标记和规则。通过自定义 Schema,可以实现特定的文档结构和约束。
  5. Vue/React components:Tiptap 提供了 Vue 和 React 的组件,使得编辑器可以轻松地集成到这两个框架中。

首先,为了了解这些模块之间的关联关系,我们可以看看下面这系统架构幅图。

Tiptap 作为主要的入口,连接了 Core、Extensions、Commands、Schema 和 Vue/React components。Extensions 又包括了多个功能模块,如 Bold、Italic、List 和 Link。这样的架构使得 Tiptap 可以根据需求灵活地扩展功能和样式。

Tiptap 的 Core模块原理简介

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的架构图,我们可以参考如下

  1. Document Model:ProseMirror 提供了一个灵活的文档模型,用于表示富文本编辑器中的内容。文档模型由节点(Node)和标记(Mark)组成,节点表示文档的结构元素,如段落、标题和列表等;标记表示文本的样式,如加粗、斜体和链接等。Tiptap 的 Core 模块使用 ProseMirror 的文档模型来表示和操作编辑器中的内容。
  2. Transactions:ProseMirror 中的所有编辑操作都是通过事务(Transaction)来完成的。事务是一系列对文档模型的修改操作,如插入、删除和修改等。Tiptap 的 Core 模块使用 ProseMirror 的事务系统来处理编辑操作,确保文档模型的一致性和可撤销性。
  3. View:ProseMirror 提供了一个视图系统,用于将文档模型渲染到 DOM 中,并处理用户的输入和交互。Tiptap 的 Core 模块使用 ProseMirror 的视图系统来实现编辑器的显示和交互功能。
  4. Plugins:ProseMirror 支持插件系统,允许开发者为编辑器添加自定义的功能和行为。Tiptap 的 Core 模块使用 ProseMirror 的插件系统来实现扩展功能,如撤销和重做、拖放和粘贴等。

基本上,可以理解为 是 ProseMirror的那套把戏。

我们如何在TipTap 上去实现一个扩展(Extension),以及扩展的实现原理

在 Tiptap 中,插件的各种能力(如快捷键、命令等)是通过扩展(Extension)的 API 实现的。当你将扩展添加到编辑器时,编辑器会自动加载和应用这些 API。以下是一些主要的 API 和它们的原理:

  1. 快捷键:在扩展中定义 inputRuleskeymap 属性,可以添加快捷键。inputRules 是一种基于输入模式的快捷键,例如在输入 * 和空格时自动创建一个列表。keymap 是一种基于按键组合的快捷键,例如按 Ctrl+B 时切换加粗样式。当用户输入或按下快捷键时,编辑器会自动调用相应的命令。
  2. 命令:在扩展中定义 commands 方法,可以添加命令。命令是一个函数,接受一个参数 params,并返回一个处理函数。处理函数接受两个参数:statedispatchstate 是当前的编辑器状态,dispatch 是一个用于分发事务的函数。你可以在处理函数中执行一些操作,如修改文档模型、更新视图和触发事件等。
  3. 菜单项:在扩展中定义 menuItems 属性,可以添加菜单项。菜单项是一个对象,包含一些属性,如 commandicontitle 等。当用户点击菜单项时,编辑器会自动调用相应的命令。
  4. 插件:在扩展中定义 plugins 属性,可以添加 ProseMirror 插件。ProseMirror 插件是一个对象,通常包含一个或多个处理函数,如 handleDOMEventsappendTransactionfilterTransaction 等。这些处理函数用于处理编辑器的事件和事务。

以下是用户操作时,扩展Extension于编辑器Editor的交互序列图,当然隐藏了诸多细节,但是不妨碍我们理解一个扩展在整个编辑过程中扮演的角色。

一个简单的扩展的实现

在Tiptap上实现一个扩展最简单的办法莫过于基于他的模板了:npm init tiptap-extension@latest

为了极大的降低理解难度,我选择直接使用加粗扩展来了解下一个扩展的主要逻辑。

代码语言:typescript
复制
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 就是干这个事情的,但是,加粗的方式不一,所以,多种形式都可以被解析为是 文本加粗。

那么,如何实现一个类似于NotionAI的方式的扩展插件呢?

想必,大家都基本上体验过Notion那种Ai赋能的写作之爽了吧,总之开始用的时候是惊艳到我了,那么,像NotionAI那种输入 / ,就呼出菜单的扩展,该如何实现呢?实际上,这种就就需要用到addProseMirrorPlugins的方式。

代码语言:javascript
复制
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即可体验。

示例
示例
示例
示例

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Tiptap 的 Core模块原理简介
  • 我们如何在TipTap 上去实现一个扩展(Extension),以及扩展的实现原理
  • 一个简单的扩展的实现
    • 那么,如何实现一个类似于NotionAI的方式的扩展插件呢?
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档