ProseMirror State 模块
ProseMirror 将编辑器的状态存储到一个对象中,prosemirror-state 模块提供了相关的类和方法用于生成 state 编辑器状态。另外该模块该定义了 selection 选区(表示用户框选的内容范围)和 plugin 插件系统(可用于拓展编辑器状态)。
编辑器状态
其中编辑器状态对象由 3 个核心部分构成:
- doc 文档:即文档所包含的 nodes、marks 等
- selection 选区:即用户选中的内容
- storedMark 预设的样式标记:即对于之后输入的内容应用哪些样式标记
EditorState 类
EditorState 类提供了一些静态方法进行实例化:
- static
EditorState.create(config)静态方法:基于配置对象config(它需要符合一种 TypeScript interfaceEditorStateConfig)创建一个新的 state 状态对象EditorStateConfig
TypeScript interface
EditorStateConfig描述编辑器状态可接受哪些配置参数schema(可选)属性:一个 schema 数据结构约束对象,用于设置编辑器所允许容纳的内容。在没有设置属性doc时才会使用该属性doc(可选)属性:一个 node 节点对象(作为根节点),用于设置编辑器的起始内容
注意
以上两个可选属性
schema和doc必须设置其中一个selection(可选)属性:一个 selection 对象,用于设置编辑器的选区storedMarks(可选)属性:一个数组(其元素是一系列 mark 对象),表示预设的样式标记(即对于之后输入的内容应用哪些样式标记)plugins(可选)属性:一个数组(其元素是一系列 plugin 对象),用于设置编辑器所使用的插件
提示
可以通过注册插件 plugin 来为编辑器状态对象 state 新增字段/属性
- static
EditorState.fromJSON(config, json, pluginFields?)静态方法:基于配置参数config和 JSON 对象,创建一个新的 state 状态对象
相关参数的具体说明如下:- 第一个参数
config是一个对象,至少包含schema属性,以设置编辑器所允许容纳的内容;一般还会有plugins属性,它是一个数组(包含一系列 plugin 插件对象),这些插件会在初始化编辑器的状态时被调用 - 第二个参数
json是需要反序列化的 JSON 对象,它会被转换为编辑器的内容 - 第三个(可选)参数
pluginFields是一个对象。如果第二个参数jsonJSON 对象中包含了 plugin 的 state 则需要设置该参数。该对象是用来基于插件的 key 和 JSON 对象的属性名的对应关系,指明 JSON 对象中的哪些字段对应(存储)哪个 plugin 的 state,在反序列 deserialize 过程中可以将 JSON 相应的属性值转换为 plugin 的 state
提示
如果设置了
pluginFields参数,则在对应插件的属性state(一个对象)中需要设置fromJSON方法(该方法传入的参数是config、插件对应的 json 和根据 config 生成的编辑器state)如果 JSON 对象的字段没有对应到任一个 plugin 的 key,则会直接调 plugin 的 state 的
init方法(该方法传入的参数是config和根据 config 生成的编辑器的state) - 第一个参数
EditorState 类的实例化对象表示编辑器的一个 state 对象,以下称作「编辑器状态对象」
注意
state 虽然是一个 JavaScript 对象,但它应该是 immutable 持久化的,即其不应该直接修改它,而应该基于原有的状态,(通过 apply transaction 应用事务)生成一个新的状态
state 编辑器状态对象包含一些属性和方法:
doc属性:一个 node 节点对象,表示当前的文档(根节点)selection属性:一个 selection 选区对象,表示当前用户选中的内容storedMarks属性:一个数组(其中包含一系列 mark 样式标记对象),表示即将应用到下一次输入的内容的样式标记(例如预先开启了粗体的标记,再输入文本就直接显示为粗体的),如果没有预设的样式标记则该属性值为nullschema属性:一个 schema 数据结构约束对象,表示编辑器可容纳哪些格式的内容plugins属性:一个数组(其元素是一系列 plugin 对象),表示当前所激活的插件apply(tr)方法:对当前编辑器状态应用一个事务tr并返回一个新的编辑器状态applyTransaction(rootTr)方法:和方法apply()类似,也是将事务rootTr应用到当前编辑器状态,但它返回一个对象{ state: EditorState, transactions: [Transaction] }包含(关于这次状态更新的)更详细的信息(其中属性state是新的编辑器状态,属性transactions是在此过程中通过插件所应用的诸多事务)tr属性:基于当前编辑器状态创建一个 transaction 事务reconfigure(config)属性:基于当前编辑器状态和传入(新的)配置参数config(对编辑器的激活插件进行调整)创建一个新的状态。
在配置参数config中包含一个属性plugins,该属性值是一个数组(包含一系列 plugin 对象),更新编辑器状态时将会应用这些插件
在两个状态 state 中都有的属性保持不变;对于配置参数config中舍弃的属性会在新的状态中移除;对于配置参数config中新增的属性会调用插件的init()方法来设置它们的初始值toJSON(pluginField?)方法:将当前的编辑器状态序列化为 JSON 对象
(可选)参数pluginField可以是多种数据格式。如果想序列化时保留 plugin 的 state,则需要传入一个对象,以键值对的方式指定序列化得到 JSON 对象中哪个字段对应存储哪个 plugin 的 state(键名是 JSON 字段名,值是 plugin 实例对象);也可以传入一个字符串或数字作为参数,则与 plugin 的 state 将会在转换过程中被忽略。提示
如果想将编辑器状态序列化为 JSON 对象时,保留 plugin 的 state,则需要同时在相应的 plugin 的属性
state中提供toJSON(key)方法(其入参key是插件的 key)
Transaction 类
Transaction 类与操作编辑器内容相关
通过将一个 transaction 事务对象(可以调用状态对象的方法 state.apply(transaction) 生成一个事务对象)应用到当前(旧的)编辑器状态对象,才可以生成一个新的状态。
例外场景
也有例外的情况,即初始化编辑器时,通过载入文档内容的方式来创建一个全新的编辑器状态对象
Transform
Transaction 类继承自 Transform 类(具体可参考 prosemirror-transform 模块),此外 transaction 还包括选区的变化,它提供了一些相关的方法来操作选区,例如方法 transaction.replaceSelection(slice)
通过编辑器状态对象的属性 state.tr 基于编辑器当前的状态创建一个空的事务对象实例
step
其实 transaction 是多个步骤 step 的一个合集,将它们应用于当前的文档状态上以创建一个新的文档状态
以上通过 state.tr 所创建的是一个空的事务对象,即其中并不包括任何 step,所以应用该 transaction 并不会对文档进行操作,可以调用 transaction 对象的相关方法为 transaction 添加相应的 steps 或其他 updates
提示
另外可以在事务中添加一些元信息 metadata,以对该事务操作提供额外的描述。
例如用户通过鼠标或触屏设备选中内容时,由 editor view 编辑器的视图所创建的 transaction 事务中,元信息会包含属性 pointer 其值为 true;如果用户通过粘贴、剪切、拖放等操作改变编辑器内容时,editor view 编辑器的视图所创建的 transaction 事务中,元信息会包含属性 uiEvent 其值分别为 paste、cut 或 drop
Transaction 类的实例化对象 transaction 称作「事务对象」,一般简称为 tr
事务对象 transaction 包含一些属性和方法:
提示
transaction 事务对象的方法一般返回该事务对象本身,以便进行链式调用,创建一系列连续的操作
time属性:一个数字,该事务创建时的时间戳,和Date.now()的值相同setTime(time)方法:根据传入的参数time(它的值是数字)更新该事务的时间戳。返回该事务对象本身storedMarks属性:一个数组(其中包含一系列 mark 样式标记对象,它们作为预设样式标记,即将应用到下一次输入的内容的样式标记),表示该事务要将预设样式标记修改为什么样子setStoredMarks(marks)方法:将该事务的预设样式标记设置为参数mark(它是是一个数组,其中每个元素都是 mark 样式标记对象)。返回该事务对象本身storedMarksSet属性:一个布尔值,表示该事务是否设置调整了预设样式标记ensureMarks(marks)方法:确保该事务中的预设样式标记与参数marks(一个由一系列mark对象构成的数组)一致,如果不一致就对其进行修改,以保证与入参marks一致。返回该事务对象本身
如果该事务没有对预设样式标记进行设置,则需要确保光标(选中内容)的样式标记与参数marks一致addStoredMark(mark)方法:将参数mark样式标记对象添加到该事务的预设样式标记集合中。返回该事务对象本身removeStoredMark(mark)方法:将给定的mark样式标记(可以是一个样式标记对象,或 markType 样式标记类型)从该事务的预设样式标记集合中移除。返回该事务对象本身selection属性:一个 selection 选区对象,表示编辑器(在应用该事务后)选中的内容。它默认是对原来的选区经过该事务中不同 steps 步骤 map 映射后的结果。Tip
可以调用方法
transaction.setSelection()修改(通过 step 步骤映射)默认生成的选区setSelection(selection)方法:将该事务的选区更新为参数selection。它会覆盖原有的映射行为,决定应用事务后编辑器所选中的内容。返回该事务对象本身selectionSet属性:一个布尔值,表示该事务是否显式地对选区进行调整操作replaceSelection(slice)方法:用传入的slice切片对象替换当前选区选中的内容。返回该事务对象本身replaceSelectionWith(node, inheritMarks?)方法:将选中内容替代为给定的node节点对象,如果第二个(可选)参数inheritMarks设置为true,且替换插入的node节点的内容/片段是 inline 内联内容,则插入的内容将会继承该位置原有的样式标记。返回该事务对象本身提示
在内部调用 transform 对象的方法
replaceRangeWith对选中内容进行替换,ProseMirror 可能会自动调整替换范围(例如当选区的 from 和 to 相同且都位于父级节点的起始或结尾位置,而给定的 node 并不适合此位置时,则可能将给定的 node 放置到合适的祖先节点里,以满足 schema 的约束)deleteSelection()方法:删除选区选中的内容。返回该事务对象本身insertText(text, from, to)方法:用给定的text文本节点对象的内容,替换从from到to范围的文档内容;如果没有指定范围,就将text文本节点对象的内容替换当前选区的内容。返回该事务对象本身setMeta(key, value)方法:为该事务设置元信息。第一个参数key可以是一个字符串、plugin 插件对象或 pluginKey 表示插件的键名,作为新增的元信息的属性名;第二个参数value是新增的元信息的值。返回该事务对象本身提示
因为一个事务可能会被不同的 plugin 设置不同的 metadata 信息,因此需要区分。所以参数
key不仅可以是简单的一个字符串,还可以传递 plugin 或 PluginKey。getMeta(key)方法:获取特定的元信息。其入参key可以是一个字符串、plugin 插件对象或 pluginKey 表示插件的键名,其返回值是给定键名的相应元信息isGeneric属性:一个布尔值,表示该事务是否含有元信息,如果不包含任何元信息,则该属性值为true提示
通过该属性可以判断该事务是否包含额外的元信息,如果没有就可以放心对该事务扩展修改
例如与其他的 step 操作步骤进行合并,如果事务有元信息,则它可能是对于某个插件有特殊用途的,不能简单地与其他 step 进行合并
scrollIntoView()方法:表示应用该事务后(更新完 state 编辑器状态后),编辑器要将选区滚动到视图窗口中。返回该事务对象本身scrollIntoView属性:一个布尔值,表示该事务是否调用了scrollIntoView()方法将选区滚动到视图窗口中。
command
另外该模块还导出了一个 TypeScript 类型 type Command 它用于约束一个函数类型
type Command = fn(
// 编辑器状态对象
state: EditorState,
// dispatch 方法用来分发该指令
// 该方法可以接受一个 tr 事务作为参数,它用以更新编辑器状态
dispatch?: fn(tr: Transaction),
// 编辑器的视图对象
view?: EditorView
) → boolean
// 最后返回一个布尔值,表示该指令是否触发成功(即 tr 事务是否执行成功)
该类型的函数用于在 ProseMirror 中注册 command 指令,具体实现主要在 prosemirror-commands 模块中
Selection 类
ProseMirror 的 Selection 概念和 web 原生的 Selection 概念类似,表示文档中的一个选区,但具体的实现有所不同。
Selection 是一个 TypeScript abstract class 抽象类,它给出了一些抽象方法和具体的方法
抽象类
TypeScript abstract class 不直接实例化,由于它一般只定义了抽象的方法(虽然也可以包含非抽象的方法),即针对这些方法只是对入参和输出值的类型进行了约束,而没有给出这些方法的具体实现
抽象类可以被其他类继承,子类需要实现抽象类中的抽象方法,所以一般是创建子类再使用(才能进行实例化)
所以这里的 abstract class Selection 抽象类不直接实例化,要先被继承,给出父类的抽象方法 eq、map、toJSON 的具体实现,构建出各种子类再使用
ProseMirror 内置实现了两种不同类型的选区子类,TextSelection 文本选区子类(它包括了选区为空/处于光标状态的情况),NodeSelection 节点选区子类
开发者可以继承这个抽象类,自定义其他类型的选区
通过方法 new Selection($anchor, $head, ranges) 进行实例化,获取一个选区对象(实际上需要先继承该类,再可以对其子类进行实例化),各参数的具体说明如下:
- 第一、二个参数
$anchor和$head都是一个 resolvedPos 对象,分别表示选区的锚点(锚定「不动」的一端,即框选开始时光标的位置)和动点(即框选结束时光标的位置,如果选区变化会在这一侧发生) - 第三个(可选)参数
ranges是一个数组(一般仅含有一个元素的),该元素是一个SelectionRange对象,表示文档中的一个选区对应于文档中的范围,如果省略该参数 ProseMirror 会根据$anchor和$head构建出该对象SelectionRange
SelectionRange类表示一个选区所对应于文档中的范围通过方法
new SelectionRange($from, $to)实例化,将其称为「选区范围」,它接受两个 resolvedPos 对象$from和$to作为参数,分别表示该选区范围的 lower boundary 下界(在文档中较前的位置)和 upper boundary 上界(在文档中较后的位置)
也可以使用该类所提供的一些静态方法进行实例化(实际上需要先继承该类,再可以通过调用其子类的相应的静态方法进行实例化):
- static
Selection.findFrom($pos, dir, textOnly?)静态方法:在给定位置$pos(一个resolvedPos 对象)的一侧寻找一个合法/合适的地方创建一个文本选区(该文本选区处于光标状态)或节点选区(该节点是叶子节点)。如果没有找到可用的地方来创建选区则返回null
第二个参数dir的正负值决定寻找的方向,如果是正值就从位置$pos开始向右侧寻找;如果是负值就向左侧寻找
第三个(可选)参数textOnly是一个布尔值,以判断是否只寻找适合于创建文本选区(该文本选区处于光标状态)的地方提示
该方法一般是在执行粘贴或其他操作后调用的,在该场景中可能不知道要将光标放到哪个合适的位置,该方法会自动寻找一个合适的位置,而不需要显式地手动设置
另一个类似的静态方法是
Selection.near() - static
Selection.near($pos, bias?)静态方法:在给定位置$pos(一个resolvedPos 对象)附近寻找一个合法/合适的地方创建一个文本选区(该文本选区处于光标状态)或节点选区(该节点是叶子节点)
第二个(可选)参数bias是一个数值(默认值为1),设置优先寻找的方向。如果bias为正数则优先向右侧寻找;如果参数bias为负值则优先向左侧寻找 - static
Selection.atStart(doc)静态方法:在给定文档doc(一个节点对象,根节点,表示文档)的开头开始去寻找一个合法/合适的地方创建一个文本选区(该文本选区处于光标状态)或节点选区(该节点是叶子节点)。如果没有寻找到可用的选区,则返回一个allSelection全选选区 - static
Selection.atEnd(doc)静态方法:在给定文档doc(一个节点对象,根节点,表示文档)的结尾开始去寻找一个合法/合适的地方创建一个文本选区(该文本选区处于光标状态)或节点选区(该节点是叶子节点)。
另外还提供了一些静态方法用于将选区序列化为 JSON 对象或将 JSON 对象反序列化为一个选区对象:
- static
Selection.fromJSON(doc, json)静态方法:反序列化一个给定的json对象,在文档doc(一个节点对象,根节点,表示文档)中构建一个选区Tip
ProseMirror 内置的
TextSelection文本选区子类,NodeSelection节点选区子类均有预设的静态方法subClass.fromJSON()而对于自定义的选区子类,该静态方法也必须自己来定义
- static
Selection.jsonID(id, selectionSubClass)静态方法:为 Selection 子类selectionSubClass注册一个唯一的标识符。其返回值是该选区子类
第一个参数id是一个字符串,用以表示该选区子类,这个标识符需要是唯一的且有区分度(不容易与其他模块序列化为 JSON 对象时的名称冲突);第二个参数是对应的选区子类
这个标识符会用于将选区子类序列化为 JSON 对象时,作为 JSON 对象的属性type的值。这样在调用上一个静态方法Selection.fromJSON()时,ProseMirror 会基于标识符将 JSON 对象反序列化为对应的选区示例
该类给出了一些抽象方法和具体的方法:
$anchor属性:一个 resolvedPos 对象,表示该选区的锚点,选区锚定「不动」的一端,即框选开始时光标的位置$head属性:一个 resolvedPos 对象,表示该选区的动点,即框选结束时光标的位置,如果选区变化会在这一侧发生ranges属性:一个数组(一般仅含有一个元素的),该元素是一个 SelectionRange 对象,它表示选区所对应于文档中的范围anchor属性:一个数字(符合 index schema 规则,表示文档的位置),表示该选区的锚点的位置head属性:一个数字(符合 index schema 规则,表示文档的位置),表示该选区的动点的位置$from属性:一个 resolvedPos 对象,表示该选区所覆盖的文档范围的 lower boundary 下界(在文档中较前的位置)$to属性:一个 resolvedPos 对象,表示该选区所覆盖的文档范围的 upper boundary 上界(在文档中较后的位置)from属性:一个数字(符合 index schema 规则,表示文档的位置),表示该选区所覆盖的文档范围的 lower boundary 下界的位置to属性:一个数字(符合 index schema 规则,表示文档的位置),表示该选区所覆盖的文档范围的 upper boundary 上界的位置empty属性:一个布尔值,表示该选区是否包含内容- abstract
eq(selection)抽象方法:返回一个布尔值,表示当前选区与给定的选区selection是否相等 - abstract
map(doc, mapping)抽象方法:通过一个mappingmappable 对象将选区映射到新文档doc上(doc是一个节点对象,根节点,表示一个文档),返回映射后的选区对象提示
可以通过
tr.doc获取到新文档 content()方法:获取该选区的内容,并构建为一个 slice 切片对象replace(tr, content?)方法:用给定的内容content(一个 slice 切片对象)替换当前选区(选中的内容)。如果第二个(可选)参数content没有指定,则将当前选区删除。该操作步骤会 append 添加到给定的tr事务中替换后的光标位置
替换后会自动将新的选区(光标状态)设置在插入的内容的后面。
如果插入的内容是一个 inline 节点,则该节点后面(向右)寻找合适的选区位置。如果不是 inline 节点,则向左寻找。
replaceWith(tr, node)方法:用给定的node节点对象替代当前选区(选中的内容)。该操作步骤会 append 添加到给定的tr事务中提示
对于编辑器存在多个选区范围的情况,如果调用以上方法替换选中的内容,则除了第一个选区范围会替换插入给定的节点,而其他选区范围所选中的内容都会被删除掉
而替换操作采用的是 transform 对象的方法
replaceRangeWith实现的,ProseMirror 可能会自动调整替换范围(例如当选区的 from 和 to 相同且都位于父级节点的起始或结尾位置,而给定的 node 并不适合此位置时,则可能将给定的 node 放置到合适的祖先节点里,以满足 schema 的约束)- abstract
toJSON()抽象方法:将选区转换为 JSON 对象。提示
在子类中给出该方法的具体实现时,需要确保生成的 JSON 对象中具有属性
type它值是该选区子类的jsonID(使用方法Selection.jsonID()所注册的唯一 ID),以表示该 JSON 对象代表哪一种 Selection 子类例如 ProseMirror 的内置选区子类
TextSelection文本选区,调用其toJSON()方法获得的 JSON 对象会包含一个type属性,其值是text,因为该内置选区子类在注册时将其 ID 设置为Selection.jsonID("text", TextSelection) getBookmark()方法:获取该选区的标签,返回一个对象(它符合一种 TypeScript interface SelectionBookmark)来表示该选区,将其看作是选区的一个更「轻量级」的替代者,以便保存到历史记录中SelectionBookmark
TypeScript interface
SelectionBookmark用于约束一个对象,它作为选区的标签,可以将其理解为选区的一个「替身」,一般用于实现编辑器的历史记录功能,追踪选区的变化和恢复选区。该对象需要具有以下属性和方法map(mapping)方法:通过一个位置映射管道mapping的处理,更新当前的选区标签,返回一个经过映射的selectionBookmark对象resolve(doc)方法:将该选区标签解析为给定文档doc(一个节点对象,根节点,表示文档)的一个选区对象注意
在该方法的具体实现代码里,可能需要做一些错误检查,因为经过位置映射管道 mapping 映射处理,如果该选区标签变得不可用的话,则需要为该解析操作设置 fall back 后备方案,通常将文本选区的静态方法
TextSelection.between()所生成的文本选区,作为标签解析所得的默认选区
该对象可以让选区信息更易于存储,而且可以在不直接访问 doc 文档内容的情况下对其进行映射转换,然后在需要时可以在给定的文档中将标签 resolve 解析为真正的选区。
该方法默认实现是将当前选区先转换为一个文本选区,然后调用文本选区的相应方法生成标签。也可以为不同类型的选区子类自定义该方法,以生成不同的标签,让历史记录支持存储更丰富的选区类型visible属性:一个布尔值,以控制在创建该类型的选区时,是否对用户可见,默认值为true
TextSelection 子类
TextSelection 类继承自 Selection 类,这种类型的选区是富文本编辑器中最常见的,它表示一个文本选区,即该选区的两端都需要在 textblock 节点里(它的选区范围也可以是空的,即处于光标状态)
通过方法 new TextSelection($anchor, $head?) 进行实例化,其中参数 $anchor 和 $head 都是一个 resolvedPos 对象,分别表示选区的锚点和动点。如果第二个(可选)参数 $head 省略,则它采用 $anchor 作为其默认值,即生成的文本选区处于光标状态
也可以使用该类所提供的一些静态方法进行实例化:
- static
TextSelection.create(doc, anchor, head?)静态方法:为文档doc(一个节点对象,根节点,表示文档)创建一个文本选区。参数anchor和head都是一个数字(符合 index schema 规则,表示文档的位置),分别表示选区的锚点和动点。如果第三个(可选)参数head省略,则它采用anchor作为其默认值,即生成的文本选区处于光标状态 - static
TextSelection.between($anchor, $head, bias?)静态方法:基于给定的锚点$anchor和动点$head(它们都是一个 resolvedPos 对象)构建一个文本选区。
如果给定的位置不是在文本区域里,则会在附近进行寻找位置创建文本选区。第三个(可选)参数bias的正负值决定优先向哪个方向寻找文本选区,默认(正值)向左侧寻找,如果参数bias为负值则优先向左侧寻找。Tip
如果通过该方法没有找到适合的地方创建文本选区,则采用父类的静态方法
Selection.near()作为 fall back 备选方案来寻找选区
该类的实例 textSelection 是一个文本选区对象,除了继承父类 Selection 的一些属性和方法,还具有一个特有的属性 $cursor
属性 $cursor:如果该文本选区范围为空/无内容,则该属性值是一个 resolvedPos 对象,表示光标的位置;如果该文本选区具有内容,则该属性值为 null
NodeSelection 子类
NodeSelection 类继承自 Selection 类,它表示一个节点选区,即该选区需要选中单一的一个 node 节点
提示
在 schema 数据类型约束对象的节点配置中,如果某个节点类型的参数 selectable 设置为 true,则在文档中该类型的节点实例就可以被选中
在 NodeSelection 节点选区中,(从父类 Selection 所继承的)属性 from 和 to(都是一个数字,符合 index schema 规则,表示文档的位置)分别指向选中的 node 节点的前后位置,而属性 anchor 锚点(也是一个数字,符合 index schema 规则,表示文档的位置)与 from 相等,而属性 head 动点(也是一个数字,符合 index schema 规则,表示文档的位置)与 to 相等。
通过方法 new NodeSelection($pos) 进行实例化,参数 $pos 是一个 resolvedPos 对象,会基于在该位置的节点创建一个选区(由于该方法内部并不会对传入的参数进行可用性验证,所以这里传入的位置需要是 resolved 解析后的形式,来确保它包含一个节点)
也可以使用该类所提供的一些静态方法进行实例化:
- static
NodeSelection.create(doc, from)静态方法:基于给定的位置from(一个数字,符合 index schema 规则,表示文档的位置)在文档doc(一个节点对象,根节点,表示文档)中创建一个节点选区 - static
NodeSelection.isSelectable(node)静态方法:返回一个布尔值,判断给定的node节点对象是否可被选中
该类的实例 nodeSelection 是一个节点选区对象,除了继承父类 Selection 的一些属性和方法,还具有一个特有的属性 node 表示该选区选中的节点对象
AllSelection 子类
AllSelection 类继承自 Selection 类,它表示一个全选选区,即该选区的范围是整个文档
Tip
由于很多时候不能通过 TextSelection 类的实例对象来表示全文档选区,例如在文档的开头或结尾处有一个叶子节点(而不是视为文本用文本节点对其进行选择),所以需要用另外一种不同类型的选区来表示
通过方法 new AllSelection(doc) 进行实例化,参数 doc 是一个表示文档的 node 节点对象
GapCursor 子类
提示
GapCursor 子类由 prosemirror-gapcursor 模块导出
使用该模块时,一般还需要在页面导入该模块所提供的 style/gapcursor.css 样式文件,以便在页面上展示相应的「虚拟」光标(一个短的不断闪烁的横条)
文档中有某些位置是不允许普通正常的选区聚焦定位的,例如块级的叶子节点(图像节点 ❓ )、表格节点、文档的起始位置和结尾位置,该模块提供了一种新的选区类型 GapCursor 可以定位到这些特殊的位置(光标状态)
通过方法 new GapCursor($pos) 进行实例化,参数 $pos 是一个 ResolvedPos 对象,表示文档中的位置(由于 GapCursor 选区一般表示处于光标状态的选区,所以该类型的选区的锚点 $anchor 和动点 $head 都是一样的)
注意
默认情况下 GapCursor 选区只能定位到内容为文本 textblock(通过 schema 的 content 限制)的节点里,如果其他类型的节点也希望 GapCursor 在其中定位,可以在节点的配置对象 NodeSpec 中添加一个属性 allowGapCursor,将其值设置为 true(则该类型的节点里任何地方都可以放置 GapCursor 选区)
推荐使用插件
另外可以通过调用该模块所导出的方法 gapCursor() 创建一个插件,将插件注册到编辑器上就可以自动捕获用户通过点击或按下方向键移动光标位置,(如果这个地方不允许创建一个正常的选区)并创建一个 GapCursor 选区实例,页面上表示「虚拟」光标的元素会带有 class 类名 ProseMirror-gapcursor
可以加载该模块所提供的 style/gapcursor.css 样式文件 对该元素应用官方预设的样式,也可以基于 class 类名为该元素应用自定义的样式
Plugin 类
ProseMirror 支持插件系统来扩展编辑器的功能。
它是 state 编辑器状态对象的一部分,可以影响编辑器的 state 和 view(即为编辑器状态添加额外的字段,以及控制视图如何响应用户操作)
通过 new Plugin(spec) 实例化一个插件对象,其中配置参数 spec 是一个对象(它需要符合一种 TypeScript interface PluginSpec)
PluginSpec
TypeScript interface PluginSpec 描述插件可接受哪些配置参数
props(可选)属性:一个对象(它需要符合一种 TypeScript interfaceEditorProps,具体可以参考 prosemirror-view 模块),它包含一些用于配置编辑器视图的属性,即插件通过该属性设置一些事件处理函数 event-handling functions 来响应用户的操作
如果属性值以函数(最后返回一个符合EditorProps接口约束的对象)的形式来设置,则函数内的this指向该插件实例state(可选)属性:一个对象(它需要符合一种 TypeScript interfaceStateField),它用于为该插件设置其专属的 state(与之相对应的是编辑器状态)StateField
TypeScript interface
StateField描述该插件所特有的状态,以及如何进行状态值的初始化,如何应用事务 transaction 变更状态,如何将其序列化为 JSON 对象,以及如何从 JSON 对象中反序列化为插件的状态对象以下列出的方法中
this都指向当前的插件实例init(config, instance)方法:初始化插件的状态值。其返回值可以是任何数据类型
第一个参数config是编辑器的配置信息(即使用方法EditorState.create(obj)初始化编辑器时所传递给的配置对象,它需要符合一种 TypeScript interfaceEditorStateConfig)
第二个参数instance是编辑器状态对象注意
这里的编辑器状态对象并不「完整」,因为编辑器的插件是依序注册的,所以该编辑器状态对象
instance并不包含在当前插件之后才注册的插件的信息,所以这是一个半初始化的编辑器状态对象apply(tr, value, oldState, newState)方法:更新插件状态。将给定的事务tr应用到插件的状态,以生成一个新的状态。第一个参数tr是一个transaction事务对象;第二个参数value是该插件的当前状态值(即旧的状态值,还没有根据tr进行更新);第三个和第四个参数oldState和newState分别是应用事务前后的编辑器状态对象
其返回值是应用完事务后插件的状态值(它与插件状态初始值的数据类型相同)注意
由于插件的状态也是编辑器状态对象的一部分,所以它也是 immutable 不可变的
则该方法返回的值是作为一个新的状态,而不能直接修改原有的状态
这里的
newState也是一个半初始化的编辑器状态对象提示
在 transaction 事务上增加一些
metadata额外的信息通常是比较有用的,可以让插件按需执行特定的操作例如执行一个 undo 操作的时候, 为相应的 transaction 事务添加一个标记, 当插件检测到这个标记的时候, 将这个 transaction 事务特殊对待, 插件将会移除 undo stack 顶部的 item, 同时增加这个 transaction 事务到 redo stack(而不是执行一般的修改状态值的操作)
js// 该插件的作用是统计编辑器所触发的 transaction 事务的数量 let transactionCounter = new Plugin({ state: { init() { return 0 }, apply(tr, value) { // 获取当前事务的元信息 // 如果存在则不执行操作,直接返回当前值 if (tr.getMeta(transactionCounter)) return value // 如果不存在额外添加的元信息,则执行默认操作(将统计值增加 1) else return value + 1 } } }) // 如果执行一个 undo 操作时,为相应的 transaction 事务设置相应的元信息 function markAsUncounted(tr) { tr.setMeta(transactionCounter, true) }toJSON(value)(可选)方法:将插件的状态序列化为一个 JSON 对象(如果不设置该方法,则该插件的状态值无法序列化为 JSON)。参数value是插件当前的状态值fromJSON(config, value, state)(可选)方法:将 JSON 对象反序列化为插件的状态。
第一个参数config是编辑器的配置信息(即使用方法EditorState.create(obj)初始化编辑器时所传递给的配置对象,它需要符合一种 TypeScript interfaceEditorStateConfig)
第二个参数value是任意值 ❓
第三个参数state是编辑器状态对象注意
这里的
state也是一个半初始化的编辑器状态对象
如果属性值以函数(最后返回一个符合 TypeScript interfaceStateField约束的对象)的形式来设置,则函数内的this指向该插件实例key(可选)属性:一个pluginKey对象,作为该插件的标识符。编辑器所注册的插件中,它们的标识符是不能重复的,即每一个带有标识符的插件,它们的标识符都应该是唯一的。可以仅通过标识符(而不需要访问插件实例对象)获取相应插件的配置、特有状态。PluginKey
通过
new PluginKey(name?)创建一个插件标识符对象,(可选)参数name是一个字符串用于设置名称(默认值是key)该对象具有以下方法和属性:
get(state)方法:从给定的编辑器状态对象state中获取该标识符所对应的插件。参数state是编辑器状态对象。其返回值是相应的插件实例plugin(也可能无法找到,则返回undefined)getState(state)方法:从给定的编辑器状态对象state中获取该标识符所对应的插件的特有状态值。其返回值是相应插件的特有状态值(也可能无法找到相应的插件,则返回undefined)
view(可选)属性:一个函数fn(view)(入参view是一个 editorView 编辑器视图对象)。当插件的特有的状态与编辑器的视图对象相关时,该函数就会被调用(该插件会在编辑器上添加可视元素,或可以与编辑器的视图进行交互)。该函数返回值是一个pluginView插件视图对象PluginView
pluginView插件视图对象具有以下(可选)属性:update(可选)属性:一个函数fn(view, preState)每当编辑器的视图更新时,会调用该函数。参数view是(更新后)editorView 编辑器的视图对象;参数preState是(更新前)编辑器状态对象destroy(可选)属性:一个函数fn()当编辑器的视图被销毁时,或当编辑器的状态对象被重新配置而plugins属性不包含该插件时,该函数被调用
filterTransaction(可选)属性:一个函数fn(transaction, state)用于筛选控制哪一些事务可以应用到编辑器的状态中。该函数的第一个参数transaction是一个事务对象,第二个参数state是编辑器状态对象。该函数会在事务应用到编辑器状态前调用,以筛选控制哪些事务可以起作用。如果返回值是false则取消该事务,不会应用到编辑器的状态中。appendTransaction(可选)属性:一个函数fn(transactions, oldState, newState),返回值是一个transaction事务对象。可以让该插件添加一个事务(到即将应用到编辑器状态对象中的一系列事务transactions的末尾)。
第一参数transactions是一个数组(由一系列即将应用到编辑状态对象的事务构成)
第二、第三个参数oldState和newState分别是更新前后的编辑器状态对象说明
当另一个插件又附加了一个 transaction,而且是在当前 plugin 之后调用的,则当前 plugin 的该函数会再调用一次(使用新的 state)。但是仅含新的 transaction,即它不会再将之前处理过的 transaction 再处理一次。
Plugin 类的实例化对象表示一个 plugin 对象,以下称作「插件对象」
用于何处
插件对象可以在实例化编辑器状态对象时使用,它作为静态方法 EditorState.create(config) 的参数对象的属性 config.plugins(一个数组)的元素
也可以在实例化编辑器视图时使用,它作为 new EditorView(place, props) 的参数对象的属性 props.plugins(一个数组)的元素
const myPlugin = new Plugin({
props: {
handleKeyDown(view, event) {
console.log("A key was pressed!")
return false // We did not handle this
}
}
});
const state = EditorState.create({schema, plugins: [myPlugin]});
// 或者在实例化编辑器视图 view 时添加插件
// const view = new EditorView(editorContainerDOM, {
// plugins: [myPlugin]
// });
注意
在实例化编辑器状态 state 时添加插件更为常见,因为通过(实例化 view 时)视图所添加的插件有一定的条件限制的
应用到编辑器视图上的插件(其配置对象)不能具有 state 字段、filterTransaction 字段或 appendTransaction 字段,否则会抛出错误
具有上述字段的插件只能(在实例化 state 时)应用到编辑器状态上
虽然可以将插件都注册到 state 上(由于注册到 view 上的插件有一定的限制),但是还是推荐根据插件的用途将它们分开,可以按照以下的方式来考虑:
- 如果在实例化插件
new Plugin(spec)时,在配置对象spec中只设置了属性props和(或)属性view,它们一般需要访问编辑器视图对象,所以将这些插件注册到 view 上提示
属性
props的值是一个对象(需要符合一种 TypeScript interfaceEditorProps),其中大量的属性以handler为前缀的属性,是用于为相应事件设置处理函数,这些事件处理函数一般都会接受编辑器视图作为参数属性
view的值是一个函数fn(view),一般用于为编辑器视图添加 DOM - 如果在实例化插件
new Plugin(spec)时,在配置对象spec中设置了state属性和(或)属性filterTransaction和(或)属性appendTransaction只能将这些插件注册到 state 上
关于为何要在两个地方分开注册插件,具体可以参考这个论坛的帖子,以及这个 ProseMirror 的提案,主要是实现插件对于 state 和 view 的依赖分离,便于将编辑器状态 state 与外部(前端框架层面的)状态管理相结合
plugin 插件对象包含一些属性和方法:
spec属性:一个对象(它符合一种 TypeScript interfacePluginSpec),表示该插件的详细配置props属性:一个对象(它符合一种 TypeScript interfaceEditorProps),该插件对于编辑器视图的配置(就是在spec.props里设置的一系列事件处理函数)getState(state)方法:获取该插件的特有的状态(该值是从编辑器的状态对象中抽取的,由于插件的状态也是编辑器状态对象的一部分)。入参state是编辑器状态对象