【文档编辑器专栏】3-ProseMirror 文档 ’块‘ 结构

272 阅读2分钟

背景

我们学习了schema的nodes和marks, 知道大概nodes和marks的作用。

node节点里面有个group的属性,定义了该节点为block还是inline。

在渲染层面,其实是定义了该节点是块节点还是inline节点

当然还有个重要的content属性,定义了其children的构成

最基本的nodes数据结构需要包含doc / text / 一个block的节点

‘块’ 是什么

nodes: {
  doc: {
    content: "block+"
  } as NodeSpec,
  
  paragraph: {
    content: "inline*",
    group: "block",
    parseDOM: [{tag: "p"}],
    toDOM() { return pDOM }
  } as NodeSpec,
  
  text: {
    group: "inline"
  } as NodeSpec,
}

image.png

通过这个结构,可以看到

  • doc为最顶的块级元素,包含了多个block(paragraph节点), 这里面和doc的content: 'block+' 表现一致
  • 段落内包含了一个text节点,当然也可以包含多个inline的节点

这样的块级结构,相信大家是很容易理解的,因为和html的结构理解是一样的。

  • doc是块
  • paragraph是块
  • text也是块

一砖一瓦都是块,拼凑出一个html页面

平常我们是直接去写html, prosemirror上则需要通过nodes去描述这个结构,再通过复制黏贴或者代码操作等的方式去得到dom

‘块’ 的自定义

如果我们想实现类似如下的html结构,需要怎样去定义呢?

  • 一个div包裹p
  • 一个div包裹h1
<div>
    <p>this is p</p>
</div>
<div>
    <h1>this is h1</h1>
</div>

从prosemirror的角度来看,上面为2个块。我们先定义出div的这个节点

block_tile: {
    content: 'block+',
    group: 'block',
    inline: false,
    toDOM: () => {
      return ['div', { class: 'block_tile' }, 0]
    },
    parseDOM: [{ tag: 'div.block_tile' }]
}

可以打印看下这个nodeType的属性

再和node block_tile的属性配置关联发现,其实是根据schema node 的配置生成的产物: nodeType

image.png

通过prosemirror构造dom

const { state, dispatch } = editorView;
const { schema, tr, selection } = state;
const textNode = schema.text('this is block_tile p');
const pNode = schema.nodes.paragraph.create({}, textNode);
const bNode = schema.nodes.block_tile.create({}, pNode);
const pos = selection.from;
tr.insert(pos, bNode);

const textNode1 = schema.text('this is block_tile h1');
const h1Node = schema.nodes.heading.create({ level: 1 }, textNode1);
const bNode1 = schema.nodes.block_tile.create({}, h1Node);
tr.insert(pos + bNode.nodeSize, bNode1);

dispatch(tr);

image.png

代码

import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state";
import { nodes, marks } from "prosemirror-schema-basic";
import { DOMParser, Schema } from "prosemirror-model";
import { keymap } from 'prosemirror-keymap'
import { baseKeymap } from 'prosemirror-commands'
import { history, undo, redo } from 'prosemirror-history';

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

const id = 'prosemirror-editor';

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

const customNodes = {
  block_tile: {
    content: 'block+',
    group: 'block',
    inline: false,
    defining: true,
    toDOM: () => {
      return ['div', { class: 'block_tile' }, 0]
    },
    parseDOM: [{ tag: 'div.block_tile' }]
  }
};

const schema = new Schema({
  nodes: Object.assign({}, nodes, customNodes),
  marks
});

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

  // 1. 提供schema (文档结构, 这里暂时用现成的)
  // 2. 创建一个editor state数据实例
  const editorState = EditorState.create({
    // schema
    doc: DOMParser.fromSchema(schema).parse(content),
    plugins: [
      keymap(baseKeymap),
      history(),
      keymap({ "Mod-z": undo, "Mod-y": redo }),
    ]
  });

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

  window.editorView = editorView;

  const blockTileButton = document.querySelector('#blockTileButton');
  blockTileButton?.addEventListener('click', () => {
    const { state, dispatch } = editorView;
    const { schema, tr, selection } = state;
    const textNode = schema.text('this is block_tile p');
    const pNode = schema.nodes.paragraph.create({}, textNode);
    const bNode = schema.nodes.block_tile.create({}, pNode);
    const pos = selection.from;
    tr.insert(pos, bNode);

    const textNode1 = schema.text('this is block_tile h1');
    const h1Node = schema.nodes.heading.create({ level: 1 }, textNode1);
    const bNode1 = schema.nodes.block_tile.create({}, h1Node);
    tr.insert(pos + bNode.nodeSize + 1, bNode1);

    dispatch(tr);
  });

  const headingButton = document.querySelector('#headingButton');
  const strongButton = document.querySelector('#strongButton');

  headingButton?.addEventListener('click', () => {
    const { state, dispatch } = editorView;
    const { schema, tr, selection } = state;
    const textNode = schema.text('this is h3');
    const node = schema.nodes.heading.create({ level: 3 }, textNode);
    const pos = selection.from;

    tr.insert(pos, node);
    dispatch(tr);
  });

  strongButton?.addEventListener('click', () => {
    const { state, dispatch } = editorView;
    const { schema, tr, selection } = state;
    const { from, to } = selection;

    tr.addMark(from, to, schema.marks.strong.create());
    dispatch(tr);
  });
}

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

mount();

总结

前面三章,其实都是围绕着schema node来给大家讲述schema数据结构的作用,以及呈现到界面上的块级表现。

大家都动手去操作下,围绕着复制黏贴/schema.nodes.xx.create代码方式去体验下,相信大家很容易能理解