ProseMirror Dino Example For Command
ProseMirror 在官方文档中演示了如何创建自定义 node 节点,并创建自定义指令(以实现点击按钮往编辑器中插入该节点的功能),完整的源码可以查看相关的 Github 仓库
一些值得注意的细节:
- 通过
schema.spec.nodes获取所有节点类型,它是一个 OrderMap 映射对象,以键值对(键名是字符串)的形式表示 schema 数据约束对象支持的节点类型
与普通的对象的主要不同的是 OrderMap 是有顺序的注意
OrderMap 是 ProseMirror 作者所创建的一种数据结构,但是它并没有针对大量的键值对的数据进行优化,所以这种数据结构在处理大量数据时可能并不那么高效
由于编辑器的节点类型(或样式标记类型)一般是有限的小型键值对数据集,所以在 ProseMirror 使用 OrderMap 性能应该是足够的
OrderMap 内置了一些属性和方法以更方便地操作数据
在该编辑器示例中,使用orderMap.addBefore(place, key, string)方法将给定的键值对(键名为key)放置在place(键值对的键名)之前(如果键名place未能在原 orderMap 里找到,则会将给定的键值对添加到末尾)const dinoSchema = new Schema({ // 这里的 schema 是一个对象,来自 prosemirror-schema-basic 模块(所导出的变量) // schema 包含了一些预设的简单节点 // 将 `dino` 恐龙节点放置在 `img` 图像节点前面 nodes: schema.spec.nodes.addBefore("image", "dino", dinoNodeSpec), marks: schema.spec.marks })
通过schema.spec.marks获取的所有样式标记类型,也是一个 OrderMap 映射 - 根据前面对 OrderMap 的介绍,节点类型在 schema 中的是有先后顺序的(先出现/注册的优先级更高,对于样式标记类型也一样),由于编辑器中涉及对 DOM 的匹配和转换,而优先级别对此有重要影响
在该示例中dino恐龙节点和img图像节点都是对应于<img>元素。但是dino节点还需要<img>元素带有特定的属性img[dino-type]才可以匹配;而img节点仅需要<img>元素带有图片通用的属性img[src]即可匹配
在 schema 注册节点类型时,需要确保dino节点优先级更高(出现/排在img节点之前),这样才可以确保<img dino-type="...">元素可以转换为dino节点,而不会都转换为img节点priority
除了在 schema 注册节点类型时将
dino节点置于img节点之前,还可以通过设置priority属性以提升恐龙节点的优先级在
dino节点配置对象中parseDOM(一个数组)包含一些将 DOM 元素解析为该节点的规则,可以在这些规则中设置属性priority以提高该节点解析的优先级,该属性值的默认值为50,设置解析规则的优先级顺序,数值越大优先级越高,匹配时该规则会先执行设置 - 创建自定义 command 指令,可以通过点击按钮在编辑器中插入
dino节点,根据官方示例编写的简化版本的相关代码如下ts// command to insert dino node // refer to https://github.com/ProseMirror/website/blob/master/example/dino/index.js const dinoType = simpleSchemaWithDino.nodes.dino; function insertDino(type: any): Command { return (state, dispatch) => { const { $from } = state.selection; const index = $from.index(); // 光标所在的文本节点(在父节点里)的索引值 // 这里通过方法 `node.canReplaceWith(from, to, nodeType)` 判断用类型为 nodeType 的节点替换(索引值)从 from 到 to 的节点是否可行 // 由于这里假设选区处于光标状态,所以替换节点的索引值都是 index 即假设用 dino 节点替换光标所在的文本节点 if(!$from.parent.canReplaceWith(index, index, dinoType)) return false; if(dispatch) { dispatch(state.tr.replaceSelectionWith(dinoType.create({type}))); } return true; } } const dinoBtns = document.getElementsByTagName('button'); const insertDinoCommands: {[key: string]: Command} = {}; for(let i = 0; i < dinoBtns.length; i++) { const dinoType = dinoBtns[i].id; insertDinoCommands[dinoType] = insertDino(dinoType); dinoBtns[i].addEventListener('click', (event) => { if(event.target) { const type = (event.target as HTMLElement).id; insertDinoCommands[type](view.state, view.dispatch); } }) }
在官方示例中,还会使用command(state)的方式调用指令(没有设置第二个参数dispatch,表示该指令只是一个 dry run 「试运行」,即进行模拟或预演操作,但不对文档进行实际修改),根据返回的布尔值判断该指令在当前编辑器状态下是否可以执行