【文档编辑器专栏】2-ProseMirror Schema 数据结构

474 阅读2分钟

介绍

schema是prosemirror-model的一个对象

schema描述了文档中可能出现的节点类型,以及它们的嵌套方式。

例如,它可以说明顶层节点可以包含一个或多个区块,段落节点可以包含任意数量的内联节点,并对其应用任意标记。

interface SchemaSpec<Nodes extends string = any, Marks extends string = any> {
    nodes: {
        [name in Nodes]: NodeSpec;
    } | OrderedMap<NodeSpec>;
    marks?: {
        [name in Marks]: MarkSpec;
    } | OrderedMap<MarkSpec>;
    topNode?: string;
}

schema的定义,包含三个对象

  • nodes 节点结构
  • marks 节点标记
  • topNode 顶层节点

nodes 节点结构

描述该节点的解析规则/渲染成dom的规则

group

描述节点属于哪个组,一般是 block或inline, 和css的描述差不多

  • group: 'block' 块级节点
  • group: 'inline' 行内节点
  • 也可以自定义一个group, 例如 group: 'tile'
paragraph: {
   content: 'tile*'
},

block_tile: {
   group: 'tile'
}

content

官方解释 prosemirror.net/docs/guide/…

可以理解为定义了该节点的child nodes的类型

比如,你想定义该节点

  • 只含有一个段落 content:'paragraph'
  • 含有一个或多个段落 content:'paragraph+'
  • 含有零个或多个段落 content:'paragraph*'

同时支持定义复杂子节点行为

  • heading节点后带有一个或多个段落 content:'heading paragraph+'
  • 一个或多个 (段落或引用) "(paragraph | blockquote)+"

一般情况下,看到的都是(block|inline是指这个节点归属在那个group, 具体看group的定义)

  • 一个或多个block节点 content:'block+'
  • 零个或多个inline节点 content: "inline*"

toDOM

结合node.attrs的属性或其它信息,去定义需要渲染成怎样的dom结构

return的语法 [dom: DOMNode, contentDOM?: HTMLElement]

  • [tag, 0] 0 的意思为这里是一个插槽slot, children的内容插入到这里
  • ["pre", ["code", 0]] 支持嵌套
// 
heading: {
    toDOM(node) {
        const tag = `h${node.attrs.level}`;
       
        return [tag, 0]; // 渲染出 <h1 />等
    }
},

image: {
    toDOM(node) { 
        let {src, alt, title} = node.attrs; 
        return ["img", {src, alt, title}];  // 渲染出 <img src='', alt='', title='' />
    }
},

// 支持嵌套标签
code: {
    toDOM(node) { 
        ["pre", ["code", 0]] // 渲染出 <pre><code></code></pre>
    }
}

parseDOM

定义了怎样解析dom的规则,例如黏贴到文档内,粘贴板内有html格式的数据(复制了一个h1)

<h1>this is heading</h1>

我们通过parseDOM的规则去识别它

  • parseDOM是数组,可以定义多个解析规则(tag, style)
  1. {tag:'h1'} 指定h1标签
  2. {tag:'div.heading1'} div带有heading1的类
  3. {style: "heading2"} css样式含有heading2
  4. {tag: 'div[data-xxxe="xxx"]'} div带有属性

解析的规则比较丰富,用的其实是css selector的规则

更多规则可查阅prosemirror.net/docs/ref/#m…

