介绍
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)
- {tag:'h1'} 指定h1标签
- {tag:'div.heading1'} div带有heading1的类
- {style: "heading2"} css样式含有heading2
- {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的获取和设置
- 直接指定 {tag: "h1", attrs: {level: 1}
- 通过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
将heading的defining设置为true
会发现复制heading黏贴到paragraph上,能保留heading的格式
具体关于definingAsContext/definingForContent的区别, 后续再补充上来
dragable
节点是否支持拖拽移动位置
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();
效果如下
代码
总结
schema的nodes和marks,需要大家去理解他们的作用,特别是需要自定义实现block的
文末用到的一些schema.node addMark等语法,大家不用太着急,只需要大概知道是这么用的就行,后面会有章节单独去讲