ProseMirror Custom Schema Example
本文介绍 ProseMirror 官方提供的几个例子,从最基本的 schema 入手,然后介绍构建自定义的节点和样式标记,以及如何使用指令
简化版本
根据官方示例编写了一个简化版本
最简单的 schema
最简单的 schema 可以仅包含 doc 和 text 节点类型,由此数据结构所约束的编辑器只能包含 inline 文本(不能换行,仅有一行)
import {Schema} from "prosemirror-model"
const textSchema = new Schema({
nodes: {
text: {},
doc: {content: "text*"}
}
})
自定义节点
在自定义节点时,对于非文本节点 text 或非根节点 doc 都应该设置 toDOM 属性(这样才可以将该类型的节点渲染到页面上)和 parseDOM 属性(这样才可以将页面上的相应的元素解析为该类型的节点)
自定义元素
节点的 toDOM 属性将节点转换为 HTML 元素渲染到页面上,一般采用标准的元素
也支持 custom element 自定义元素,例如在官方示例中使用了 <note> 和 <notegroup> 自定义元素
在 parseDOM 属性需要针对相应的元素进行解析
创建指令和绑定快捷键
要在页面插入自定义节点,一般需要构建相应的 command 指令,然后可以通过菜单栏的按钮触发指令,或与相应的键盘按键进行映射/绑定通过快捷键触发指令
指令 command 是一个函数 fn(state, dispatch?, view?) 最后应该返回一个布尔值,以表示该操作是否可以执行
提示
使用指令在编辑器中插入自定义节点,需要保证满足 schema 数据约束,应该先使用方法 node.canReplaceWith(from, to, nodeType, marks?) 进行条件判断
function insertStar(state, dispatch) {
let type = starSchema.nodes.star // 星形节点类型 nodeType
let {$from} = state.selection // 选区的开始位置
// 方法 $from.index() 返回的是光标所在的文本节点(在父节点里)的索引值
// 通过方法 `node.canReplaceWith(from, to, nodeType)` 判断用类型为 nodeType 的节点替换(索引值)从 from 到 to 的节点是否可行
// 由于这里假设选区处于光标状态,所以替换节点的索引值都是 $from.index() 即假设用星形节点替换光标所在的文本节点
if (!$from.parent.canReplaceWith($from.index(), $from.index(), type))
return false
// 执行替换/插入操作
dispatch(state.tr.replaceSelectionWith(type.create()))
return true
}
如果要将指令与快捷键绑定,则需要方法 keymap({shortcutKey: Command}) 构建一个 plugin 插件,再应用到编辑器上
new EditorView(place, {
state: EditorState.create({
doc,
plugins:[
// baseKeymap 是 prosemirror-command 模块导出的变量,包含了富文本编辑器常见/通用的快捷键
// 它是一个对象,以键值对 {shortcutKey: Command} 的形式表示键盘按键与指令的映射关系
keymap(baseKeymap)
]
})
})
样式标记的属性 inclusive
样式标记 mark 的属性 inclusive 默认值为 true,表示当光标位于带有该样式标记的文本的末尾处,继续输入的文字依然采用该样式标记(如果带有该样式标记的文本位于文本块的开头,则光标位于开头继续在前面输入的文字,也依然采用该样式标记)
对于 inclusive 为 true 的样式标记,可以在光标状态(未选中文本内容时)进行 toggle 切换,所影响的是称作 active mark 激活的/待添加的样式标记,即对未来输入的内容是否应用上该样式标记
但是对于 link 链接样式这种行为并不符合预期,所以需要将 link 样式标记的属性 inclusive 设置为 false,即位于一个已存在的链接前后继续输入文本都不会被纳入链接里
对于 inclusive 为 false 的样式标记,在光标状态(未选中文本内容时)进行 toggle 切换是无意义/不合理的(例如对于 link 样式标记,需要针对选中的文本内容设置链接,而不能先设置链接再输入文本),所以在 toggle 切换这种类型的样式标记时,需要判断选区是否为空
// 用于切换 link 样式标记的指令
function toggleLink(state, dispatch) {
let {doc, selection} = state
// 判断选区是否为空
if (selection.empty) return false
let attrs = null
// 使用方法 node.rangeHasMark(from, to, type) 判断选中范围是否已经应用了 link 样式标记
// 如果选区范围的文本已经应用了 link 样式,则删除该样式标记
if (!doc.rangeHasMark(selection.from, selection.to, starSchema.marks.link)) {
// 如果选区范围的文本没有应用 link 样式,则添加该样式标记,先要使用 prompt 提示让用户输入链接地址
attrs = {href: prompt("Link to where?", "")}
if (!attrs.href) return false
}
// 执行样式标记切换操作
return toggleMark(starSchema.marks.link, attrs)(state, dispatch)
}
// ⚠️ 使用 prompt 弹出提示框让用户输入链接地址,它会阻塞进程,等待用户的响应,所以上述操作都是同步代码,这样就可以保证参数 state 是当前编辑器的(最新)状态
// ⚠️ 为了更好的用户体验(使用 prompt 会冻结整个页面,用户暂时无法与页面的其他部分进行交互),也可以使用其他方式获取链接地址,例如 fetch 等异步代码获取数据
// ⚠️ 但是需要注意最后 toggleMark 切换样式标记时,要采用最新的编辑器状态对象(通过 view.state 获取,而不能使用传入的参数 state,因为存在异步代码,原来传入的 state 可能不是最新的)