【文档编辑器专栏】1-从ProseMirror的第一个案例入门

1,157 阅读5分钟

介绍

ProseMirror 提供了一套用于构建富文本编辑器的工具和概念,使用受“所见即所得”启发的用户界面,但试图避免这种编辑风格的缺陷。

ProseMirror 的主要原则是您的代码可以完全控制文档及其内容。此文档不是 HTML 块,而是一个自定义数据结构,它仅包含您明确允许其包含的元素,并且与您指定的关系有关。所有更新都经过一个点,您可以在此检查它们并对其做出反应。

核心库不是简单的嵌入式组件——我们优先考虑模块化和可定制性,而不是简单性,希望将来人们能够基于 ProseMirror 分发嵌入式编辑器。因此,这更像是乐高积木,而不是火柴盒汽车。

有四个基本模块,是进行任何编辑所必需的,还有许多由核心团队维护的扩展模块,它们的状态与第三方模块相似 - 它们提供了有用的功能,但您可以省略它们或将它们替换为实现类似功能的其他模块。

基本模块包括:

此外,

第一个demo

上面的名词和概念有点多, 我们从MVC模式来先讲解下

prosemirror mvc

传统MVC

image.png

prosemirror mvc

image.png 从设计上,设计理念是一样的

  • prosemirror-model定义编辑器的文档模型,用于描述编辑器内容的数据结构。
  • prosemirror-state提供描述编辑器整个状态(包括选择)的数据结构以及从一个状态移动到下一个状态的事务系统。
  • prosemirror-view实现一个用户界面组件,将给定的编辑器状态显示为浏览器中的可编辑元素,并处理用户与该元素的交互。
  • prosemirror-transform包含以可记录和重放的方式修改文档的功能,这是模块中交易的基础state,并使撤消历史记录和协作编辑成为可能。

初始化代码框架

npm create vite
// 建议选择vanilla和typescript
npm install

// 安装prosemirror package
npm i prosemirror-model prosemirror-state prosemirror-view
// 安装一个basic schema
npm i prosemirror-schema-basic 

npm run dev

编写第一个案例

import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
import { schema } from "prosemirror-schema-basic";

import './style.css';

const id = 'prosemirror-editor';

export const mount = () => {
  const el = document.querySelector(`#${id}`);

  // 1. 提供schema (文档结构, 这里暂时用现成的)
  // 2. 创建一个editor state数据实例
  const editorState = EditorState.create({
    schema
  });

  // 3. 创建editor view编辑器视图实例
  const editorView = new EditorView(el, {
    state: editorState
  });
}

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `<div id="${id}"></div>`

mount();

看element元素渲染成这样,就意味着第一个案例成功跑起来了

image.png

model

由上面的流程,可以看出,schema是一切的开始

那schema是什么东西呢?

schema定义了编辑器的元素结构, 包含nodes和marks

你能指定编辑器可以有哪些节点,节点元素的具体表现,复制黏贴时候怎样解析成对应的节点

上文使用schema-basic, 不方便理解,我们编写一个自己的schema

import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
// import { schema } from "prosemirror-schema-basic";
import { Schema, DOMParser } from "prosemirror-model";

import './style.css';
import './prosemirror.css';

const id = 'prosemirror-editor';

const schema = new Schema({
  nodes: {
    /// 定义最顶层doc节点
    doc: {
      content: "block+" // 子节点为多个block
    },
    /// 定义最底层text节点
    text: {
      group: "inline"
    },
    /// 定义段落节点
    paragraph: {
      content: "inline*",
      group: "block",
      parseDOM: [{ tag: "p" }],
      toDOM() { return ['p', 0] }
    },
    /// heading节点
    heading: {
      attrs: {level: {default: 1}},
      content: "inline*",
      group: "block",
      defining: true,
      parseDOM: [{tag: "h1", attrs: {level: 1}},
                 {tag: "h2", attrs: {level: 2}},
                 {tag: "h3", attrs: {level: 3}},
                 {tag: "h4", attrs: {level: 4}},
                 {tag: "h5", attrs: {level: 5}},
                 {tag: "h6", attrs: {level: 6}}],
      toDOM(node) { return ["h" + node.attrs.level, 0] }
    }
  },
  marks: {}
});

// 初始化一个p段落
const content = new window.DOMParser().parseFromString(
  `<p>this is paragraph</p><h1>this is h1</h1>`,
  "text/html"
).body;

export const mount = () => {
  const el = document.querySelector(`#${id}`);

  // 1. 提供schema (文档结构, 这里暂时用现成的)
  // 2. 创建一个editor state数据实例
  const editorState = EditorState.create({
    // schema
    doc: DOMParser.fromSchema(schema).parse(content)
  });

  // 3. 创建editor view编辑器视图实例
  const editorView = new EditorView(el, {
    state: editorState
  });
}

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `<div id="${id}"></div>`;

mount();

image.png

如果我们不定义schema nodes里面的heading(删除schema nodes heading), 会发现h1表现没能正确渲染

image.png

所以,在这里主要表述一个概念。ProseMirror会根据你给它的元素标签内容来渲染对应的节点

heading: {
      attrs: {level: {default: 1}}, // 代表定义当前节点有哪些属性
      content: "inline*", // 该节点可以是0或多个inline的节点
      group: "block", // 属于group分组
      defining: true, // 复制黏贴后,是否保持原节点
      // 黏贴到编辑器内时,匹配tag h1/2/3/4/5/6的标签,设置上attrs.level的属性
      parseDOM: [{tag: "h1", attrs: {level: 1}}, 
                 {tag: "h2", attrs: {level: 2}},
                 {tag: "h3", attrs: {level: 3}},
                 {tag: "h4", attrs: {level: 4}},
                 {tag: "h5", attrs: {level: 5}},
                 {tag: "h6", attrs: {level: 6}}],
      // 转换为html标签时,生成h1/2/3/4/5/6/的标签
      toDOM(node) { return ["h" + node.attrs.level, 0] }
    }

可以将{tag: "h1", attrs: {level: 1} 改为 {tag: "h1", attrs: {level: 7}

你会发现,元素渲染成了 h7

image.png

所以

parseDOM 定义了解析数据源dom的行为。

heading节点将h1解析后,根据parseDOM上的设定{tag: "h1", attrs: {level: 7},设置attrs上的level值为7。

最后通过toDOM定义了最终要渲染出的dom结构, h + attrs.level,即 h7

可以理解

schema的意义是,通过json配置化的形式,让开发者自己定义解析和渲染的行为

至于里面的字段,下一章会详细讲schema

state

State 是Prosemirror 的数据结构对象。包含了schema, selection, tr等一些对象

image.png

  • doc 是整个文档的doc tree结构 image.png

  • selection 选区系统

image.png

  • plugins 插件相关 目前我们还没有定义插件,所以这里没有数据

image.png

  • tr transaction 操作类相关

image.png

view

view就是视图对象,可以访问到顶层dom对象,也有state等

image.png

在原型上还挂了一些view操作方法

image.png

总结

至此,你已经成功跑起了第一个案例。

希望本文能让你对prosemirror的model/state/view有初步的理解

但我相信你一定有很多疑问,带着疑问,可以持续关注后续专栏文章。

demo代码

github.com/pm-editor/d…

文档