富文本编辑器的第三方库ProseMirror
如果0-1的开发一个富文本编辑器,成本还是非常高的,里面很多坑要踩,市面上很多库可以帮助我们搭建一个富文本编辑器,
ProseMirror
就是其中最流行的库之一。
认识ProseMirror
ProseMirror 提供了一套工具和概念,用于构建富文本编辑器,使用的用户界面受“所见即所得”启发,但试图避免这种编辑风格的陷阱。
ProseMirror 的主要原则是您的代码可以完全控制文档及其发生的事情。这个文档不是一个 HTML 的 blob,而是一个自定义的数据结构,只包含您明确允许它包含的元素,并且在您指定的关系中。所有更新都通过一个单一的点,可以在此检查并对其做出反应。
ProseMirror更像是一个乐高积木套装,而不是一个火柴盒汽车。
ProseMirror有四个基本模块:
prosemirror-model
定义了编辑器的文档模型,用于描述编辑器内容的数据结构。prosemirror-state
提供了描述编辑器整体状态的数据结构,包括选择,以及从一个状态移动到下一个状态的事务系统。prosemirror-view
实现了一个用户界面组件,在浏览器中将给定的编辑器状态显示为可编辑元素,并处理用户与该元素的交互。prosemirror-transform
包含用于修改文档的功能,这些修改可以被记录和重放,这是state模块中事务的基础,并且使撤销历史和协作编辑成为可能。
此外,还有基本编辑命令、绑定键、撤销历史、输入宏、协作编辑、简单文档模式等模块
实现最简约编辑器
官方文档上给了一个最简约版的案例
import {schema} from "prosemirror-schema-basic"
import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"let state = EditorState.create({schema})
let view = new EditorView(document.body, {state})
Schema
其中schema是重中之重,在开发 prosemirror 项目中,第一件最重要的事情是先定义自己的数据模型,即 Schema。
我们先看看官方给的prosemirror-schema-basic
插件中提供了哪些能力
在.d.ts文件中
import { NodeSpec, MarkSpec, Schema } from 'prosemirror-model';/**
[Specs](https://prosemirror.net/docs/ref/#model.NodeSpec) for the nodes defined in this schema.
*/
declare const nodes: {/**NodeSpec The top level document node.*/doc: NodeSpec;/**A plain paragraph textblock. Represented in the DOMas a `<p>` element.*/paragraph: NodeSpec;/**A blockquote (`<blockquote>`) wrapping one or more blocks.*/blockquote: NodeSpec;/**A horizontal rule (`<hr>`).*/horizontal_rule: NodeSpec;/**A heading textblock, with a `level` attribute thatshould hold the number 1 to 6. Parsed and serialized as `<h1>` to`<h6>` elements.*/heading: NodeSpec;/**A code listing. Disallows marks or non-text inlinenodes by default. Represented as a `<pre>` element with a`<code>` element inside of it.*/code_block: NodeSpec;/**The text node.*/text: NodeSpec;/**An inline image (`<img>`) node. Supports `src`,`alt`, and `href` attributes. The latter two default to the emptystring.*/image: NodeSpec;/**A hard line break, represented in the DOM as `<br>`.*/hard_break: NodeSpec;
};
/**
[Specs](https://prosemirror.net/docs/ref/#model.MarkSpec) for the marks in the schema.
*/
declare const marks: {/**A link. Has `href` and `title` attributes. `title`defaults to the empty string. Rendered and parsed as an `<a>`element.*/link: MarkSpec;/**An emphasis mark. Rendered as an `<em>` element. Has parse rulesthat also match `<i>` and `font-style: italic`.*/em: MarkSpec;/**A strong mark. Rendered as `<strong>`, parse rules also match`<b>` and `font-weight: bold`.*/strong: MarkSpec;/**Code font mark. Represented as a `<code>` element.*/code: MarkSpec;
};
/**
This schema roughly corresponds to the document schema used by
[CommonMark](http://commonmark.org/), minus the list elements,
which are defined in the [`prosemirror-schema-list`](https://prosemirror.net/docs/ref/#schema-list)
module.To reuse elements from this schema, extend or read from its
`spec.nodes` and `spec.marks` [properties](https://prosemirror.net/docs/ref/#model.Schema.spec).
*/
declare const schema: Schema<"blockquote" | "image" | "text" | "doc" | "paragraph" | "horizontal_rule" | "heading" | "code_block" | "hard_break", "link" | "code" | "em" | "strong">;export { marks, nodes, schema };
其中两部分内容,nodes
与 marks
,简单理解两者的关系,node
代表文档中的某种节点,主要用来渲染内容,mark
更多为一些附加样式的定义,如上面的文本加粗,以及文本颜色,背景色,是否斜体等,这些 marks 是可以附加在 node 上的.
其中可见官方模块定义了一个简单的 schema。你可以直接拿来使用,或者扩展它,亦或者仅仅是抄其中的一些节点和 mark 的配置对象然后应用到新的 schema 中。
自定义schema
// model.ts 文件命名暂时还是以 mvc 模式命名,方便理解,实际中 命名为 schema.ts 更好
import { Schema } from 'prosemirror-model';export const schema = new Schema({nodes: {// 整个文档doc: {// 文档内容规定必须是 block 类型的节点(block 与 HTML 中的 block 概念差不多) `+` 号代表可以有一个或多个(规则类似正则)content: 'block+'},// 文档段落paragraph: {// 段落内容规定必须是 inline 类型的节点(inline 与 HTML 中 inline 概念差不多), `*` 号代表可以有 0 个或多个(规则类似正则),你也可以使用类似正则表达式的范围,例如{2}(“正好两个”){1, 5}(“一到五个”)或{2,}(“两个或更多”)在节点名称之后。content: 'inline*',// 分组:当前节点所在的分组为 block,意味着它是个 block 节点group: 'block',// 渲染为 html 时候,使用 p 标签渲染,第二个参数 0 念做 “洞”,类似 vue 中 slot 插槽的概念,// 证明它有子节点,以后子节点就填充在 p 标签中toDOM: () => {return ['p', 0]},// 从别处复制过来的富文本,如果包含 p 标签,将 p 标签序列化为当前的 p 节点后进行展示parseDOM: [{tag: 'p'}]},// 段落中的文本text: {// 当前处于 inline 分株,意味着它是个 inline 节点。代表输入的文本group: 'inline'},// 1-6 级标题heading: {// attrs 与 vue/react 组件中 props 的概念类似,代表定义当前节点有哪些属性,这里定义了 level 属性,默认值 1attrs: {level: {default: 1}},// 当前节点内容可以是 0 个或多个 inline 节点content: 'inline*',// 当前节点分组为 block 分组group: 'block',// defining: 特殊属性,为 true 代表如果在当前标签内(以 h1 为例),全选内容,直接粘贴新的内容后,这些内容还会被 h1 标签包裹// 如果为 false, 整个 h1 标签(包括内容与标签本身)将会被替换为其他内容,删除亦如此。// 还有其他的特殊属性,后续细说defining: true,// 转为 html 标签时,根据当前的 level 属性,生成对应的 h1 - h6 标签,节点的内容填充在 h 标签中(“洞”在)。toDOM(node) {const tag = `h${node.attrs.level}`return [tag, 0]},// 从别处复制进来的富文本内容,根据标签序列化为当前 heading 节点,并填充对应的 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}}],}},// 除了上面定义 node 节点,一些富文本样式,可以通过 marks 定义marks: {// 文本加粗strong: {// 对于加粗的部分,使用 strong 标签包裹,加粗的内容位于 strong 标签内(这里定义的 0 与上面一致,也念做 “洞”,也类似 vue 中的 slot)toDOM() {return ['strong', 0]},// 从别的地方复制过来的富文本,如果有 strong 标签,则被解析为一个 strong markparseDOM: [{ tag: 'strong' },],},// 下划线underline: {parseDOM: [{ tag: 'u' },{style: 'text-decoration',getAttrs: value => value === 'underline' && null},{style: 'text-decoration-line',getAttrs: value => value === 'underline' && null},],toDOM: () => ['span', { style: 'text-decoration: underline;' }, 0],}}
})
实现文本加粗、下划线
import { toggleMark } from 'prosemirror-commands'
...
/**
editorView就是 new EditorView 的实例
*/
// 加粗
toggleMark(editorView.state.schema.marks.strong)(editorView.state, editorView.dispatch)
...
// 下划线
toggleMark(editorView.state.schema.marks.underline)(editorView.state, editorView.dispatch)
prosemirror 的插件系统Plugins
插件用于以各种方式扩展编辑器和编辑器状态的行为。有些相对简单,比如将键映射插件将操作绑定到键盘输入。其他的则更复杂,比如历史插件通过观察事务并存储它们的逆操作来实现撤销历史,以防用户想要撤销它们。
让我们将这两个插件添加到我们的编辑器中以获得撤销/重做功能:
// (省略重复的导入)
import {undo, redo, history} from "prosemirror-history"
import {keymap} from "prosemirror-keymap"let state = EditorState.create({schema,plugins: [history(),keymap({"Mod-z": undo, "Mod-y": redo})]
})
let view = new EditorView(document.body, {state})
插件在创建状态时注册(因为它们可以访问状态事务)。在为这个启用历史记录的状态创建视图后,您将能够按下 Ctrl-Z(或在 OS X 上按 Cmd-Z)来撤销您上次的更改。