prosemirror-tables 源码解读

2023-02-22  本文已影响0人  翔子丶

为什么写这篇文章

公司使用tiptap富文本编辑器,在tiptap的官网有这么一段话Tiptap is a headless wrapper around [ProseMirror](https://prosemirror.net/),这里的headless wrapper意思是“无头编辑器”,指的是不提供任何UI样式,完全自由的定制任何想要的UI,特别适合二次开发。

tiptap是对prosemirror的封装,在prosemirror的基础上提供了更友好的API、模块封装以及将MVVM的接入封装在框架内部,适用于各种流行框架,使开发者更容易上手。

tiptap提供大量官方扩展,像本文介绍的prosemirror-tabls,但官方的毕竟是官方,一些样式或基本功能的改动,就必须要通过修改源码的方式实现。

名次解释

PS:理解完概念再往下看,不然容易一脸懵

document

用于表示ProseMirror的整个文档,使用editor.view.state.doc引用,ProseMirror定义自己的数据结构来存储document内容,通过输出可以看到document是一个Node类型,包含content元素,是一个fragment对象,而每个fragment又包含 0 个或多个字节点,组成了document解构,类似于DOM

doc-node.jpg

Schema

用于定义文档的结构和内容。它定义了一组节点类型和它们的属性,例如段落、标题、链接、图片等等。Schema 是编辑器的模型层,可以通过其 API 创建、操作和验证文档中的节点。每个document都有一个与之相关的schema,用于描述存在于此document中的nodes类型

Node

文档中的节点,节点是 Schema 中定义的类型之一,整个文档就是一个Node实例,它的每个子节点,例如一个段落、一个列表项、一张图片也是Node的实例。Node的修改遵循Immutable原则,更新时创建一个新的节点,而不是改变旧的节点,统一使用dispatch去触发更新。

const node = $cell.node(-1);
// 当前节点类型
node.type;
// 节点的attributes
node.attrs;
// 从指定node中获取符合条件的子节点
findChildren(tr.doc, (node) => node.type.name === 'table');

Mark

用于给节点添加样式、属性或其他信息的一种方式。Prosemirror 将行内文本视作扁平结构而非 DOM 类似的树状结构,这样是为了方便计数和操作。例如,一个文本节点可以添加加粗、斜体、下划线等样式,也可以添加标签、链接等属性。Mark 本身没有节点结构,只是对一个节点的文本内容进行修饰。Marks通过Schema创建,用于控制哪些marks存在于哪些节点以及用于哪些attributes

State

Prosemirror 的数据结构对象,相当于是 reactstate,有 viewstateplugin 的局部 state 之分。 如上面的 schema 就定义在其上: state.schemaProseMirror 使用一个单独的大对象来保持对编辑器所有 state 的引用(基本上来说,需要创建一个与当前编辑器相同的编辑器)

prosemirror-state.jpg

Transaction

继承自Transform,不仅能追踪对文档进行修改的一组操作,还能追踪state的其他变化,例如选区更新等。每次更新都会产生一个新的state.transactions(通过state.tr来创建一个transaction实例),描述当前state被应用的变化,这些变化用来应用当前state来创建一个更新之后的state,然后这个新的state被用来更新view

此处的state指的是EditorState,描述编辑器的状态,包含了文档的内容、选区、当前的节点和标记集合等信息。每次编辑器发生改变时,都会生成一个新的 EditorState

View

ProseMirror编辑器的视图层,负责渲染文档内容和处理用户的输入事件。View 接受来自 EditorState 的更新并将其渲染到屏幕上。同时,它也负责处理来自用户的输入事件,如键盘输入、鼠标点击等。其中state就是其上的一个属性:view.state

新建编辑器第一步就是new一个EditorVIew

Plugin

ProseMirror 中的插件,用于扩展编辑器的功能,例如点击/粘贴/撤销等。每个插件都是一个包含了一组方法的对象,这些方法可以监听编辑器的事件、修改事务、渲染视图等等。每个插件都包含一个key属性,如prosemirror-tables设置keytableColumnResizing,通过这个key就可以访问插件的配置和状态,而无需访问插件实例对象。

const pluginState = columnResizingPluginKey.getState(state);

Commands

表示Command函数集合,每个command函数定义一些触发事件来执行各种操作。

Decorations

表示节点的外观和行为的对象。它可以用于添加样式、标记、工具提示等效果,以及处理点击、悬停、拖拽等事件。Decoration 通常是在渲染视图时应用到节点上的,但也可以在其他情况下使用,如在协同编辑时标记其他用户的光标位置。

用于绘制document view,通过decorations属性的返回值来创建,包含三种类型

prosemirror 为了快速绘制这些类型,通过 decorationSet.create 静态方法来创建

import { Plugin, PluginKey } from 'prosemirror-state';
let purplePlugin = new Plugin({
  props: {
    decorations(state) {
      return DecorationSet.create(state.doc, [
        Decoration.inline(0, state.doc.content.size, {
          style: 'color: purple',
        }),
      ]);
    },
  },
});

ResolvedPos

Prosemirror中通过Node.resolve解析位置信息返回的对象,包含了一些位置相关的信息。它会告诉我们当前position的父级node是什么,它在父级node中的偏移量(parentOffset)是多少以及其他信息。

const $cell = doc.resolve(cell);
// 从根节点开始,父级点的深度,如果直接指向根节点则为0,如果指定一个顶级节点,则为1
$cell.deth;
// 该位置相对于父节点的偏移量
$cell.parentOffset;
// 相当于$cell.parent() 获取父级节点,$cell.node(-2)获取父级的父级,以此类推
$cell.node(-1);
// 获取父节点的开始位置,相对于doc根节点的位置,一般用来定位
$cell.start(-1);

Selection

表示当前选中内容,prosemirror中默认定义两种类型的选区对象:

也可以通过继承Selection父类来实现自定义的选区类型,如CellSelection

// 获取当前选区
const sel = state.selection;
// 使用TextSelection创建文本选区
const selection = new TextSelection($textAnchor, $textHead);
// 使用NodeSelection创建节点选区
const selection = new NodeSelection($pos);
// 使用AllSelection创建覆盖整个文档的选区 可以作为cmd + a的操作
const selection = new AllSelection(doc);
// 用new之后的选区,更新当前 transaction 的选区
state.tr.setSelection(selection);
// 从指定选区获取符合条件的父节点
findParentNode(
  (node) =>
    node.type.spec.tableRole && node.type.spec.tableRole.includes('cell'),
)(selection);

Slice

源码目录

├── README.md
├── cellselection.ts
├── columnresizing.ts
├── commands.ts
├── copypaste.ts
├── fixtables.ts
├── index.html
├── index.ts
├── input.ts
├── schema.ts
├── tablemap.ts
├── tableview.ts
└── util.ts

cellselection.ts

定义CellSelection选区对象,继承自Selection

columnresizing.ts

定义columnResizing插件,用于实现列拖拽功能,大致思路如下:

commands.ts

定义操作表格的一系列方法

copypaste.ts

用于处理将单元格内容粘贴到表格中、或将任何内容粘贴到单元格选择中,如用选择内容替换单元格块。

当在单元格中cmd + v触发粘贴时,步骤为:

  1. 调用input.ts中的handlePaste方法,根据传入的文档片段去做相应处理

  2. 调用pastedCells,从文档片段中获取单元格的矩形区域,如果文档片段的外部节点不是表格单元格或行,则返回null,如果是的话会根据当前slice传入ensureRectangular去生成新的一组单元格

    // 判断是否为单元格或行,主要通过schema中定义的tableRole来判断
    // 行
    first.type.spec.tableRole === 'row';
    // 单元格
    first.type.spec.tableRole === 'cell';
    first.type.spec.tableRole === 'header_cell';
    
  3. 判断当前选区是否为CellSelection,即是否选中一个或多个单元格的情况,会调用clipCells方法根据生成的cells生成表格新的一组单元格,通过insertCells插入原表格指定位置

    • insertCell:将给定的一组单元格(由 pastedCells 返回)插入表格中 rect 指向的位置
    • growTable:isolateHorizontalisolateVertical主要是为了确保被插入的表格足够大,足够容得下插入的单元格
  4. 如果当前选区不是CellSelection,但是pastedCells生成了新的cells,即复制的是表格单元格,则同样使用insertCells插入

  5. 不满足上面两个条件时,返回false,即不用处理,按浏览器默认行为处理

fixtables.ts

定义了tiptap中的fixTables命令,用于检查文档中的所有表格并在必要时修复。通过代码可以看到fixTables就是遍历state.doc的所有子节点,如果是table的话就调用fixTable。而fixTable修复表格主要是根据表格是否存在TableMap.get(table).problems来做处理,problems包含四种类型

因为目前我没遇到过这些错误,所以对这些名词的理解还不是很清晰。

index.ts

定义插件tableEditing,用于处理单元格选择的绘制、以及创建和使用此类选择的基本用户交互。这个插件需要放在所有插件数组的末尾,因为它处理表格中的鼠标事件相当广泛。而其他插件,比如列宽拖动columnResizing插件,需要首先执行更具体的行为。
插件的props上定义了以下事件处理函数,这些事件处理函数如果返回true,说明它们处理了相应的事件,如果返回false则还是触发浏览器对应的事件

input.ts

定义了一些功能函数,用于链接用户输入与table相关功能

schema.ts

tablemap.ts

定义 TableMap 类,可以参考prosemirror-tables关于class TableMap的说明,或中文翻译。这里为了性能考虑,做了缓存处理。如果缓存中不存在对应表格的tableMap时,会通过computeMap重新获取tableMap,并放入缓存中。

tableview.ts

参考

util.ts

定义一些用于处理表格的各种辅助函数

上一篇下一篇

猜你喜欢

热点阅读