ProseMirror 编程式操作文档
一般用户需要与编辑器的在页面的视图直接交互(光标聚焦到文档上,然后通过敲击键盘)才可以修改文档内容
而将 prosemirror-command 模块与 prosemirror-keymap 模块相结合,就可以实现编程式的方式来操作修改文档,例如通过将指令与菜单 UI 或快捷键相绑定,则可以实现点击按钮或按下快捷键来一键修改文档内容
ProseMirror Commands 模块
ProseMirror 的指令 command 是一个函数对各种编辑操作 editing action 进行封装,可以实现以编程式/命令式地修改文档。
指令 command 是一个函数 fn(state, dispatch?, view?) 它可以接受三个参数:
- 第一个参数
state是一个编辑器对象 - 第二个(可选)参数
dispatch是一个函数,用来分发一个 transaction 事务对象(对文档进行操作)提示
当没有设置
dispatch参数时,表示该命令只是一个 dry run 「试运行」,即进行模拟或预演操作(以判断它是否应该被执行),但不对文档进行实际修改 - 第三个(可选)参数
view是一个编辑器视图对象
该函数最后应该返回一个布尔值,以表示该操作是否可以执行(即是否已经执行成功)
prosemirror-commands 模块主要提供了一系列用于创建 commands 指令的函数,以及一些预设的 command 指令(以变量的形式提供):
deleteSelection变量:表示一个 command 指令。调用该指令删除选区内容joinBackward变量:表示一个 command 指令。调用该指令(满足特定条件时)尝试减小当前节点与它前面节点之间的距离
如果选区内容为空(处于光标状态),且位于一个 textblock 开头位置,调用指令会尝试减小当前节点与它前面节点之间的距离(文档中的位置距离是根据 index schema 规则而定的)
如果当前节点与前一个节点可以连接起来 joinable,则通过将它们相融合为一个节点,来减小距离;如果前后节点不可连接,则会尝试提升 lift 当前节点的层级(将它移出父节点),或将它移入到前一个节点的父节点里,来减小与前一个节点之间的距离
如果调用该指令时传递了第三个(可选)参数view则编辑器视图对象会用于更准确地判断文本块 textblock 的方向(bidi-aware 识别编辑器的书写的方向是从左往右,还是从右往左)textblock 文本块
文本块 textblock 所指的节点为 block 块级类型,且它的 content 内容由 inline 行内类型的节点构成(相当于节点的配置对象的属性
isBlock和inlineContent均为true)一般是指以文本作为内容的节点,例如
paragraph段落节点使用场景
该指令常见的应用场景是在文本块的开头按下
Backspace键删除与前一个节点之间的「间隔」,将它们融合为一个节点joinTextblockBackward属性:表示一个 command 指令,调用该指令(满足特定条件时)尝试当前节点与它前面节点相融合为一个节点
如果选区内容为空(处于光标状态),且位于一个 textblock 开头位置,如果当前节点与前一个节点可以连接起来 joinable,调用该指令则会将它们相融合为一个节点提示
该指令相当于
joinBackward指令 的特殊版本,它只会尝试将前后两个节点相融合,而不会尝试提升 lift 节点的层级selectNodeBackward属性:表示一个 command 指令,调用该指令(满足特定条件时)选择前一个节点
如果选区内容为空(处于光标状态),且位于一个 textblock 开头位置,调用该指令会尝试选择前一个节点使用场景
该指令常与
Backspace键相绑定如果在文本块的开头按下
Backspace键,但是(可能由于 schema 的约束,无法在该位置执行删除操作)无法执行joinBackward指令(或其他与删除内容相关的指令),那么该指令会作为 fallback 回退「保底」操作,将选区向前移动,选中前一个节点joinForward变量:表示一个 command 指令。与joinBackward指令类似,调用该指令(满足特定条件时)尝试减小当前节点与它后面节点之间的距离
如果选区内容为空(处于光标状态),且位于一个 textblock 结尾位置,调用指令会尝试减小当前节点与它后面节点之间的距离(文档中的位置距离是根据 index schema 规则而定的)使用场景
该指令常见的应用场景是在文本块的结尾按下
Delete键删除与后一个节点之间的「间隔」,将它们融合为一个节点joinTextblockForward变量:表示一个 command 指令。与joinTextblockBackward指令类似,调用该指令(满足特定条件时)尝试当前节点与它后面节点相融合为一个节点提示
该指令相当于
joinForward指令 的特殊版本,它只会尝试将前后两个节点相融合,而不会尝试提升 lift 节点的层级selectNodeForward变量:表示一个 command 指令,与selectNodeBackward指令类似,调用该指令(满足特定条件时)选择后一个节点使用场景
该指令常与
Delete键相绑定,(如果无法执行删除操作时)指令会一般作为 fallback 回退「保底」操作,将选区向后移动,选中后一个节点joinUp变量:表示一个 command 指令,将当前 block 块节点与前面相邻的兄弟节点相融合
如果当前选区并不是一个节点选区,而是 text selection 文本选区,则调用该指令时,会沿着节点树向上寻找最近的祖先节点,可与其兄弟节点进行融合使用场景
该命令通常用于将两个相邻块合并为一个,例如撤销分段操作,即在一个段落中按回车键会将当前段落一分为二,此时可以使用该指令将它们重新合并
joinDown变量:表示一个 command 指令,与joinUp指令类似,将当前 block 块节点与后面相邻的兄弟节点相融合lift变量:表示一个 command 指令,提升 lift 当前选中节点的层级(将它移出父节点)
如果当前选区并不是一个节点选区,而是 text selection 文本选区,则调用该指令时,会沿着节点树向上寻找最近的祖先节点,它可以执行提升 lift 操作使用场景
该指令常与
Tab键相绑定newlineInCode变量:表示一个 command 指令,如果选区在代码类型的节点中(节点的配置对象的属性code是真值),调用该指令会用一个\n换行符替换当前选区内容,在视觉上相当于换行(但不是创建一个新的 block 类型的节点),并移动选区(光标状态)到换行符后的位置使用场景
该指令常在代码块中与
Enter键相绑定exitCode变量:表示一个 command 指令,如果选区在代码类型的节点中(节点的配置对象的属性code是真值),调用该指令会在该节点后创建一个默认的 block 块类型的节点(一般是段落节点),并将选区移到新建的节点里(其效果相当于将光标移除代码块)createParagraphNear变量:表示一个 command 指令,如果选中一个 block 块类型的节点,调用该指令会在它的附近创建一个空的段落节点
如果当前选中的节点是(相对于其父节点)第一个子节点,则在它前面创建一个空的段落节点;否则在它的后面创建一个空的段落节点liftEmptyBlock变量:表示一个 command 指令,如果选区(光标状态)在一个空的 textblock 里,调用该指令可以将它提升 lift 一个层级(从其父节点提取出来,并将其移动到文档树的上一层)splitBlock变量:表示一个 command 指令,调用该指令会在选区处对其所在节点的父级节点进行分割。如果当前选区是一个文本选区,还会同时删除选区内容splitBlockKeepMarks变量:表示一个 command 指令,与splitBlock指令类似,但是分割后没有重置光标的 stored marks 预设样式标记(预设样式标记是指即将应用到下一次输入的内容的样式标记)splitBlockKeepMarks(splitNode?)方法:通过调用该方法创建一个 command 指令,执行该指令会在选区处对其所在节点的父级节点进行分割,与splitBlock指令类似(相当于它的特殊版本),但是分割生成的节点可以进行自定义
该方法接受一个(可选)参数splitNode它是一个函数fn(node: Node, atEnd: boolean) → {type: NodeType, attrs?: Attrs}该函数的第一个参数node是需要分割的原始节点对象 ❓ ;第二个参数atEnd是一个布尔值,表示选区是否在节点的结尾。该函数返回一个对象{type: NodeType, attrs?: Attrs}描述分割所生成的节点使用场景
如果光标位于段落中并按下
Enter键时,通常通过splitBlock指令将当前的段落分割成两个段落如果需要定制 block 块类型的节点分割操作的结果,例如希望新的分割块不仅仅是一个新的段落,而是一个具有特定属性的节点,则可以使用
splitBlockAs指令,可以更灵活地控制和自定义块的分割行为。调用该指令进行分割操作时,会调用该指令所接受的参数
splitNode(一个函数),根据返回结果来创建新的节点(而不是默认的段落)selectParentNode变量:表示一个 command 指令,调用该指令将当前选区移至其父节点(如果父节点是可选中的,但是并不会选中文档的根节点)使用场景
当需要快速扩大选区,从选择文档中较小的内容(比如单个字符或者单词)变成包含整个父级结构(比如整个段落或者列表项)的时候,可以使用该指令
selectAll(state, dispatch?)方法:该方法就是一个指令,调用该指令以全选文档。第一个参数state是编辑器状态对象,第二个(可选)参数dispatch是一个函数fn(tr)它接受一个事务对象以修改选区。该方法最后返回一个布尔值,以表示指令是否执行成功。wrapIn(nodeType, attr?)方法:通过调用该方法创建一个 command 指令,执行该指令会创建一个给定类型nodeType的节点对象,它具有特定的属性attr,用该节点来包裹选区。
该方法的第一个参数nodeType是一个 nodeType 对象,用于指定所需创建的节点的类型;第二个(可选)参数attr是一个对象,用于设置节点需要具有哪些属性setBlockType(nodeType, attrs?)方法:通过调用该方法创建一个 command 指令,执行该指令会将选区所在的 textblock 文本块转换为给定类型的节点
该方法的第一个参数nodeType是一个 nodeType 对象,用于指定所需创建的节点的类型;第二个(可选)参数attrs是一个对象,用于设置节点需要具有哪些属性textblock 文本块
文本块 textblock 所指的节点为 block 块级类型,且它的 content 内容由 inline 行内类型的节点构成(相当于节点的配置对象的属性
isBlock和inlineContent均为true)一般是指以文本作为内容的节点,例如
paragraph段落节点toggleMark(markType, attrs)方法:通过调用该方法创建一个 command 指令,执行该指令会对给定类型markType样式标记进行切换。
该方法的第一个参数markType是一个 markType 对象,用于指定所需切换的样式标记类型;第二个(可选)参数attrs是一个对象,用于设置样式标记需要具有哪些属性
执行指令的结果有多种情况:- 如果当前选区已经存在给定类型
markType的样式标记,则会将它移除;否则会将其添加到选区内容上 - 如果选区为空(光标状态)则会将该样式标记添加到光标的 stored marks 预设样式标记(预设样式标记是指即将应用到下一次输入的内容的样式标记)
- 如果当前选区不支持该类型
markType的样式标记,则指令最后会false
- 如果当前选区已经存在给定类型
autoJoin(command, isJoinable)方法:通过调用该方法,对给定的指令command进行封装,返回一个新的指令,正如该方法的名称,返回的指令可以实现自动将两个节点融合的操作。
如果给定的指令command所触发的转换 transform 最终会在文档中生成两个相邻可连接 joinable 的节点,那么经过该方法对给定的指令进行封装,就会得到一个新的指令,它除了可以完成原有指令command的操作,还会进一步自动将原有指令生成的两个节点进行融合
该方法的第一个参数command是原有的需要封装的指令。第二个参数isJoinable可以是一个函数fn(before: Node, after: Node) → boolean它接受两个参数before和after都是 node 节点对象,表示相邻的两个节点,返回布尔值以判断是否它们可以合并;也可以是一个数组,它的元素是字符串以表示节点名称,在该数组中的节点类型是可以合并的可融合的节点
可融合的 joinable 节点是需要具有相同的节点类型
应用场景
这个方法对原有的指令进行封装,可以使得它们更「智能」,让文档编辑过程中的节点合并操作自动化完成,提升了编辑效率和用户体验。
例如在编辑列表或段落时,当用户执行某些操作(删除操作)导致两个列表或段落紧挨在一起时,这两个节点可以合并(比如它们都是同类型的列表),如果经过
autoJoin对这些操作进行封装得到新的指令,则调用新的指令除了完成原有的操作,还会自动合并这两个节点,而不需要用户进行额外的操作。
该模块还提供了一个方法 chainCommands(...commands) 用于将给定的一系列指令封装为一个新的指令,调用该指令会依次执行 ...commands 传入的指令,直到其中一个指令返回值为 true 为止(则停止执行后续的指令)
该模块还提供了一个对象 baseKeymap 以键值对的形式将常见的按键和指令进行匹配,键名是字符串(以出现在按键事件对象属性 KeyEvent.key 值)来表示按下什么按键(也支持组合键,具体规则参考 prosemirror-keymap 模块的介绍),值是对应的指令
提示
其实 baseKeymap 会基于编辑器所运行的系统环境,提供不同的按键与指令的匹配方案,(基于浏览器的 navigator 接口)如果侦测到系统环境是 MacOS 就会采用 macBaseKeymap,否则就采用 pcBaseKeymap
// 基于浏览器的 navigation 或 Node.js 的 os 模块判断用户的操作系统
const mac = typeof navigator != "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) : typeof os != "undefined" && os.platform ? os.platform() == "darwin" : false
/// Depending on the detected platform, this will hold
/// [`pcBasekeymap`](#commands.pcBaseKeymap) or
/// [`macBaseKeymap`](#commands.macBaseKeymap).
export const baseKeymap: {[key: string]: Command} = mac ? macBaseKeymap : pcBaseKeymap
这两种按键与指令的匹配方案具体如下
pcBaseKeymap 适用于 Linux 和 Windows
export const pcBaseKeymap: {[key: string]: Command} = {
// 将 Enter 键与一系列指令相绑定
// 该链式指令的效果是在光标位置分割 textblock 文本块,并创建新行
// 如果所在的文本块内容为空,则将光标跳出当前的层级(向上提升,将光标移到父节点外)
"Enter": chainCommands(newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock),
// 将 Mod+Enter 组合键与 exitCode 指令相绑定
"Mod-Enter": exitCode,
// 将 Backspace 键与一系列指令相绑定(该链式指令的具体如下)
// let backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward)
// 该链式指令的效果是将光标向前回退,删除前面的内容
// 如果在文本块的开头,则将该文本块向上提升跳出父节点
"Backspace": backspace,
// 将 Mod+Backspace 组合键与一系列指令相绑定
"Mod-Backspace": backspace,
// 将 Shift+Backspace 组合键与一系列指令相绑定
"Shift-Backspace": backspace,
// 将 Delete 键与一系列指令相绑定(该链式指令的具体如下)
// let del = chainCommands(deleteSelection, joinForward, selectNodeForward)
// 该链式指令的效果是将光标后面的内容删除
"Delete": del,
// 将 Mod+Delete 组合键与一系列指令相绑定
"Mod-Delete": del,
// 将 Mod+A 组合键与 selectAll 指令相绑定
// 该指令的效果是全选文档
"Mod-a": selectAll
}
macBaseKeymap 适用于 Mac
// 复用 pcBaseKeymap 的按键与指令的映射关系,并且将一些指令再额外绑定其他的快捷键
export const macBaseKeymap: {[key: string]: Command} = {
// 将 Ctrl+H 组合键也绑定到 Backspace 键所相关的指令
"Ctrl-h": pcBaseKeymap["Backspace"],
// 将 Alt+Backspace 键也绑定到 Mode+Backspace 组合键所相关的指令
"Alt-Backspace": pcBaseKeymap["Mod-Backspace"],
// 将 Ctrl+D 组合键也绑定到 Delete 键所相关的指令
"Ctrl-d": pcBaseKeymap["Delete"],
// 将 Ctrl+Alt+Backspace 组合键也绑定到 Mod+Delete 组合键所相关的指令
"Ctrl-Alt-Backspace": pcBaseKeymap["Mod-Delete"],
// 将 Alt+Delete 组合键也绑定到 Mod+Delete 组合键所相关的指令
"Alt-Delete": pcBaseKeymap["Mod-Delete"],
// 将 Alt+D 组合键也绑定到 Mod+Delete 组合键所相关的指令
"Alt-d": pcBaseKeymap["Mod-Delete"],
// 将 Ctrl+A 组合键与 selectTextBlockStart 指令相绑定
// 该指令的效果是将光标移动到文本块的开头
"Ctrl-a": selectTextblockStart,
// 将 Ctrl+E 组合键与 selectTextblockEnd 指令相绑定
// 该指令的效果是将光标移动到文本块的结尾
"Ctrl-e": selectTextblockEnd
}
// 遍历 pcBaseKeymap 对象的属性,将它们复用到 macBaseKeymap 对象中
for (let key in pcBaseKeymap) (macBaseKeymap as any)[key] = pcBaseKeymap[key]
使用方式
前面所述对象 baseKeymap(也包括 pcBaseKeymap 和 macBaseKeymap 对象)只是存储了按键与指令的匹配信息,如果要将它们应用到编辑器中,还需要使用方法 keymap(bindings)
这些对象作为 方法 keymap(bindings) 的参数,调用该方法会返回一个浏览器插件,然后将插件注册到编辑器上,就可以实现按键与指令的绑定
具体可以参考官方所提供的基础编辑器样例
// refer to https://github.com/ProseMirror/prosemirror-example-setup/
// ...
import {baseKeymap} from "prosemirror-commands"
// ...
export function exampleSetup(options){
// ...
let plugins = [
buildInputRules(options.schema),
keymap(buildKeymap(options.schema, options.mapKeys)),
keymap(baseKeymap),
dropCursor(),
gapCursor()
]
// ...
return plugins.concat(new Plugin({
props: {
attributes: {class: "ProseMirror-example-setup-style"}
}
}))
}
import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
import {Schema, DOMParser} from "prosemirror-model"
import {schema} from "prosemirror-schema-basic"
import {addListNodes} from "prosemirror-schema-list"
import {exampleSetup} from "prosemirror-example-setup"
// Mix the nodes from prosemirror-schema-list into the basic schema to
// create a schema with list support.
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks
})
window.view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
doc: DOMParser.fromSchema(mySchema).parse(document.querySelector("#content")),
plugins: exampleSetup({schema: mySchema})
})
})
ProseMirror Keymap 模块
prosemirror-keymap 模块提供了一些辅助函数可以将 command 指令与按键相绑定,以实现编辑器的 shortcut 快捷键功能
该模块导出了两个方法
keymap(bindings)方法:基于给定的bindings对象,创建一个插件(将插件注册到编辑器上,就可以实现按键与指令的绑定)
该方法所接受的参数bindings是一个对象,以键值对的形式将常见的按键和指令进行匹配,键名是字符串来表示按下什么按键,值是对应的 command 指令键名的规则
采用出现在按键事件对象属性
KeyEvent.key值来表示按下什么按键💡 可以在该网页查看所有键值列表
使用小写字母来表示只按下某个字母相关的键,使用大写字母来表示同时按下
Shift键(不需要再显式地添加Shift-前缀,因为 ProseMirror 会隐式地添加了该判别条件)使用
Space来表示按下" "空格键键名也可以是组合键,支持在按键前面添加相应的 modifiers 修饰键(用
-连字符相隔,多个修饰键的先后次序并无所谓),例如Shift-Ctrl-Enter。修饰键还支持别名:Shift-或s-Alt-或a-Ctrl-或c-或ControlCmd-或m-或Meta-
也可以使用
Mod-来作为 MacOS 系统上Cmd-与其他系统上的Ctrl-的通用别名说明
可以为编辑器添加多个由
keymap生成的创建,根据插件的注册顺序来决定相应指令的执行优先次序当其中一个指令返回
true则表示对按键所分发的事件已经处理完成(之后的指令就不再执行了)keydownHandler(bindings)分发:基于给定的bindings对象,创建一个事件处理函数fn(view, event)(当编辑器接收到keydown事件时会调用该处理函数)
该方法所接受的参数bindings是一个对象,以键值对的形式将常见的按键和指令进行匹配,键名是字符串来表示按下什么按键,值是对应的 command 指令在哪里使用
该方法所返回的事件处理函数,是作为
new EditorView(place, props)实例化编辑器视图对象时,配置对象的属性props.handleKeyDown的值该事件处理函数接受两个参数,第一个参数
view是编辑器视图对象,第二个参数event是 KeyboardEvent 键盘事件对象。最后返回一个布尔值或undefined,以表示该处理函数是否已经响应/处理完成了该事件