heading: {
    attrs: {level: {default: 1}},
    content: "inline*",
    group: "block",
    defining: true,
    parseDOM: [{tag: "h1", attrs: {level: 1}},
               {tag: "div.heading1", attrs: {level: 1}},
               {style: "heading2", 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] }
  } as NodeSpec,
  • attrs的获取和设置
  1. 直接指定 {tag: "h1", attrs: {level: 1}
  2. 通过getAttrs
 image: {
     parseDOM: [
         {
             tag: "img[src]", 
             getAttrs(dom: HTMLElement) {
              return {
                src: dom.getAttribute("src"),
                title: dom.getAttribute("title"),
                alt: dom.getAttribute("alt")
              }
        }
        }]
}

attrs

代表定义当前节点有哪些属性,通过node.attrs来访问,在需要parseDOM等方式来设置

code: {
    attrs: {
        type: { default: 'javascript'},
        theme: { default: 'dark'},
        // 其它
    }
}

defining

官方解释 When enabled, enables both definingAsContext and definingForContent.

  • definingAsContext决定在进行替换操作(如粘贴)时,是否将此节点视为重要的父节点。
    • 默认,节点会在其全部内容被替换时被丢弃
    • 定义为true, 节点则会持续存在并包裹插入的内容
  • definingForContent 在插入的内容中,尽可能保留内容的定义父级。
    • 尽可能保留。通常情况下,非默认段落
    • 文本块类型,可能还有列表项,都会被标记为定义项

将heading的defining设置为false 1721277033501.gif

将heading的defining设置为true

1721277130295.gif

会发现复制heading黏贴到paragraph上,能保留heading的格式

具体关于definingAsContext/definingForContent的区别, 后续再补充上来

dragable

节点是否支持拖拽移动位置

1721277698657.gif

atom

可以设为 true,表示虽然这不是一个leaf 节点,但它没有可直接编辑的内容,在视图中应被视为一个单独的单元。(prosemirror.net/docs/ref/#m…)

selectable

控制是否可以将此类型的节点作为Node Selection。

文本的selectable为false,选中时是TextSelection

其它节点默认都为 true,选中时是NodeSelection

code

可用于表示该节点包含代码

marks 标记

给node节点添加标记,例如加粗/下划线/斜体等,这些都是属于marks

attrs / group / toDOM / parseDOM

类似nodes

inclusive

当光标位于其末端时,该标记是否处于激活状态。 默认为 true。

 strong: {
    parseDOM: [
      {tag: "strong"},
      {tag: "b", getAttrs: (node: HTMLElement) => node.style.fontWeight != "normal" && null},
      {style: "font-weight=400", clearMark: m => m.type.name == "strong"},
      {style: "font-weight", getAttrs: (value: string) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null},
    ],
    toDOM() { return ["strong", 0] }
}

上述例子,遇到strong/b/font-weight=400/font-weight的标签,解析["strong", 0] strong标签

实战使用

  • 插入heading标题
  • 加粗
import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
import { schema } from "prosemirror-schema-basic";
import { DOMParser } from "prosemirror-model";

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

const id = 'prosemirror-editor';

// 初始化一个p段落
const content = new window.DOMParser().parseFromString(
  `<p>this is paragraph</p>
   <h1>this is h1</h1>
   <h2>this is h2</h2>`,
  "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
  });

  const headingButton = document.querySelector('#headingButton');
  headingButton?.addEventListener('click', () => {
    const { state, dispatch } = editorView;
    const { schema, tr, selection } = state;
    // 创建一个text节点
    const textNode = schema.text('this is h3');
    // 创建heading节点,level为3(h3)
    const node = schema.nodes.heading.create({ level: 3 }, textNode);
    // 光标位置
    const pos = selection.from;
    // 插入
    tr.insert(pos, node);
    // 广播事件才会成功
    dispatch(tr);
  });

  const strongButton = document.querySelector('#strongButton');
  strongButton?.addEventListener('click', () => {
    const { state, dispatch } = editorView;
    const { schema, tr, selection } = state;
    const { from, to } = selection;
    // 将选区内容,添加上strong的标注
    tr.addMark(from, to, schema.marks.strong.create());
    dispatch(tr);
  });
}

document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
  <div>
    <div>
      <button id="headingButton">插入h3</button>
      <button id="strongButton">加粗</button>
    </div>
    <div id="${id}"></div>
  </div>
  `;

mount();

效果如下

1721284912027.gif

代码

github.com/pm-editor/d…

总结

schema的nodes和marks,需要大家去理解他们的作用,特别是需要自定义实现block的

文末用到的一些schema.node addMark等语法,大家不用太着急,只需要大概知道是这么用的就行,后面会有章节单独去讲