Lexical 核心模块
核心模块 lexical 是一个不依赖其他第三方库 dependency-free 的文本编辑引擎
参考
- lexical - Github
- lexical - documentation
Lexical 的引擎提供/实现三个主要部分:
- 编辑器实例,每个实例依附于一个的可编辑的
contenteditable元素 - 一组编辑器状态,它们表示任意时刻的编辑器 current 当前状态和 pending 待定状态。
- 一个 DOM reconciler 协调器,它获取一组编辑器状态,计算差异并根据状态(按需)更新 DOM
最小化原则
核心模块试图尽可能地实现最小化,即不会直接关注编辑器的具体实现,例如 UI 组件、工具栏或者富文本功能和 markdown 的支持等。
相反,这些功能的逻辑可以通过插件接口引入,在需要的时候再使用。这确保了极好的可扩展性,并使代码体积最小化,确保应用只为实际使用的功能载入所需的代码。
类
核心模块提供了一些基础类,可以把它们作为父类进行拓展实现更丰富的功能
编辑器
LexicalEditor 类的实例表示一个编辑器,该类的实例化对象(以下简称为 editor)提供了一些实用的方法,在后续的小节列出
源文件
该类在核心模块的 LexicalEditor.ts 文件中进行定义
该文件提供了一个方法 createEditor(config?) 它对编辑器的实例化过程 new LexicalEditor() 进行了封装,支持传递一个(可选)参数 config,用于预设配置编辑器,最终返回一个编辑器实例
参数 config(一个对象)可以配置以下属性:
- 属性
disableEvents:是否取消响应用户的输入,默认为false即不取消 - 属性
editable:是否可编辑 - 属性
editorState:以一个给定的编辑器状态进行初始化 - 属性
namespace:设置命名空间(为了区分嵌套式的编辑器 ❓) - 属性
nodes:一个数组,包含编辑器需要使用到的自定义节点,相当于为编辑器引入这些自定义节点的 schema - 属性
onError:错误处理函数 - 属性
parentEditor:父编辑器 ❓ - 属性
theme:主题,为节点相应的 DOM 元素添加 Class 类名
根元素
方法 editor.setRootElement(domElement) 为编辑器绑定一个 DOM 元素 domElement(需要具有 contenteditable 属性)作为编辑器的根元素
聚焦与可编辑性
editor.blur()移除/取消编辑器的焦点状态,使编辑器失去焦点editor.focus(callbackFn?, options?)使编辑器获得焦点,响应用户输入
可以接受两个可选参数:callbackFn是编辑器获得焦点后会触发的回调函数options是一个对象,用于设置聚焦后的光标位置是在编辑器的开头还是结尾
其默认值为{}空对象,会将光标置于结尾
可以通过传入一个对象{ defaultSelection: 'rootStart' }或{ defaultSelection: 'rootEnd' }来显式指定光标的位置
editor.setEditable(booleanValue)设置编辑器的可编辑状态,通过传入的一个布尔值booleanValue来设置可编辑状态。如果传入的是false编辑器就不会响应用户的输入说明
用方法
editor.setEditable(false)将编辑器切换到只读模式时,并没有改变页面 RootElement 根元素的contenteditable的值,即用户依然可以与页面上的 DOM 元素交互(输入文字),只是 Lexical 并不会对其作出响应(取消了侦听用户事件)
获取编辑器信息
editor.getDecorators()获取编辑器中所含有的所有 DecoratorNode 装饰节点。返回值是一个映射,其中每一个键为节点的 key 唯一标识符,相应的值为节点对象editor.getEditorState()获取编辑器当前的状态editor.getKey()获取编辑器的唯一标识符,由字符串表示,在创建编辑器实例时自动生成editor.getRootElement()获取编辑器实例的根元素。返回值是null(如果编辑器未挂载到一个可编辑的 DOM 元素上)或一个 HTML 元素editor.isComposing()判断编辑器当前正处于「组合」输入模式。
如果通过 IME 或第三方扩展接收输入时(最终输出的内容并不是键盘上的字母,而是转换为其他语言符号)则返回true;否则就返回falseeditor.isEditable()判断编辑器是否支持编辑,返回一个布尔值
编辑器状态相关
editor.toJSON()返回当前编辑器的序列化形式(JSON 对象,该对象只有一个属性{ editorState }主要包含编辑器的状态)提示
以上方法返回的是一个对象,如果要通过网络传递或保存到数据库中,还需要对返回的结果
jsonObj调用JSON.stringify(jsonObj)转换为字符串形式editor.parseEditorState(maybeStringifiedEditorState, updateFn?)将传入的序列化形式的编辑器状态(一般为 JSON 字符串格式)maybeStringifiedEditorState反序列化为编辑器状态对象;第二个(可选)参数updateFn是在反序列化的同时所执行的编辑器更新操作editor.setEditorState(editorState, options?)将传入的editorState编辑器状态(对象)应用到编辑器中,相当于触发一次编辑器更新,使用传入的编辑器状态覆盖当前的内容;第二个(可选)参数options是一个对象{ tag?: string }为这类型的更新设置一个标记,它会被传递给编辑器的 Update Listener 更新监听器,这样就可以针对特定类型的编辑器更新设置一些响应操作
说明
以上的三个方法一般配合使用:
- 先通过方法
editor.toJSON()保存当前编辑器状态 - 等到需要恢复历史记录时,从远端服务器获取以前保存的(经过序列化的)编辑器状态
- 先通过方法
editor.parseEditorState()反序列化 - 再通过
editor.setEditorState()将以前的编辑器状态加载到当前的编辑器中
editor.update(updateFn, options?)对编辑器状态进行更新。
第一个参数updateFn是一个函数,在其中编写对编辑器状态执行的代码逻辑,这是唯一可以变更 mutated 编辑器状态 Editor State 的地方
第二个(可选)参数options是一个对象,用于配置更新的行为,它可以设置以下属性- 属性
onUpdate:一个函数,会在当前更新完成后执行,常见的使用场景是在编辑器更新完成后执行同步操作 - 属性
skipTransforms:一个布尔值,如果设置为true则在当前更新流程中会忽略所有节点转换器 node transform - 属性
tag:一个字符串,为这类型的更新设置一个标记 ❓ 它会被传递给编辑器的 Update Listener 更新监听器,可以针对特定类型的编辑器更新设置一些响应操作 ❓
- 属性
节点相关
editor.getElementByKey(key)根据节点的 key 唯一标识符获取页面上相应的 DOM 元素。返回值是null或一个 HTML 元素editor.hasNode(nodeClass)判断给定的节点类型nodeClass是否已经注册到编辑器上(即编辑器是否支持特定的节点类型)。返回一个布尔值editor.hasNodes(nodesClass)判断给定的一系列节点类型nodesClass(数组)是否都已经注册到编辑器上。返回一个布尔值
响应性
调用以下各个方法注册监听器以编辑器的特定的操作,与此同时这些方法都会会返回一个函数,调用它会取消所注册的监听器
editor.registerEditableListener(callbackListener)为编辑器的可编辑性变化注册一个监听器/回调函数注意
需要使用
editor.setEditable()方法进行可编辑状态的切换不能直接使用 JS(原生方法)直接更改根元素的
contenteditable属性,因为这样是无法触发以上设置的监听器的editor.registerRootListener(callbackListener)为编辑器所绑定的根元素的变更注册一个监听器/回调函数editor.registerUpdateListener(callbackListener)为编辑器的状态更新注册一个监听器/回调函数。
回调函数callbackListener接受一个对象作为参数,它具有以下属性(包含了当前更新的一些信息)- 属性
dirtyElements:一个映射,以键值对的形式列出了当前更新流程中被标记为 dirty 的节点,其中键为节点的 key 唯一标识符,值则是一个布尔值以表示是否特意标记为 dirty ❓ - 属性
dirtyLeaves:一个集合,包含所有被标记为 dirty 的叶子节点(以节点的 key 唯一标识符来表示节点) - 属性
editorState:当前的编辑器状态 - 属性
normalizedNodes:一个集合,经过标准化的节点 ❓(以节点的 key 唯一标识符来表示节点) - 属性
prevEditorState:更新前的编辑器状态 - 属性
tags:(字符串)集合,用于标记特定类型的更新
- 属性
editor.registerTextContentListener(callbackListener)为编辑器的文本内容变更注册一个监听器/回调函数editor.registerCommand(commandType, callbackListener, priority)在编辑器上为一种 Lexical 指令类型commandType注册一个监听器/回调函数callbackListener,并设置该回调函数的优先级priority(使用数字来表示,有五个级别0、1、2、3、4,数值越大优先级越高,如果针对同一种指令类型的两个回调函数同时具有相同的优先级,会按照它们在代码中的注册顺序依次执行)提示
在该核心模块中,Lexical 导出了 5 个关于指令优先级的变量
COMMAND_PRIORITY_EDITOR相当于数值 0COMMAND_PRIORITY_LOW相当于数值 1COMMAND_PRIORITY_NORMAL相当于数值 2COMMAND_PRIORITY_HIGH相当于数值 3COMMAND_PRIORITY_CRITICAL相当于数值 4
这 5 个变量分别对应于 5 个级别的优先级,使用这些变量(而不是直接用数字)指定指令的优先级,会让代码更有语义,便于阅读和后期的维护
editor.dispatchCommand(commandType, payload)分发指令commandType同时传递数据payload给相应的回调函数。返回一个布尔值,以表示指令是否处理完成(该值其实由回调函数的返回的布尔值值所决定)
说明
以上两个方法配合使用,先在编辑器上注册 register 好特定指令的回调函数,待特定类型的指令被分发 dispatch 时,编辑器会按照优先级别执行相应的回调函数
其中监听器/回调函数返回一个布尔值,以表示是否阻止指令的「冒泡」 propagation,如果返回 true 表示指令已经处理完成,则不会「冒泡」而触发下一个针对相同类型的指令所注册的回调函数
editor.registerDecoratorListener(callbackListener)为装饰节点注册监听器/回调函数callbackListener,当编辑器中的任何一个装饰节点发生改变时,该回调函数会被执行。
回调函数会接受一个参数,它是一个对象(映射),包含编辑器中的所有装饰节点(以键值对的方式表示,键为节点的 key 唯一标识符,值为相应的装饰节点)提示
由于装饰节点一般用于对接其他前端框架,所以在回调函数中一般会配合/编辑其他前端框架的代码
editor.registerMutationListener(lexicalNodeClass, callbackListener)设置节点变更监听器/回调函数。第一个参数lexicalNodeClass指定需要针对哪一类节点,第二个参数callbackListener是回调函数,当特定类型的节点发生变化时,就会触发该回调函数执行。
监听器/回调函数的入参是一个映射,里面所包含的元素就是发生了变化的节点,其键名是该节点的 key 唯一标识符(对于非根节点,以数字表示;根节点以root表示),其值是该节点的变化类型(created|destroyed|updated三个值之一)提示
一个应用场景是在特定类型的节点被创建时,为对应的 DOM 元素添加事件监听器。
可以在
editor.registerMutationListener()的回调函数中筛选出created类型的变化,并通过editor.getElementByKey(nodeKey)来获取节点所对应的 DOM 元素,再用 JS 原生方法为 DOM 添加事件监听器editor.registerNodeTransform(lexicalNodeClass, callbackListener)为特定类型的节点注册节点转换器。第一个参数lexicalNodeClass指定需要针对哪一类节点,第二个参数callbackListener是回调函数,当特定类型的节点被标记为 dirty 时,就会触发该回调函数执行。
回调函数的入参是(该类型节点中)发生改变的节点说明
节点转换 node transform 是指当某个节点(发生变化后)符合特定的规则时,会触发相应的转换/操作
当一个节点在一次编辑器的更新 Update 中被标记为 dirty 「脏」时,相应的节点转换器会被调用,直到节点被移除 dirty 标记。在 Lexical 中并没有具体的机制可以保证节点转换器按特定的顺序执行
在转换规则中,必须要设置合理的 preconditions 预设/判断条件,以避免进入无限循环的节点转换。
编辑器状态
EditorState 类的实例表示一个编辑器的状态,该类的实例化对象(以下简称为 editorState)提供了一些实用的方法,在后续列出
源文件
该类在核心模块的 LexicalEditorState.ts 文件中进行定义
提示
除了可以通过 new EditorState(nodeMap, selection?)(根据给定的节点树 nodeMap 它是一个映射,记录了各种节点的内容和关系)实例化该类,还可以调用编辑器对象的方法 editor.getEditorState() 获取当前的编辑器状态
editorState.clone(selection?)返回一个当前编辑器状态的克隆(返回的编辑器状态处于只读 readOnly 模式),可以传入一个选区selection作为参数以手动设置选中的内容editorState.isEmpty()判断编辑器内容是否为空(即编辑器状态的节点数中只有一个根节点 RootNode)editorState.read(callbackFn)在当前编辑器状态下执行给定的回调函数callbackFn一般用于调试 ❓editorState.toJSON()返回当前编辑器状态的序列化形式(JSON 对象,该对象只有一个属性{ root }主要包含根节点及其后代节点的状态)
节点类
Nodes 节点是 Lexical 编辑器的核心,各种不同类型的节点是用户与编辑器交互的基本单位,构成了编辑器的可视化交互界面;同时这些节点是抽象数据模型的实例,构成了 Editor State 编辑器状态。
最核心的节点是 LexicalNode,在该模块中 Lexical 对其进行扩展,构建出 5 个基础节点
ElementNode元素节点TextNode文本节点DecoratorNode装饰节点RootNode根节点(继承自ElementNode元素节点)LineBreakNode换行节点
该模块包导出了其中的三个节点 ElementNode、TextNode、DecoratorNode 以便开发者进一步扩展
例如在该模块就通过扩展这些基础节点,提供了一些子类
ParagraphNode段落节点:继承自ElementNodeTabNode缩进节点:继承自TextNode
LexicalNode 核心节点
核心节点 LexicalNode 所提供的方法一般都是比较通用的,便于被所有的子孙节点所继承,操作各种类型的节点
源文件
该类在核心模块的 LexicalNode.ts 文件中进行定义
核心节点 LexicalNode 在核心模块的 LexicalNode.ts 文件中进行定义
说明
该类一般不进行实例化而是用作父类,被扩展/继承为各种子类,例如 Lexical 在核心模块中就基于核心节点构建出 5 个基础节点
节点信息
node.getKey()获取节点的 key 唯一标识符提示
节点的唯一标识符
key的数据类型是字符串,但内容是数字(除了 RootNode 根节点的 key 为root)LexicalNode.getType()获取节点的类型/名称提示
该方法其实是 LexicalNode 类的静态方法,所有类型的节点都需要具有该方法
返回的节点类型需要具有唯一性,即与(注册在当前编辑器中)其他节点名称不同
node.isAttached()返回一个布尔值,表示该节点是否已经添加到编辑器中(可以沿着节点树回溯找到 RootNode 根节点),如果返回false(即节点并未插入到编辑器的节点树中),则 Lexical 在编辑器更新 DOM Reconciler 协调器不会对其进行处理,而且最终会被 Lexical 垃圾回收处理掉。node.is(otherNode)返回一个布尔值,比较给定节点otherNode与当前节点是否为同一个节点node.getLatest()从当前编辑器状态 editorState 中获取该节点的最新版本最新值
通过该方法可以确保所获得的节点值是最新的,而避免从以前的引用获取到旧的节点的值
node.getWritable()获取一个可更新 mutable 版本的节点注意
如果该方法在编辑器实例方法
editor.update(callbackFn)的回调函数以外调用以上方法,会抛出错误提示node.getTextContent()获取节点内的文本内容提示
该方法供其他自定义节点覆写,以指定在(纯)文本中哪些内容表示这种类型的节点(例如在复制粘贴时)
ElementNode 元素节点就覆写了该方法,获取其所有后代节点里所有 TextNode 文本节点,并将它们的内容拼接起来
node.getTextContentSize()获取节点内的文本的长度。其实是基于前一个方法const str = node.getTextContent()再通过str.length()得到字符串的长度node.isBefore(targetNode)返回一个布尔值,判断当前节点是否在给定节点targetNode之前node.isDirty()返回一个布尔值,表示该节点在当前更新流程中,是否被标记为 dirty 「脏节点」node.isSelected(selection?)返回一个布尔值,表示该节点是否在给定的选区selection中
操作节点
node.markDirty()手动将该节点标记为 dirty 「脏节点」,强制该节点在当前更新流程中被节点转换器 transform 以及 DOM reconciler 协调器处理node.remove(preserveEmptyParent?)移除该节点,第二个(可选)参数preserveEmptyParent是一个布尔值,表示移除该节点后,如果造成其父节点内容为空,是否还需要保留该父节点(默认值为false即移除内容为空的父节点)提示
可以通过设置父节点的方法
parentNode.canBeEmpty()来覆写子节点以上方法所设置的行为node.replace(replaceWithNode, includeChildren?)用给定的节点replaceWithNode替换当前节点,第二个(可选)参数includeChildren是一个布尔值,表示是否将原节点的内容(后代节点)转移到给定节点内LexicalNode.clone(_data)返回该节点的一份克隆,该拷贝会具有不同的 key 唯一标识符,用于复制粘贴 ❓提示
该方法其实是 LexicalNode 类的静态方法,所有类型的节点都需要具有该方法
LexicalNode.transform()为当前类型的节点设置一个节点转换器 transform,该方法是会在编辑器初始化时进行注册
该方法返回的是一个函数(node: LexicalNode) => void作为节点转换器提示
该方法其实是 LexicalNode 类的静态方法
为特定节点类型添加/注册节点转换器 transform 更常见的方式是使用编辑器对象的方法
editor.registerNodeTransform()
DOM 相关
node.createDOM(_config, _editor)该方法一般会被所继承的后代子类覆写,用于指定该类型的节点在页面上所对应的 DOM 元素结构。最后返回一个 HTML 元素
该方法会接受两个参数- 第一个参数
_config是编辑器的配置对象(一般用于获取_config.theme编辑器主题为相应的 DOM 元素添加 Class 类名) - 第二个参数
_editor是编辑器实例
注意
该方法会在 Lexical 使用 DOM Reconciler 协调器(更新页面上的元素)时被调用,请不要在该方法中修改/更新编辑器状态 editorState,会触发循环更新
该方法所返回的只能是单一个 HTML 元素,而不能具有嵌套节点 ❓
- 第一个参数
node.updateDOM(_prevNode, _dom, _config)当节点变化时,该方法会被调用,其作用主要是更新 DOM 元素,以便与节点的最新状态保持一致
该方法的返回值是一个布尔值,表示更新节点时,是否需要调用上一个方法node.createDOM()方法重新创建一个新的 DOM 元素来取代页面上原有的旧元素提示
该方法的返回值一般是
false,因为大部分节点在其生命周期中,所对应的 DOM 元素类型是一直不变的而对于节点某个属性变更后触发 DOM 元素类型变化,则该方法需要返回
true以触发页面的 DOM 元素重新渲染
导出导入相关
node.exportDOM(editor)该方法一般会被所继承的后代子类覆写,在导出节点时,将节点序列化为 DOM 元素(字符串)时会调用该方法。该方法会接受一个参数editor编辑器实例说明
将节点序列化为 DOM 元素导出,一般发生在复制粘贴时,为了兼容 Lexical 与其他文本编辑器,以及不同命名空间的 Lexical 编辑器之间进行内容的复制粘贴
该方法的返回值类型是DOMExportOutput(Typescript 类型)的对象ts// DOMExportOutput 类型(对象) // * 属性 element 其值就是转换生成的 HTMLElement // 但是 element 属性值也可以是 null,这样该节点在序列化为 HTML 时就会「丢失」不见了 // * 属性 after 是一个 hook,它是一个函数(以所生成的 element 作为入参),会在导出生成 HTML 元素后运行 // 因为有时候需要对转换生成的 HTML 元素进一步处理,Lexical 为此提供了一个 API,即可实现对 HTMLElement 的「后处理」 export type DOMExportOutput = { after?: (generatedElement: ?HTMLElement) => ?HTMLElement, element?: HTMLElement | null, };node.exportJSON()该方法一般会被所继承的后代子类覆写,在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。说明
将节点序列化为 JSON 格式导出,一般发生在相同命名空间的 Lexical 编辑器内/之间进行内容的复制粘贴
而且 JSON(对象)格式很容易序列化为字符串(使用 JS 原生方法
JSON.stringify(value)),这样就可以将让数据更容易地在网络中传输或保存到数据库中注意
该方法的返回值一个 JSON 对象,其中所包含的属性因不同类型的节点而异
但是所有序列化后的节点都需要确保具有
type字段和version字段 属性,以标注该 JSON 对象所对应的节点类型/名称以及版本LexicalNode.importJSON(_serializedNode)该方法一般会被所继承的后代子类覆写,将 JSON 数据反序列化为节点时调用的方法,返回一个该节点实例提示
该方法其实是 LexicalNode 类的静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 JSON 的节点)生成一个该节点的实例
父节点相关
node.isParentOf(targetNode)返回一个布尔值,判断当前的节点是否为给定节点targetNode的祖先节点node.getParent()获取父节点,如果没有则返回null提示
如果希望没有获取到父节点时,在终端抛出错误提示(而不是返回
null)可以使用另一个方法node.getParentOrThrow()该方法会在未获取到父节点时抛出
'Expected node {key} to have a parent.'错误提示node.getParents()获取祖先节点(直到 RootNode,包含根节点),返回一个数组(元素的顺序是按照祖先节点从里到外)node.getParentKeys()获取祖先节点的 key 唯一标识符(直到 RootNode,包含根节点),返回一个数组(元素的顺序是按照祖先节点从里到外)node.getCommonAncestor(otherNode)获取当前节点node与给定节点otherNode最近的共同祖先节点(ElementNode 元素节点),如果没有则返回null( ❓ 至少会返回 RootNode 根节点,除非其中一个节点就是根节点)node.getIndexWithinParent()获取节点相对于其父节点的排序位置(索引值从0开始)node.getTopLevelElement()获取该节点(沿着节点树回溯)最顶部的祖先(元素)节点,如果没有则返回null说明
最顶部的祖先节点并不包含根节点或影子根节点 ShadowRoot
影子根节点 ShadowRoot 是指一些节点的行为类似于根节点,一般表示局部的节点树结构的终点/树根,例如表格的单元格节点
TableCellNode就是影子根节点可以使用方法
$isRootOrShadowRoot(node)判断给定的节点node是否为根节点或影子根节点 ShadowRoot提示
如果希望没有获取到祖先节点时,在终端抛出错误提示(而不是返回
null)可以使用另一个方法node.getTopLevelElementOrThrow()该方法会在未获取到父节点时抛出
'Expected node {key} to have a top parent element.'错误提示node.isParentRequired()返回一个布尔值,表示该节点存在时,是否需要有父节点将其包裹,一般在复制粘贴时会配合调用以下的node.createParentElementNode()方法,以保证在复制粘贴过程中节点的完整性node.createParentElementNode()该方法一般会被所继承的后代子类覆写,用于创建父节点。返回一个ElementNode元素节点作为父节点说明
当节点的方法
node.isParentRequired()返回true时,在复制粘贴时会配合调用以上方法,以保证在复制粘贴过程中节点的完整性例如对于列表项节点,复制粘贴会包含相应的父节点(列表节点)
兄弟节点相关
node.getPreviousSibling()获取该节点的前一个兄弟节点,如果没有则返回nullnode.getPreviousSiblings()获取该节点之前的所有兄弟节点,返回一个数组,里面包含这些兄弟节点node.getNextSibling()获取该节点的下一个兄弟节点,如果没有则返回nullnode.getNextSiblings()获取该节点之后的所有兄弟节点,返回一个数组,里面包含这些兄弟节点node.getNodesBetween(targetNode)获取该节点和给定的目标节点targetNode之间的节点,返回一个数组,里面包含当前节点node和目标节点targetNode以及它们之间的节点注意
如果当前节点
node和目标节点并不在同一个父节点下则可以将数组中所列出的节点,想象为沿着节点树从
node节点到达targetNode所遇到的节点,即它们各自及其父节点,以及之间的节点(及其后代节点)都会被包含在其中node.insertBefore(nodeToInsert, restoreSelection?)将给定的节点nodeToInsert插入到当前节点的前面(作为前一个兄弟节点),第二个(可选)参数restoreSelection是一个布尔值,以控制是否在插入节点之后,重新调整选区(将光标移到回当前节点的后面,在视觉上的效果就是光标保持不动)node.insertAfter(nodeToInsert, restoreSelection?)将给定的节点nodeToInsert插入到当前节点的后面(作为下一个兄弟节点),第二个(可选)参数restoreSelection是一个布尔值,以控制是否在插入节点之后,重新调整选区(将光标移到所插入节点的后面)node.selectPrevious(anchorOffset?, focusOffset?)将选区移动到上一个兄弟节点,第一个(可选)参数anchorOffset用于设置选区的锚点/固定点位置,第二个(可选)参数focusOffset用于设置选区的焦点/可移动点的位置node.selectNext(anchorOffset?, focusOffset?)将选区移动到下一个兄弟节点,第一个(可选)参数anchorOffset用于设置选区的锚点/固定点位置,第二个(可选)参数focusOffset用于设置选区的焦点/可移动点的位置
ElementNode 元素节点
元素节点 ElementNode 的作用是充当其他节点的父节点(即可以包含其他子节点),可以理解为容器节点。
它既可以是块状节点(ParagraphNode 段落节点、HeadingNode 标题节点等),也可以是行内节点(如 LinkNode 链接节点)。
源文件
该类在核心模块的 nodes/LexicalElementNode.ts 文件中进行定义
通过 new ElementNode(key?) 进行实例化(以下简称为 elementNode),但该节点一般都不进行实例化,而是用作父类,成为其他自定义节点的「模板」
该节点扩展自 LexicalNode 核心节点,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
elementNode.canBeEmpty()返回一个布尔值,用于设置元素节点是否允许内容为空的情况下依然存在提示
如果子节点
node通过调用方法node.remove(false)来移除自身时,造成父节点(一般是 ElementNode 元素节点)内容为空,则会同时删除父节点而以上方法如果返回
true则会覆写子节点的行为,保留内容为空的父节点elementNode.canExtractContents()返回一个布尔值,用于设置是否可以提取该节点的(文本 ❓ )内容提示
该方法可能 ❓ 是用于区分
TableNode表格节点的,该节点不允许提取节点的文本内容(会丢失格式)elementNode.canIndent()返回一个布尔值,用于表示元素节点是否允许设置缩进elementNode.canInsertAfter(targetNode)返回一个布尔值,用于表示是否允许在该节点的后面插入目标节点targetNode(作为兄弟节点)提示
使用该方法可以对兄弟节点进行约束
例如可以使用该方法来限制在列表项节点的后面只能插入的也是列表项节点
elementNode.canInsertTextBefore()返回一个布尔值,用于表示如果光标在节点最前面时继续输入文字的行为方式。如果返回true(默认值)则文字会插入 prepend 到节点内(作为节点的内容);如果返回false则会紧挨着链接节点创建一个段落节点(作为兄弟节点)来容纳新输入的文本提示
例如对于
LinkNode链接节点覆写了该方法,让返回值为false所以在链接前面继续输入文本,并不会纳入到链接节点的范围中,可以在 Playground 尝试与链接节点进行交互,看看具体效果
elementNode.canInsertTextAfter()返回一个布尔值,用于表示如果光标在节点最后面时继续输入文字的行为方式。如果返回true(默认值)则文字会插入 append 到节点内(作为节点的内容);如果返回false则会紧挨着链接节点创建一个段落节点(作为兄弟节点)来容纳新输入的文本提示
例如对于
LinkNode链接节点覆写了该方法,让返回值为false所以在链接后面继续输入文本,并不会纳入到链接节点的范围中,可以在 Playground 尝试与链接节点进行交互,看看具体效果
elementNode.canMergeWith(targetNode)返回一个布尔值,用于表示当前节点是否可以与给定的节点targetNode进行合并提示
一般返回
false即两个节点并不会融合但可以对该方法进行覆写,例如列表项节点可以与特定类型的节点/内容融合,以简化列表结构
elementNode.canReplaceWith(targetNode)返回一个布尔值,用于表示当前节点是否可以被给定的节点targetNode替换提示
使用该方法可以约束粘贴替换的行为
例如可以使用该方法来限制列表项节点只能被其他列表项节点替换,以保证列表的结构层次
elementNode.excludeFromCopy(destination)返回一个布尔值,以表示是否在复制中排除该节点。(可选)参数destination用于设置复制的特定场景,它的值可以是"clone"或"html"elementNode.extractWithChild(child, selection, destination)返回一个布尔值(默认值为false),用于设置复制的行为。由于 ElementNode 元素节点一般不易选中,当复制该节点的子节点时,是否同时用该节点进行包裹。
该方法会传入三个参数:- 第一个参数
child是被选中的后代节点 - 第二个参数
selection是选区 - 第三个参数
destination表示复制的特定场景,它的值可以是"clone"或"html"
提示
该方法用于 @lexical/clipboard 模块中,以实现复制粘贴的相关功能
例如对于
HeadingNode标题节点,覆写了该方法让返回值为true,所以即使只是复制标题节点内的(部分)文本,再粘贴回编辑器也会是标题节点- 第一个参数
elementNode.getDirection()返回元素节点种的文本书写方向,返回值是"ltr"表示从左往右,"rtl"表示从右到左,如果没有设置则返回nullelementNode.getFormat()返回元素节点中的文本对齐类型,返回值是数值
不同的数值对应于不同的对齐类型1表示左对齐 left2表示居中对齐 right3表示右对齐 center4表示两端对齐 justify5表示(根据文本的书写方向的)起始对齐 start6表示(根据文本的书写方向的)结束对齐 end
elementNode.getFormatType()返回元素节点中的文本对齐类型,返回值是字符串"left"|"right"|"center"|"justify"|"start"|"end"如果没有设置则返回""空字符串elementNode.hasFormat(type)返回一个布尔值,表示节点的对齐类型和给定的类型type一致。传入的参数type可以是"left"|"right"|"center"|"justify"|"start"|"end"|""(空字符串) 这些字符串中的任一值elementNode.getIndent()返回元素节点的缩进值,返回值是数值elementNode.getTextContent()获取节点内的文本内容(包含所有后代节点里所有 TextNode 文本节点,并将它们的内容拼接起来,而且在块元素的文本之间添加了双换行符\n\n)elementNode.getTextContentSize()获取节点内的文本的长度。其实是基于前一个方法const str = node.getTextContent()再通过str.length()得到字符串的长度elementNode.isDirty()返回一个布尔值,表示该节点在当前更新流程中,是否被标记为 dirty 「脏节点」elementNode.isEmpty()返回一个布尔值,表示该节点内容是否为空(即没有子节点)elementNode.isInline()返回一个布尔值,表示该节点内容是否为行内节点elementNode.isLastChild()返回一个布尔值,表示该节点内容是否作为其父节点的最后一个子节点elementNode.isShadowRoot()返回一个布尔值,表示该节点是否作为影子根节点影子根节点
影子根节点 ShadowRoot 是指一些节点的行为类似于根节点,一般表示局部节点树结构的终点/树根,例如表格的单元格节点
TableCellNode就是影子根节点也可以使用方法
$isRootOrShadowRoot(node)判断给定的节点node是否为根节点或影子根节点 ShadowRoot
操作节点
elementNode.clear()移除该节点内部的所有后代节点elementNode.collapseAtStart(selection)该方法是在光标位于节点的开头(即选区的锚点 anchor 位于该节点,且偏移量offset为0时),并且用户按下 Backspace 键时调用的。传入的参数selection是 RangeSelection 范围选区
该方法返回一个布尔值,用来表示该方法是否已经处理完用户的操作交互。如果返回false则除了执行该函数中的一些逻辑,Lexical 在后台还会继续执行一些(关于删除单个字符的)通用处理逻辑注意
该方法用于删除单个字符的场景(具体参考核心模块的文件
LexicalSelection源码中的deleteCharacter方法)但是一般情况下该方法其实并不会被调用,由于 ElementNode 元素节点一般作为容器,所以它会包含子节点(如文本节点),当光标从视觉上移动这些节点开头,但是实际上选区的锚点 anchor 依然位于子节点内(一般是位于文本节点的开头),所以当用户按下 Backspace 键时并没有调用相应的元素节点的
collapseAtStart方法,而是执行删除单个字符的通用处理逻辑如果要测试以上方法,则可以将节点置于编辑器的开头,在这个特殊的位置按下 Backspace 键,一般都会触发元素节点的
collapseAtStart方法,因为按下多次 Backspace 键后,将 ElementNode 元素节点的内容/子节点都删除掉以后,就可以保证光标真的触达元素节点的开头了所以对于一些元素节点,它们位于编辑器的开头与位于中间,行为会有所不同
具体可以参考官方编写的一些元素节点实例
elementNode.insertNewAfter(selection, restoreSelection?)设置在该节点上按 Enter 回车键时的行为(一般处理逻辑是根据光标锚点的位置而插入不同类型的节点)。第一个参数selection是范围选区,第二个(可选)参数restoreSelection是一个布尔值,以控制是否在插入节点之后,是否重新调整选区(将光标移到所插入节点里)。返回值是新插入的节点或null(如果该操作没有插入节点)elementNode.setDirection(direction)设置文本书写方向,传入的参数direction可以是"ltr"|"rtl"|null这些中的任一值elementNode.setFormat(type)设置文本对齐类型,传入的参数type可以是"left"|"right"|"center"|"justify"|"start"|"end"|""(空字符串) 这些字符串中的任一值elementNode.setIndent(indentLevel)设置节点的缩进级别,传入的参数indentLevel是数值,表示缩进的级别elementNode.splice(start, deleteCount, nodesToInsert)在特定的索引值start位置删除特定数量的deleteCount的子节点,并在后面插入给定的一系列节点nodesToInsert(数组)作为子节点
导出导入相关
elementNode.exportJSON()在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。说明
该方法覆写了父类
LexicalNode核心节点的对应方法,提供了元素节点序列化 JSON 对象的模板ts// 返回的 JSON 对象模板 { children: [], // 所包含的子节点 direction: this.getDirection(), // 书写方向 format: this.getFormatType(), // 对齐方式 indent: this.getIndent(), // 缩进级别 type: 'element', // 节点类型名称 version: 1, // 节点版本 }除了所有节点都需要确保具有
type字段和version字段 属性,ElementNode 还必须要有children、direction等属性,以描述元素节点的相关信息该方法一般会被所继承的后代子类覆写,在返回的 JSON 对象中增添更多的属性
后代节点相关
elementNode.getFirstChild()获取该节点的第一个(直接)子节点(即节点的属性__first所指向的子节点),如果未能获取相应的子节点则返回null提示
如果希望没有获取到子节点时,在终端抛出错误提示(而不是返回
null)可以使用另一个方法elementNode.getFirstChildOrThrow()该方法会在未获取到子节点时抛出
'Expected node {key} to have a first child.'错误提示elementNode.getLastChild()获取该节点的最后一个(直接)子节点(即节点的属性__last所指向的子节点),如果未能获取相应的子节点则返回null提示
如果希望没有获取到子节点时,在终端抛出错误提示(而不是返回
null)可以使用另一个方法elementNode.getLastChildOrThrow()该方法会在未获取到子节点时抛出
'Expected node {key} to have a last child.'错误提示elementNode.getChildrenKeys()获取所有子节点的 key 唯一标识符,返回一个数组elementNode.getChildrenSize()返回所包含的子节点数量elementNode.getChildAtIndex(index)根据给定索引index获取该节点的某个(直接)子节点,如果未能获取相应的子节点(索引值不在范围中)则返回nullelementNode.getFirstDescendant()获取「第一个」末端节点。如果元素节点本来就没有子节点则返回null说明
这里所说的后代节点是指在节点树种「末端」的节点(即没有子节点的元素节点,或叶子节点)
这里所指的「第一个」是指按照节点__first属性所指向的子节点,以上方法就是按照此规则进行递归寻找,直至找到「末端」的节点elementNode.getLastDescendant()获取「最后一个」末端节点。如果元素节点本来就没有子节点则返回null说明
这里所说的后代节点是指在节点树种「末端」的节点(即没有子节点的元素节点,或叶子节点)
这里所指的「最后一个」是指按照节点__last属性所指向的子节点,以上方法就是按照此规则进行递归寻找,直至找到「末端」的节点elementNode.getDescendantByIndex(index)根据给定索引index获取该节点的末端节点,如果未能获取相应的子节点则返回null
如果传入的参数index比直接子节点的数量大,则获取「最后一个」末端节点。
如果传入的参数index在直接子节点的数量范围中,则先判断索引所对应的直接子节点是否就是末端节点,如果是则直接返回;如果不是则获取「第一个」末端节点说明
这里所说的后代节点是指在节点树种「末端」的节点(即没有子节点的元素节点,或叶子节点)
「最后一个」是指按照节点
__last属性所指向的子节点,并按照此规则进行递归寻找,直至找到「末端」的节点「第一个」是指按照节点
__first属性所指向的子节点,并按照此规则进行递归寻找,直至找到「末端」的节点elementNode.append(nodesToAppend)将给定的一系列节点nodesToAppend(数组)插入到该元素节点内,作为最后的子节点elementNode.getAllTextNodes()获取该节点内的所有文本节点,返回一个数组,里面包含一系列的文本节点(以递归的方式获取文本节点,所以包括嵌套在后代节点中的文本节点)
选区相关
elementNode.select(_anchorOffset?, _focusOffset?)根据传入的(可选)参数_anchorOffset和_focusOffset作用选区的锚点偏移量和焦点偏移量(以选中该元素节点内的子节点),返回所创建的 RageSelection 范围选区elementNode.selectStart()将光标移动到「第一个」末端节点的开头(从视觉上其效果就相当于将光标移动到该元素节点的开头)说明
末端节点是指即没有子节点的元素节点,或叶子节点
「第一个」是指按照节点
__first属性所指向的子节点,并按照此规则进行递归寻找,直至找到「末端」的节点elementNode.selectEnd()将光标移动到「最后一个」末端节点的结尾(从视觉上其效果就相当于将光标移动到该元素节点的尾部)说明
末端节点是指即没有子节点的元素节点,或叶子节点
「最后一个」是指按照节点
__last属性所指向的子节点,并按照此规则进行递归寻找,直至找到「末端」的节点
TextNode 文本节点
文本节点 TextNode 的作用是展示文本内容(作为叶子节点,即不再包含其他子节点),它是 Lexical 的一种默认节点
源文件
该类在核心模块的 nodes/LexicalTextNode.ts 文件中进行定义
通过调用该模块所导出的方法 $createTextNode(text?) 进行实例化(以下简称为 textNode),该方法可以接受一个(可选)参数 text 它是一个字符串,作为文本节点的内容
该节点扩展自 LexicalNode 核心节点,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
在文本节点中所使用的一些概念
format 格式:可以是 bold、italic、underline、strikethrough、code、subscript、superscript 这些值的组合
mode 模式:可以是 normal、token、segmented 这三种模式之一
- 如果为
token表示该节点是不可变的 immutable,它的内容不可更改,在删除时只能整体删掉 - 如果为
segmented表示该节点的内容是片段化的,即在删除内容时,每次删掉一个单词。所以该节点是可编辑的,但是内容更新后就会变成非片段化的 ❓
style 样式:用于为文本添加行内 CSS 样式
以下是获取文本节点信息的一些方法
TextNode.getType()获取节点的类型/名称
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回值为'text'提示
该方法其实是 TextNode 类的静态方法,所以不需要实例化就可以直接调用该方法
textNode.canInsertTextAfter()返回true,用于表示如果光标在节点最后面时继续输入文字,则文字会插入 append 到节点内(作为节点的内容)提示
如果返回
false则会紧挨着该节点创建一个兄弟节点来容纳新输入的文本例如官方基于
TextNode所创建的一个子类TabNode它用来表示一个缩进,并不允许在该节点中添加文本,所以要覆写该方法返回falsetextNode.canInsertTextBefore()和上一个方法类似,也是返回true,表示在节点前可以继续输入文字textNode.getDetail()一个 32 位的整数,关于书写方向和是否可进行节点融合的信息提示
该方法所返回的是一个数值,可读性较低
可以分别调用
textNode.isUnmergeable()和textNode.isDirectionless()方法获取节点相应的信息textNode.isUnmergeable()返回一个布尔值,以表示节点是否不可以和临近的兄弟文本节点进行 merge 连接/融合为一个文本节点textNode.getFormat()返回一个 32 位的整数,表示节点所设有的格式提示
还有另一个类似的方法
textNode.getFormat(type, alignWithFormat)也是返回一个 32 位的整数,表示节点所设有的格式这些方法所返回的是一个数值,可读性较低
可以调用
textNode.hasFormat(textFormatType)方法检测文本节点是否具有某种格式textNode.hasFormat(type)返回一个布尔值,以表示文本节点是否具有给定的type格式
其中入参type可以是"bold"、"underline"、"strikethrough"、"italic"、"highlight"、"code"、"subscript"或"superscript"textNode.getMode()获取文本节点的模式,可以是normal|token|segmented这三种模式之一textNode.getStyle()获取应用于文本节点所对应的 DOM 元素的行内 CSS 样式(字符串)textNode.getTextContent()获取节点的文本内容
该节点覆写了父节点(LexicalNode 核心节点)的相应方法textNode.isComposing()返回一个布尔值,表示检测文本节点的内容是否正在处于「组合」输入模式说明
「组合」输入模式是指通过 IME 或第三方扩展接收输入时(最终输出的内容并不是键盘上的字母,而是转换为其他语言符号)
通过该方法可以判断用户是否使用 IME 输入法并正在输入时,如果是则编辑器一般不执行相关的逻辑处理,要等到真正的内容键入/插入到编辑器中再执行相关的处理
textNode.isDirectionless()返回一个布尔值,表示文本内容排版并不受 RTL 从右往左或 LTR 从左往右的书写方式的影响textNode.isSegmented()返回一个布尔值,表示节点的模式是否为segmented提示
如果节点的 mode 模式是
segmented则节点的内容是片段化的,即使用光标导航时,会按照单词为单位,例如在删除内容时,每次删掉一个单词(以空格为标准)textNode.isToken()返回一个布尔值,表示节点的模式是否为token提示
如果节点的 mode 模式是
token则节点的内容不可更改,使用光标导航时,会按照单词为单位,而在删除内容时,只能整体删掉textNode.isSimpleText()返回一个布尔值,表示该节点是否为文本节点(节点类型 type 为text而不是文本节点子类),而且节点并不处于segmented或token这两种特殊的模式下textNode.isTextEntity()返回一个布尔值,表示编辑器是否针对该类型的节点,使用registerLexicalTextEntity注册了一个节点转换器说明
使用
editor.registerLexicalTextEntity方法注册的节点转换器会根据文本内容的匹配模式而对节点进行转换
操作节点
textNode.mergeWithSibling(targetTextNode)将给定的文本节点targetTextNode与当前文本节点融合(提取出内容,追加到当前节点内),并删除这个给定的文本节点textNode.setDetail(detail)用于设置节点的__detail属性(关于书写方向和是否可进行节点融合的信息),参数detail是一个 32 位的整数或'directionless'|'unmergable'这两个字符串之一textNode.toggleDirectionless()切换节点的文本内容排版是否受书写方式影响的状态textNode.toggleUnmergeable()切换文本节点的可否 merge 的属性textNode.setFormat(format)为节点设置格式,每次只能设置一种格式,⚠️ 而且该方法会覆盖/移除掉文本原本已有的格式。参数format可以是一个表示格式的 32 位的整数或bold|italic|underline|strikethrough|code|subscript|superscript这些字符串之一textNode.toggleFormat(type)为节点切换(添加或移除)给定的格式type,并不影响其他类型的格式textNode.setMode(type)为文本节点设置模式。其中入参type可以是"normal"|"token"|"segmented"这三个字符串之一textNode.setStyle(type)为文本节点所对应的 DOM 元素设置行内 CSS 样式。参数type是字符串,作为 inline styletextNode.setTextContent(str)为文本节点设置内容textNode.spliceText(offset, delCount, newText, moveSelection?)在特定的位置offset删除特定数量delCount字符,并插入新的内容newText旧文本并插入新文本。最后的(可选)参数moveSelection是一个布尔值,以决定是否将光标移动到插入内容的最后textNode.splitText(splitOffsets)将一个文本节点「切分」为多个文本节点,并以这些文本节点取代原来的文本节点。返回一个数组,包含切分后的文本节点
其中入参splitOffsets是一个数组,里面的元素都是数字,表示切分的位置TextNode.clone(targetTextNode)返回该节点的一份克隆,用于复制粘贴 ❓
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回一个文本节点,与给定的文本节点targetTextNode具有相同的内容和相同的 key 唯一标识符提示
该方法其实是 TextNode 类的静态方法,所以不需要实例化就可以直接调用该方法,基于传入的文本节点生成一个拷贝
DOM 相关
textNode.createDOM(config)用于指定该节点在页面上所对应的 DOM 元素结构。参数config是编辑器的配置对象(一般用于获取config.theme编辑器主题为相应的 DOM 元素添加 Class 类名)。最后返回一个 HTML 元素。提示
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回的 HTML 元素会根据文本节点的格式而不同
而且一些格式会对应于外层标签 Outer Tag,一些格式会对应于内层标签 Inner tag
与外层标签 Outer Tag 相关的格式(如果没有以下的格式,则不设置外层标签)
- 如果文本节点具有
code格式,则返回一个<code></code>元素 - 如果文本节点具有
highlight格式,则返回一个<mark></mark>元素 - 如果文本节点具有
subscript格式,则返回一个<sub></sub>元素 - 如果文本节点具有
superscript格式,则返回一个<sup></sup>元素
与内层标签 Inner Tag 相关的格式(如果没有以下的格式,则内层标签为
<span></span>标签)- 如果文本节点具有
strong格式,则返回一个<strong></strong>元素 - 如果文本节点具有
italic格式,则返回一个<em></em>元素
然后将外层元素和内层元素进行整合,返回一个 HTML 元素
例如对于具有
highlight和strong的文本节点,最终返回的 HTML 元素是<mark><strong></strong></mark>并从编辑器主题中
config.theme.text获取相应的 Class 类名添加到元素上- 如果文本节点具有
textNode.updateDOM(prevNode, dom, config)当节点变化时,该方法会被调用,其作用主要是更新 DOM 元素,以便与节点的最新状态保持一致
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,根据文本节点的格式的不同,执行不同的逻辑(如果格式的改变导致文本节点所对应 HTML 元素改变,则返回true,表示需要调用方法textNode.createDOM()重新渲染页面相应的 DOM 元素;如果格式的改变没有导致 HTML 元素改变,则返回false)
导出导入相关
textNode.exportDOM(editor)在导出节点时,将节点序列化为 DOM 元素(字符串)时会调用该方法。该方法会接受一个参数editor编辑器实例
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑textNode.exportJSON()在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。说明
该方法覆写了父类
LexicalNode核心节点的对应方法,提供了元素节点序列化 JSON 对象的模板ts// 返回的 JSON 对象模板 { detail: this.getDetail(), // 一个 32位的整数,关于书写方向和是否可进行节点融合的信息 format: this.getFormat(), // 对齐方式 mode: this.getMode(), // 模式,`normal`、`token`、`segmented`这三个值之一 style: this.getStyle(), // CSS 样式 text: this.getTextContent(), // 文本内容 type: 'text', // 节点类型名称 version: 1, // 节点版本 };TextNode.importDOM()将 HTML 元素反序列化为节点时调用的方法
该方法返回的是一个DOMConversionMap类型的值(表示转换成功)或null(表示该 DOM 与当前的节点不匹配,转换失败,会被更低级的转换函数进行处理)提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 HTML 的节点)生成一个该节点的实例
TextNode.importJSON(serializedNode)将 JSON 数据反序列化为节点时调用的方法,返回一个该节点实例
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 JSON 的节点)生成一个该节点的实例
选区相关
textNode.select(_anchorOffset?, _focusOffset?)根据传入的(可选)参数_anchorOffset和_focusOffset作用选区的锚点偏移量和焦点偏移量(以选中节点中的相应字符),返回所创建的 RageSelection 范围选区textNode.selectionTransform(prevSelection, nextSelection)在选区发生变化时,如果锚点或焦点位于该节点中,则会执行该方法所设置的逻辑 ❓
DecoratorNode 装饰节点
装饰节点 DecoratorNode 可以作为一种复杂视图的封装,让交互性和功能性更强大的组件嵌入到编辑器中
源文件
该类在核心模块的 nodes/LexicalDecoratorNode.ts 文件中进行定义
该节点一般都不进行实例化(以下简称为 decoratorNode),而是用作父类,成为其他自定义节点的「模板」
该节点扩展自 LexicalNode 核心节点,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
decoratorNode.decorate(editor, config)该方法一般会被所继承的后代子类覆写,用于设置装饰节点的具体实现,例如返回一个虚拟 DOM 或组件等。第一个参数editor是编辑器对象,第二个参数config是编辑器的配置对象decoratorNode.isInline()返回一个布尔值,表示该装饰节点是否为行内元素decoratorNode.isIsolated()返回一个布尔值,表示该节点是否脱离编辑器文档的瀑布流 ❓decoratorNode.isKeyboardSelectable()返回一个布尔值,表示是否可以通过键盘导航选中(创建一个节点选区) ❓
LineBreakNode 换行节点
换行节点 LineBreakNode 的作用如其名,实现换行
对比
可以将插入 LineBreakNode 换行节点的行为理解为「软换行」,而按下 Enter 键的行为理解为「硬换行」
因为插入的 LineBreakNode 换行节点后,虽然在视觉上和按下 Enter 键的行为一样(都是将光标换到下一行),但是节点树的结构有很大区别,而且实际上对应的 DOM 结构树也不同
如果在 ParagraphNode 段落节点中插入的 LineBreakNode 换行节点,其实这个 LineBreakNode 和上一行的 TextNode 文本节点是兄弟节点的关系,所以上一行和下一行的 TextNode 文本节点其实同属同一个父节点(ParagraphNode 段落节点)
相对应的 DOM 节点树是插入了 <br> 元素,而前后行的文本内容都包裹在同一个 <p> 段落元素中
而如果按下 Enter 键则是在下一行创建一个新的 ParagraphNode 段落节点,所以上一行和下一行的 TextNode 文本节点是归属于不同的父节点(ParagraphNode 段落节点)
源文件
该类在核心模块的 nodes/LexicalLineBreakNode.ts 文件中进行定义
通过调用该模块所导出的方法 $createLineBreakNode() 进行实例化(以下简称为 lineBreakNode)
该节点扩展自 LexicalNode 核心节点,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
LineBreakNode.getType()获取节点的类型/名称
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回值为'linebreak'提示
该方法其实是 LineBreakNode 类的静态方法,所以不需要实例化就可以直接调用该方法
lineBreakNode.getTextContent()获取节点内的文本内容
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回值恒为\n,即在(纯)文本中\n字符串表示换行节点(例如在复制粘贴时)
操作节点
LineBreakNode.clone(targetLineBreakNode)返回该节点的一份克隆,用于复制粘贴 ❓
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回一个换行节点,与给定的换行节点targetLineBreakNode具有相同的 key 唯一标识符提示
该方法其实是 LineBreakNode 类的静态方法,所以不需要实例化就可以直接调用该方法,基于传入的换行节点生成一个拷贝
DOM 相关
lineBreakNode.createDOM()用于指定该节点在页面上所对应的 DOM 元素结构。
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回的 HTML 元素是<br>lineBreakNode.updateDOM(prevNode, dom, config)当节点变化时,该方法会被调用,其作用主要是更新 DOM 元素,以便与节点的最新状态保持一致
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,返回false表示不需要重新创建一个新的 DOM 元素来取代页面上原有的旧元素
导出导入相关
lineBreakNode.exportJSON()在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑换行节点的 JSON 格式
换行节点序列化为 JSON 对象具有如下属性
ts// 返回的 JSON 对象 { type: 'linebreak', // 节点类型名称 version: 1, // 节点版本 }LineBreakNode.importJSON(serializedNode)将 JSON 数据反序列化为节点时调用的方法,返回一个该节点实例
该节点覆写了父节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 JSON 的节点)生成一个该节点的实例
LineBreakNode.importDOM()将 HTML 元素反序列化为节点时调用的方法
该方法根据当前节点的父节点的不同情况,返回一个DOMConversionMap类型的值(表示转换成功)或null(表示该 DOM 与当前的节点不匹配,转换失败,会被更低级的转换函数进行处理),如果当前节点作为其父节点的唯一子节点,则跳过解析导入(相当于对于单独存在的<br>元素进行「压缩」/省略的处理)说明
DOMConversionMap类型是一个映射,具体介绍可以查看另一篇笔记的相关章节提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 HTML 的节点)生成一个该节点的实例
RootNode 根节点
根节点 RootNode 的作用是充当编辑器最外层的容器(即节点树最根部的节点,对于倒置的节点树则是最顶部的节点)
源文件
该类在核心模块的 nodes/LexicalRootNode.ts 文件中进行定义
通过调用该模块所导出的方法 $createRootNode() 或 new RootNode() 进行实例化(以下简称为 rootNode)
该节点扩展自 ElementNode 元素节点,所以段落节点 ParagraphNode 除了继承了父节点 ElementNode 元素节点的属性和方法以外,还会继承祖先元素 LexicalNode 核心节点的属性和方法,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
RootNode.getType()获取节点的类型/名称
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回值为'root'提示
该方法其实是 RootNode 类的静态方法,所以不需要实例化就可以直接调用该方法
rootNode.getTextContent()获取编辑器内的所有文本内容
该节点覆写了父节点(ElementNode 元素节点)的相应方法,参考相关的源码会先判断编辑器是否处于只读模式,如果是则返回 cachedText 缓存的文本 ❓ 如果不是则调用父节点的相应,即返回编辑器内的所有文本节点的内容
操作节点
rootNode.collapseAtStart()该方法是在光标位于节点的开头(即选区的锚点 anchor 位于该节点,且偏移量offset为0时),并且用户按下 Backspace 键时调用的。
该节点覆写了父节点(ElementNode 元素节点)的相应方法,参考相关的源码该函数内部并没有进行任何其他处理就直接返回true(以表示完成了对用户操作交互的响应),即光标在根节点的开头时,如果用户按下 Backspace 键,在视觉上是看不出任何的响应操作的注意
该方法用于删除单个字符的场景(具体参考核心模块的文件
LexicalSelection源码中的deleteCharacter方法)但是一般情况下该方法其实并不会被调用,由于 ElementNode 元素节点一般作为容器,所以它会包含子节点(如文本节点),当光标从视觉上移动这些节点开头,但是实际上选区的锚点 anchor 依然位于子节点内(一般是位于文本节点的开头),所以当用户按下 Backspace 键时并没有调用相应的元素节点的
collapseAtStart方法,而是执行删除单个字符的通用处理逻辑rootNode.remove()该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法。该方法原本的目的是移除该节点,但是由于根节点是(倒置)节点树的最顶部的节点,所以它一般需要存在(不允许删除),所以这里该方法必然会抛出错误提示'remove: cannot be called on root nodes'rootNode.replace(replaceWithNode)该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法。该方法原本的目的是用给定的节点replaceWithNode替换当前节点,但是由于根节点是(倒置)节点树的最顶部的节点,所以它一般不允许被替换为其他节点,所以这里该方法必然会抛出错误提示'replace: cannot be called on root nodes'RootNode.clone()返回该节点的一份克隆,用于复制粘贴 ❓
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回一个根节点,该拷贝会具有不同的 key 唯一标识符,用于复制粘贴 ❓提示
该方法其实是 RootNode 类的静态方法,所以不需要实例化就可以直接调用该方法,生成一个根节点拷贝
DOM 相关
rootNode.updateDOM(prevNode, dom)当节点变化时,该方法会被调用,其作用主要是更新 DOM 元素,以便与节点的最新状态保持一致
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,直接返回false表示不需要重新创建一个新的 DOM 元素来取代页面上原有的旧元素
导入导出相关
rootNode.exportJSON()在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。
该节点覆写了父节点(ElementNode 元素节点)的相应方法,以实现具体的逻辑根节点的 JSON 格式
根节点序列化为 JSON 对象具有如下属性
ts// 返回的 JSON 对象 { children: [], // 所包含的子节点 direction: this.getDirection(), // 书写方向 format: this.getFormatType(), // 对齐方式 indent: this.getIndent(), // 缩进级别 type: 'root', // 节点类型名称 version: 1, // 节点版本 }RootNode.importJSON(serializedNode)将 JSON 数据反序列化为节点时调用的方法,返回一个该节点实例
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 JSON 的节点)生成一个该节点的实例
父节点相关
rootNode.getTopLevelElementOrThrow()该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法。该方法原本的目的是获取该节点(沿着节点树回溯)最顶部的祖先(元素)节点,但是由于根节点是(倒置)节点树的最顶部的节点,所以这里该方法必然会抛出错误提示'getTopLevelElementOrThrow: root nodes are not top level elements'说明
最顶部的祖先节点并不包含根节点或影子根节点 ShadowRoot
兄弟节点相关
rootNode.insertAfter(nodeToInsert)该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法。该方法原本的目的是将给定的节点nodeToInsert插入到当前节点的后面(作为下一个兄弟节点),但是由于根节点是(倒置)节点树的最顶部的节点,所以不允许有兄弟节点,所以这里该方法必然会抛出错误提示'insertAfter: cannot be called on root nodes'rootNode.insertBefore(nodeToInsert)该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法。该方法原本的目的是将给定的节点nodeToInsert插入到当前节点的前面(作为前一个兄弟节点),但是由于根节点是(倒置)节点树的最顶部的节点,所以不允许有兄弟节点,所以这里该方法必然会抛出错误提示'insertBefore: cannot be called on root nodes'
后代节点相关
rootNode.append(nodes)将给定的列表中的一系列节点nodes(数组)插入到根节点内,作为最后的子元素注意
只有
ElementNode元素节点或DecoratorNode装饰节点才可以作为rootNode根节点的直接子节点,即以上方法中所传入的参数[nodes]列表中的节点不能是TextNode文本节点(或它的子类)
ParagraphNode 段落节点
段落节点 ParagraphNode 的作用是充当一般文本的父节点(即用于容纳文本内容),可以将其理解为 Lexical 的一种默认节点(它是块元素,而文本节点 TextNode 则是另一种默认节点,它属于行内元素)
源文件
该类在核心模块的 nodes/LexicalParagraphNode.ts 文件中进行定义
通过调用该模块所导出的方法 $createParagraphNode() 进行实例化(以下简称为 paragraphNode)
该节点扩展自 ElementNode 元素节点,所以段落节点 ParagraphNode 除了继承了父节点 ElementNode 元素节点的属性和方法以外,还会继承祖先元素 LexicalNode 核心节点的属性和方法,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
ParagraphNode.getType()获取节点的类型/名称
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回值为'paragraph'提示
该方法其实是 ParagraphNode 类的静态方法,所以不需要实例化就可以直接调用该方法
操作节点
paragraphNode.collapseAtStart()该方法是在光标位于节点的开头(即选区的锚点 anchor 位于该节点,且偏移量offset为0时),并且用户按下 Backspace 键时调用的。
该方法返回一个布尔值,用来表示该方法是否已经处理完用户的操作交互。如果返回false则除了执行该函数中的一些逻辑,Lexical 在后台还会继续执行一些(关于删除单个字符的)通用处理逻辑注意
该方法用于删除单个字符的场景(具体参考核心模块的文件
LexicalSelection源码中的deleteCharacter方法)但是一般情况下该方法其实并不会被调用,由于 ElementNode 元素节点一般作为容器,所以它会包含子节点(如文本节点),当光标从视觉上移动这些节点开头,但是实际上选区的锚点 anchor 依然位于子节点内(一般是位于文本节点的开头),所以当用户按下 Backspace 键时并没有调用相应的元素节点的
collapseAtStart方法,而是执行删除单个字符的通用处理逻辑如果要测试以上方法,则可以将节点置于编辑器的开头,在这个特殊的位置按下 Backspace 键,一般都会触发元素节点的
collapseAtStart方法,因为按下多次 Backspace 键后,将 ElementNode 元素节点的内容/子节点都删除掉以后,就可以保证光标真的触达元素节点的开头了所以对于一些元素节点,它们位于编辑器的开头与位于中间,行为会有所不同
该节点覆写了父节点(ElementNode 元素节点)的相应方法,以针对删除编辑器开头的空段落的场景,如果编辑器中仅有这一个空段落(即该节点),则将该节点无法删除提示
参考相关的源码,其核心逻辑如下
首先基于该节点的子节点的数量,来判断该节点是否为空;如果该节点为空,则进一步判断该节点之后是否存在兄弟节点;如果存在兄弟节点,则执行删除该节点的操作,否则就不执行删除操作。
ParagraphNode.clone(targetParagraphNode)返回该节点的一份克隆,用于复制粘贴 ❓
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回一个段落节点,与给定的段落节点targetParagraphNode具有相同的 key 唯一标识符提示
该方法其实是 ParagraphNode 类的静态方法,所以不需要实例化就可以直接调用该方法,基于传入的段落节点生成一个拷贝
DOM 相关
paragraphNode.createDOM(config)用于指定该节点在页面上所对应的 DOM 元素结构。参数config是编辑器的配置对象(一般用于获取config.theme编辑器主题为相应的 DOM 元素添加 Class 类名)。最后返回一个 HTML 元素。
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回的 HTML 元素是<p>,并从编辑器主题中config.theme.paragraph获取相应的 Class 类名添加到该元素上paragraphNode.insertNewAfter(selection, restoreSelection)设置在该节点上按 Enter 回车键时的行为
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回一个新建的paragraphNode段落节点paragraphNode.updateDOM(prevNode, dom, config)当节点变化时,该方法会被调用,其作用主要是更新 DOM 元素,以便与节点的最新状态保持一致
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,返回false表示不需要重新创建一个新的 DOM 元素来取代页面上原有的旧元素
导出导入相关
paragraphNode.exportDOM(editor)在导出节点时,将节点序列化为 DOM 元素(字符串)时会调用该方法。该方法会接受一个参数editor编辑器实例
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑ParagraphNode.importDOM()将 HTML 元素反序列化为节点时调用的方法
该方法返回的是一个DOMConversionMap类型的值(表示转换成功)或null(表示该 DOM 与当前的节点不匹配,转换失败,会被更低级的转换函数进行处理)说明
DOMConversionMap类型是一个映射,具体介绍可以查看另一篇笔记的相关章节提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 HTML 的节点)生成一个该节点的实例
paragraphNode.exportJSON()在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。
该节点覆写了父节点(ElementNode 元素节点)的相应方法,以实现具体的逻辑段落节点的 JSON 格式
段落节点序列化为 JSON 对象具有如下属性
ts// 返回的 JSON 对象 { ...super.exportJSON(), // ElementNode 元素节点序列化为 JSON 对象所具有的属性 type: 'paragraph', // 节点类型名称 version: 1, // 节点版本 }ParagraphNode.importJSON(serializedNode)将 JSON 数据反序列化为节点时调用的方法,返回一个该节点实例
该节点覆写了祖先节点(LexicalNode 核心节点)的相应方法,以实现具体的逻辑提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 JSON 的节点)生成一个该节点的实例
TabNode 缩进节点
缩进节点 TabNode 的作用是表示段落首部的缩进层级
源文件
该类在核心模块的 nodes/LexicalTabNode.ts 文件中进行定义
通过调用该模块所导出的方法 $createTabNode() 进行实例化(以下简称为 tabNode)
该节点扩展自 TextNode 文本节点,所以缩进节点 tabNode 除了继承了父节点 TextNode 元素节点的属性和方法以外,还会继承祖先元素 LexicalNode 核心节点的属性和方法,该类型的节点会对所继承的一些属性和方法进行覆写,还会新增一些属性和方法,在后续的小节列出
节点信息
TabNode.getType()获取节点的类型/名称
该节点覆写了父节点(TextNode 文本节点)的相应方法,返回值为'tab'提示
该方法其实是 TextNode 类的静态方法,所以不需要实例化就可以直接调用该方法
tabNode.canInsertTextAfter()返回false,用于表示如果光标在节点最后面时继续输入文字,则会创建一个兄弟节点(文本节点)来容纳新输入的文本
该节点覆写了父节点(TextNode 文本节点)的相应方法,将返回值改为falsetabNode.canInsertTextBefore()和上一个方法类似,也是返回false,表示在节点前如果继续输入文字,则会创建一个兄弟节点(文本节点)来容纳新输入的文本
该节点覆写了父节点(TextNode 文本节点)的相应方法,将返回值改为false
操作节点
tabNode.setDetail(detail)该节点覆写了父节点(TextNode 文本节点)的相应方法。该方法原本的目的是设置节点的__detail属性(关于书写方向和是否可进行节点融合的信息),但是由于缩进节点与书写方向无关且不支持节点融合 ❓ 所以这里该方法必然会抛出错误提示'TabNode does not support setDetail'tabNode.setMode(type)该节点覆写了父节点(TextNode 文本节点)的相应方法。该方法原本的目的是设置文本节点的模式("normal"|"token"|"segmented"这三个字符串之一),但是由于缩进节点并不支持设置模式,所以这里该方法必然会抛出错误提示'TabNode does not support setMode'tabNode.setTextContent(str)该节点覆写了父节点(TextNode 文本节点)的相应方法。该方法原本的目的是设置文本节点的内容,但是由于缩进节点并不支持内容,所以这里该方法必然会抛出错误提示'TabNode does not support setTextContent'说明
在实例化缩进节点
tabNode时就设置好文本内容为\t而且之后不能再更改TabNode.clone(targetTabNode)返回该节点的一份克隆,用于复制粘贴
该节点覆写了父节点(TextNode 文本节点)的相应方法,返回一个缩进节点,与给定的缩进节点targetTabNode具有相同的 key 唯一标识符提示
该方法其实是 TabNode 类的静态方法,所以不需要实例化就可以直接调用该方法,基于传入的缩进节点生成一个拷贝
导入导出相关
tabNode.exportJSON()在导出节点时,将节点序列化为 JSON 格式(对象)时会调用该方法。
该节点覆写了父节点(TextNode 文本节点)的相应方法,以实现具体的逻辑缩进节点的 JSON 格式
缩进点序列化为 JSON 对象具有如下属性
ts// 返回的 JSON 对象 { ...super.exportJSON(), // TextNode 文本节点序列化为 JSON 对象所具有的属性 type: 'tab', // 节点类型名称 version: 1, // 节点版本 }TabNode.importDOM()将 HTML 元素反序列化为节点时调用的方法
该方法返回null表示(导入时)没有相应 DOM 元素与之对应提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法
TabNode.importJSON(serializedNode)将 JSON 数据反序列化为节点时调用的方法,返回一个该节点实例
该节点覆写了父节点(TextNode 文本节点)的相应方法,以实现具体的逻辑提示
该方法其实是静态方法,所以不需要实例化就可以直接调用该方法,基于传入的参数(序列化为 JSON 的节点)生成一个该节点的实例
选区类
选区是指编辑器内容中被选中的区域,该模块中 Lexical 提供了 4 种类型的选区
GridSelection网格选区NodeSelection节点选区RangeSelection范围选区null表示选区为空
BaseSelection 类型
三种选区 GridSelection 网格选区、NodeSelection 节点选区、RangeSelection 范围选区,都实现了接口 BaseSelection 这个 Typescript 类型所指明的方法
export interface BaseSelection {
clone(): BaseSelection; // 克隆选区
dirty: boolean; // 该选区是否标记为「脏」,选区经过了更改 ❓
extract(): Array<LexicalNode>; // 提取选区内的节点
getNodes(): Array<LexicalNode>; // 获取选区的节点
getTextContent(): string; // 提取选区内的内容
insertRawText(text: string): void; // 插入纯文本
is(selection: null | RangeSelection | NodeSelection | GridSelection): boolean; // 判断选区的类型
}
另外该模块还有一个类与选区相关,就是 Point 类,它表示选区的一端
源文件
以上这些类在核心模块的 LexicalSelection.ts 文件中进行定义
Point 端点
端点 Point 表示选区的一个端点
说明
GridSelection 网格选区或 RangeSelection 范围选区一般会有两个端点
端点 Point 的实例表示 GridSelection 网格选区或 RangeSelection 范围选区的一端
锚定「不动」的一端,其 Point 类的实例化对象一般使用变量 anchor 表示;而焦点「延伸」的一端,其 Point 类的实例化对象一般使用变量 focus 表示
因为用户可以从左向右框选,也可以从右向左框选,所以 anchor 所对应的光标位置可能在 focus 所对应的光标位置的右侧
通过 new Point(key, offset, type) 进行实例化(以下简称为 point),第一个参数 key 是字符串,表示该端点所在的节点的唯一标识符 ❓ ;第二个参数 offset 一个数值,表示该端点的偏移量;第三个参数 type 表示端点的类型,可以是 "text" | "element" 这两个字符串之一
该端点类的实例化对象具有以下属性:
_selection属性:端点所属的选区,可以是 RangeSelection 范围选区、GridSelection 网格选区或nullkey属性:端点所在的节点的唯一标识符offset属性:端点的偏移量type属性:端点的类型,可以是"text"|"element"这两个字符串之一
该端点类的实例化对象具有以下方法:
point.getNode()获取端点所在的节点point.is(targetPoint)返回一个布尔值,以判断当前端点与给定端点targetNode是否相同point.isBefore(targetPoint)返回一个布尔值,以表示当前端点是否位于给定端点targetPoint之前point.set(key, offset, type)根据给定的参数设置当前端点
RangeSelection 范围选区
范围选区 RangeSelection 是最常见的一种选区类型,表示文本范围或插入符号的当前位置
通过 new RangeSelection(anchor, focus, format, style) 或调用该模块所导出的方法 $createRangeSelection() 进行实例化(以下简称为 rangeSelection)
该选区类的实例化对象具有以下属性:
_cachedNodes属性:该选区内所缓存的节点(数组)或null❓anchor属性:该选区锚定「不动」的一端focus属性:该选区焦点「延伸」的一端dirty属性:一个布尔值,表示选区是否标记为「脏」,选区经过了更改 ❓format属性:一个数值,表示选区预设的格式style属性:一个字符串,表示选区预设的 CSS inline style 行内样式
说明
以上两个属性 format 和 style 的作用可以看作是「光标的预设值」。由于在设置了相应的值后,再继续输入/插入的节点(一般是文本内容)就会具有相应的格式或 CSS 行内样式
该类型的范围选区提供了一些方法,在后续的小节列出
选区信息
rangeSelection.extract()提取选区中的节点(返回一个数组,包含一系列的节点)注意
该方法会执行必要的节点分割 split node 以适应选择文本节点的部分内容的场景
例如链接节点提供一个切换 toggle 功能,可以新建一个链接节点,只对选中的文本进行包裹,使之成为链接
由于用户可能只选中一个文本节点中的部分文字,所以需要使用
rangeSelection.extract()方法基于选区的端点对文本节点进行分隔(创建新的邻近兄弟节点),仅提取出选中的文本,再使用链接节点对这部分的文字(节点)进行包裹rangeSelection.getCharacterOffsets()获取选区两个端点的偏移量,返回一个二维数组[anchorOffset, focusOffset]每个元素都是一个数值,依次表示选区的锚点和焦点的偏移量提示
对于文本/字符串内容,偏移量是按字符个数计算的;对于非文本的节点,则按照子节点的数量计算
rangeSelection.getNodes()获取选区中的节点(返回一个数组,包含一系列的节点)区分
与
rangeSelection.extract()不同,该方法不会对节点进行分割 split node 操作rangeSelection.getTextContent()获取选区中的(纯)文本内容rangeSelection.hasFormat(type)返回一个布尔值,以表示选区中的(所有)文本是否具有给定的格式type
参数type是文本节点格式,可以是"bold"|"underline"|"strikethrough"|"italic"|"highlight"|"code"|"subscript"|"superscript"这几个值之一rangeSelection.is(targetSelection)返回一个布尔值,以表示给定的选区targetSelection是否于当前的选区一样(包括 anchor 锚点、focus 焦点、format 所设置的文本格式、style 所设置的 CSS 样式)rangeSelection.isBackward()返回一个布尔值,以表示该选区的焦点 focus 是否位于锚点 anchor 之前(当用户通过从右往左进行框选时)rangeSelection.isCollapsed()返回一个布尔值,以表示该选区的焦点和锚点是否「坍缩」 collapse 到同一个位置,从视觉上而言就是当前的范围选区是否处于光标状态
操作选区
rangeSelection.applyDOMRange(range)基于给定的 DOM 原生的range设置/生成当前范围选区rangeSelection.setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset)根据给定的节点和偏移值创建一个范围选区。
正如该方法的名称所创建的范围选区与文本节点相关,因为该方法所接受的第一个参数anchorNode和第三个参数focusNode都是 TextNode 文本节点,而第二个参数anchorOffset和第三个参数focusOffset是数值,分别表示选区的两个端点的(基于字符)偏移量。rangeSelection.clone()创建一个该选区的克隆rangeSelection.deleteCharacter(isBackward)删除选区中的字符(执行了字符级删除的逻辑操作,相当于按下一次 Backspace 键或 Delete 键),参数isBackward表示删除的方向是否向后(相当于按下 Delete 键)rangeSelection.deleteWord(isBackward)删除光标前面(如果参数isBackward为true则删除后面)一个单词按单词删除
如果当前范围选区并不是光标状态,则直接删除选中的文本
按单词删除是用户按下
Backspace键 或Delete键 的同时按下Ctrl键(对于 MacOS 用户则是同时按下Alt键)时所预期的行为Lexical 提供了一些内置的指令,它们已经与特定的快捷键(按键事件)绑定,例如当用户按下上述的快捷键时就会分发内置的指令
DELETE_WORD_COMMAND,由于该快捷键在系统的文本编辑器中的预期行为是删除一个单词,所以一般在设置该指令的响应函数时调用这个范围选区的方法rangeSelection.deleteWord()rangeSelection.deleteLine(isBackward)删除光标所在的行的字符(执行了行删除的逻辑操作)按行删除
如果当前范围选区并不是光标状态,则直接删除选中的文本
按行删除只在 MacOS 系统中才起作用,一般是在用户按下
Backspace键 或Delete键 的同时,按下Meta徽标键,执行行删除Lexical 提供了一些内置的指令,它们已经与特定的快捷键(按键事件)绑定,例如当 MacOS 用户按下上述的快捷键时就会分发内置的指令
DELETE_LINE_COMMAND,由于该快捷键在 MacOS 的文本编辑器中的预期行为是删除一行内容,所以一般在设置该指令的响应函数时调用这个范围选区的方法rangeSelection.deleteLine()rangeSelection.formatText(type)为选区中的(所有)文本节点设置给定的格式type
参数type是文本节点格式,可以是"bold"|"underline"|"strikethrough"|"italic"|"highlight"|"code"|"subscript"|"superscript"这几个值之一提示
在设置选区中的文本格式时,可能会进行必要的文本节点分割和融合,由于不同的格式的字符串内容需要使用不同的文本节点进行包裹
设置格式采用的是 toggle 切换(添加或移除)的方式,即如果选区的所有文本已经具有给定的格式时,则再调用该方法的结果是取消给定的格式;如果选区中并不是所有文本具有给定的格式,则为所有文本设置该格式
rangeSelection.toggleFormat(type)设置选区的属性format,切换(添加或移除)给定的格式type
参数type是文本节点格式,可以是"bold"|"underline"|"strikethrough"|"italic"|"highlight"|"code"|"subscript"|"superscript"这几个值之一rangeSelection.setStyle(style)设置选区的属性style为传入的参数值(一般是表示 inline style 行内样式的字符串)
说明
以上两个方法分别用于设置范围选区实例化对象的两个属性 format 和 style,这两个属性的作用可以看作是「光标的预设值」。由于在设置了相应的值后,再继续输入/插入的节点(一般是文本内容)就会具有相应的格式或 CSS 行内样式
rangeSelection.insertLineBreak(selectStart?)在光标位置插入一个 LineBreakNode 换行节点。(可选)参数selectStart是一个布尔值,以表示是否在插入完成时调整光标位置,将光标调整回换行节点之前(相当于将光标留着上一行)提示
如果当前范围选区并不是光标状态,则先删除选中的文本,再插入 LineBreakNode 换行节点
rangeSelection.insertNodes(nodes, selectStart?)在光标位置插入插入给定的一系列节点(数组)nodes。(可选)参数selectStart是一个布尔值,以表示是否在插入完成时调整光标位置,将光标调整回换行节点之前(相当于将光标留着上一行)
返回一个布尔值,以表示是否插入成功提示
如果当前范围选区并不是光标状态,则先删除选中的文本,再插入给定的一系列节点
rangeSelection.insertParagraph()在光标位置插入一个 ParagraphNode 段落节点。提示
如果当前范围选区并不是光标状态,则先删除选中的文本,再插入 ParagraphNode 段落节点
rangeSelection.insertRawText(text)在光标位置插入文本提示
该方法会对给定的文本
text中的一些特殊字符进行转换处理- 将
\n或\r\n字符转换为 LineBreakNode 换行节点 - 将
\t字符转换为 TabNode 缩进节点
- 将
rangeSelection.insertText(text)在光标位置插入文本提示
该方法不会对给定的文本
text中的\t、\n等特殊字符进行转换处理虽然使用该方法将文本插入到页面上,从视觉上而言和使用方法
rangeSelection.insertRawText(text)并无区别(例如对于\n字符,浏览器也会解析为换行操作),但是节点树和 DOM 结构树会有所区别使用该方法插入的文本是扁平化的,即这些字符串都只会包含在一个节点中;而使用
rangeSelection.insertRawText(text)方法插入的文本,如果含有特殊的字符会进行转换,所以传入的文本可能对应得到多个节点rangeSelection.removeText()删除选区中的文本提示
如果当前选区处于光标状态,则不对任何文本删除
rangeSelection.modify(alter, isBackward, granularity)调整/修改当前的范围选区,可以用于移动或扩展 一个「逻辑」单位 logical unit 选区范围
该方法的各个参数的作用如下:- 第一个参数
alter可以是"move"|"extend"这两个值之一,以表示修改的类型 - 第二个参数
isBackward是一个布尔值,以表示移动的方向是否向后 - 第三个参数
granularity可以是"character"|"word"|"lineboundary"这三个值之一,以表示调整的颗粒度/单位,按照"character"字符、"word"单词还是"lineboundary"一行作为一个「逻辑」单位
提示
使用该方法可以安全地调整选区(一个「逻辑」单位),因为该方法会针对各种不同类型的节点执行相应的处理,不需要担心可能出现的边缘情况
- 第一个参数
NodeSelection 节点选区
节点选区 NodeSelection 用于表示选中一系列(非文本)节点(元素节点等)
通过 new NodeSelection(nodeKeyset)(其中参数 nodeKeyset 是一个包含一系列节点 key 唯一标识符的集合),以表示该选区选中的节点)或调用该模块所导出的方法 $createNodeSelection() 进行实例化(以下简称为 nodeSelection)
该选区类的实例化对象具有以下属性:
_nodes属性:一个包含一系列节点 key 唯一标识符的集合,表示该选区选中的节点dirty属性:一个布尔值,表示选区是否标记为「脏」,选区经过了更改 ❓_cachedNodes属性:该选区内所缓存的节点(数组)或null❓
该类型的范围选区提供了一些方法,在后续的小节列出
选区信息
nodeSelection.extract()提取选区中的节点(返回一个数组,包含一系列的节点),实际上是方法nodeSelection.getNodes()的别名nodeSelection.getNodes()获取选区中的节点(返回一个数组,包含一系列的节点)nodeSelection.getTextContent()获取选区中的(纯)文本内容nodeSelection.has(key)返回一个布尔值,以表示当前选区中是否含有给定的节点key(该参数是节点的唯一标识符)nodeSelection.is(selection)返回一个布尔值,以表示给定的选区selection与当前选区是否相同
操作选区
nodeSelection.add(key)将给定的节点key(该参数是节点的唯一标识符)添加到当前的节点选区中nodeSelection.insertNodes(nodes, selectStart)在光标位置插入插入给定的一系列节点(数组)nodes。(可选)参数selectStart是一个布尔值,以表示是否在插入完成时调整光标位置,将光标调整回插入节点之前(从视觉上就相当于将光标不动)
返回true以表示插入成功提示
如果当前范围选区并不是光标状态,则先删除选中的文本,再插入给定的一系列节点
nodeSelection.insertRawText(text)原本该方法的目的是插入文本(而且会对给定的文本text中的一些特殊字符进行转换处理),但是对于节点选区,该方法并不会执行任何操作nodeSelection.insertText()原本该方法的目的是插入文本,但是对于节点选区,该方法并不会执行任何操作nodeSelection.delete(key)将给定的节点key(该参数是节点的唯一标识符)从当前节点选区中删去nodeSelection.clear()清空节点选区(所选中的节点)nodeSelection.clone()创建一个该选区的克隆,而且所选中的节点与原选区相同
GridSelection 网格选区
网格选区 GridSelection 用于表示对网格布局的内容进行框选,例如表格
通过 new GridSelection(gridKey, anchor, focus) 或调用该模块所导出的方法 DEPRECATED_$createGridSelection() 进行实例化(以下简称为 gridSelection)
该选区类的实例化对象具有以下属性:
gridKey属性:表格节点 key 唯一标识符,表示该选区在哪个表格中anchor属性:该选区锚定「不动」的一端focus属性:该选区焦点「延伸」的一端dirty属性:一个布尔值,表示选区是否标记为「脏」,选区经过了更改 ❓_cachedNodes属性:该选区内所缓存的节点(数组)或null❓
该类型的范围选区提供了一些方法,具体可以查看官方文档
变量
该模块导出了一些变量,它们一般是与指令相关的,所以名称以大写字母表示
指令
开发者可以为这些内置指令设置响应函数,以便让编辑器响应用户的特定操作
源文件
以上这些与指令相关的变量在核心模块的 LexicalCommands.ts 文件中进行定义
该模块所导出的一些内置指令已经与相关的 DOM 事件绑定 ❓ 即相应的事件被触发时,就会分发相应的 COMMAND 指令
而有些内置指令则没有绑定相关的 DOM 事件,要分发这些指令的一个常见做法,就是在编辑器的 UI 界面上设置相关按钮,当用户点击时分发相应的指令
另外也可以将指令于按键事件相绑定,例如对于编辑「撤销」与「恢复」相关的指令,可以手动将它们分别于 Ctrl+Z 和 Ctrl+Y 快捷键(按键事件)相绑定,即按下特定的按键组合分发相应的指令,然后再在指令的响应函数中编写相应的操作编辑器的逻辑代码
提示
具体可以在该模块的 LexicalEvent.ts 文件中查看哪些内置指令已经绑定了 DOM 事件,而哪些没有绑定(需要手动设置或触发)
编辑器相关指令
BLUR_COMMAND编辑器失去焦点时分发的指令FOCUS_COMMAND编辑器获得聚焦时分发的指令
编辑器状态相关指令
CAN_REDO_COMMANDCAN_UNDO_COMMANDCLEAR_EDITOR_COMMANDCLEAR_HISTORY_COMMANDREDO_COMMAND执行恢复操作时分发的指令UNDO_COMMAND执行撤销操作时分发的指令CONTROLLED_TEXT_INSERTION_COMMAND当文本插入到编辑器时分发的指令DELETE_CHARACTER_COMMAND删除一个字符时分发的指令DELETE_WORD_COMMAND删除一个单词时分发的指令DELETE_LINE_COMMAND删除一行时分发的指令REMOVE_TEXT_COMMAND在使用「组合」输入模式时删除文本,或通过拖拽或剪切删除文本时,所分发的指令提示
「组合」输入模式是指通过 IME 或第三方扩展接收用户的输入,最终输出的内容并不是键盘上的字母,而是转换为其他语言符号
FORMAT_ELEMENT_COMMANDFORMAT_TEXT_COMMAND设置文本的格式时分发的指令INDENT_CONTENT_COMMANDOUTDENT_CONTENT_COMMANDINSERT_LINE_BREAK_COMMAND插入换行节点时分发的指令INSERT_PARAGRAPH_COMMAND插入段落节点时分发的指令INSERT_TAB_COMMAND
选区相关指令
SELECTION_CHANGE_COMMAND选区改变时分发的指令SELECT_ALL_COMMAND全选时分发的指令(使用快捷键Ctrl+A或Meta+A进行全选)
交互相关指令
CLICK_COMMAND点击编辑器时分发的指令COPY_COMMAND复制时分发的指令CUT_COMMAND剪切时分发的指令PASTE_COMMAND粘贴时分发的指令DRAGSTART_COMMAND拖拽开始时分发的指令DRAGOVER_COMMAND拖拽期间分发的指令DRAGEND_COMMAND拖拽结束时分发的指令DROP_COMMAND拖放(松开释放)时分发的指令KEY_DOWN_COMMAND按下键盘上任意按键时分发的指令KEY_ARROW_UP_COMMAND按下向上键时分发的指令KEY_ARROW_DOWN_COMMAND按下向下键时分发的指令KEY_ARROW_LEFT_COMMAND按下向左键时分发的指令KEY_ARROW_RIGHT_COMMAND按下向右键时分发的指令KEY_BACKSPACE_COMMAND按下 Backspace(向前)删除键时分发指令KEY_DELETE_COMMAND按下 Delete(向后)删除键时分发的指令KEY_ENTER_COMMAND按下 Enter 回车键时分发的指令KEY_ESCAPE_COMMAND按下 Esc 退出键时分发的指令KEY_MODIFIER_COMMAND按下修饰键 modifier 时分发的指令提示
Lexical 会在用户按下
Ctrl|Shift|Alt|Meta任意一种修饰键时,分发以上的指令KEY_SPACE_COMMAND按下 Space 空格键时分发的指令KEY_TAB_COMMAND按下 Tab 缩进键时分发的指令MOVE_TO_END按下向右键的同时按下Ctrl键(或Meta键)时(而且没有按下Shift键或Alt键)分发的指令MOVE_TO_START按下向左键的同时按下Ctrl键(或Meta键)时(而且没有按下Shift键或Alt键)分发的指令
常量
一些常量,赋值给变量再导出,可以通过变量名让这些常量更具有语义
COMMAND_PRIORITY_EDITOR所对应的常量是0COMMAND_PRIORITY_LOW所对应的常量是1COMMAND_PRIORITY_NORMAL所对应的常量是2COMMAND_PRIORITY_HIGH所对应的常量是3COMMAND_PRIORITY_CRITICAL所对应的常量是4
以上变量表示指令的响应函数的执行优先级,数值越大优先级越高
方法
该模块导出了一些通用的方法
注意
其中有一些方法的名称以 $ 符号开头,它们大部分是用于修改编辑器状态 EditorState 的,一般只能在 editor.update(callbackFn) 的回调函数 callbackFn 中实用
源文件
这些通用方法一般在核心模块的 LexicalUtils.ts 文件中进行定义,有些方法则可能在其他相关的文件中定义
编辑器相关
createEditor(editorConfig?)创建返回一个编辑器实例,(可选)参数editorConfig用于预设配置编辑器$addUpdateTag(tag)为当前的编辑器更新添加一个tag标记$hasUpdateTag(tag)返回一个布尔值,以判断在编辑器的一系列更新中,是否具有标记为tag的更新(一般结合历史记录实用 ❓ 以及实现撤销与恢复操作)$setCompositionKey(key)设置编辑器的_compositionKey属性,以表示当前是否处于「组合」输入模式说明
「组合」输入模式是指通过 IME 或第三方扩展接收用户输入,但是最终输出的内容并不是键盘上的字母,而是转换为其他语言符号
getNearestEditorFromDOMNode(domElement)基于给定的 DOM 元素domElement获取最近的编辑器实例(该方法是针对存在嵌套编辑器的场景,通过该方法可以知道给定的 DOM 元素属于哪个编辑器实例)
节点相关
$getNodeByKey(key, _editorState?)基于给定key节点唯一标识符获取对应的节点$getRoot()获取根节点$getNearestRootOrShadowRoot(node)获取给定节点node所在的节点树的根节点或影子根节点说明
影子根节点 ShadowRoot 是指一些节点的行为类似于根节点,一般表示局部的节点树结构的终点/树根,例如表格的单元格节点
TableCellNode就是影子根节点$getAdjacentNode(focus, isBackward)获取给定位置focus(一个选区端点 Point 实例)的邻近的兄弟节点,第二个参数isBackward是一个布尔值,如果是true则表示获取给定位置前面的兄弟节点,如果是false则表示获取给定位置后面的兄弟节点$getNearestNodeFromDOMNode(startingDOM, editorState?)获取离给定的 DOM 元素startingDOM最近的节点(这里的「最近」是指沿着节点树往上回溯找到的节点,所以可能得到 DOM 元素正好对应的节点,或包裹 DOM 元素的父节点)$nodesOfType(Klass)从编辑器的所有节点中提取给定类型Klass的节点,返回一个数组,里面各元素都是该类型的节点实例$hasAncestor(child, targetNode)返回一个布尔值,以判断给定的节点targetNode是否为child节点的祖先节点$copyNode(node)拷贝节点$applyNodeReplacement(node)用于手动触发覆写节点,传入的节点node经过该方法的处理后,返回一个预设的其他类型的节点进行替换$createLineBreakNode()创建一个 lineBreakNode 换行节点$createParagraphNode()创建一个 paragraphNode 段落节点$createTabNode()创建一个 tabNode 缩进节点$createTextNode(text?)创建一个 textNode 文本节点,(可选)参数text用于设置文本内容$isElementNode(node)返回一个布尔值,以判断给定的节点node是否为 ElementNode 元素节点$isBlockElementNode(node)返回一个布尔值,以判断给定的节点node是否为 ElementNode 元素节点且属于 block 块级元素类型$isInlineElementOrDecoratorNode(node)返回一个布尔值,以判断给定的节点node是否为 ElementNode 元素节点且属于 inline 行内元素类型$isLeafNode(node)返回一个布尔值,以判断给定的节点node是否为叶子节点(在该节点里没有子节点)$isDecoratorNode(node)返回一个布尔值,以判断给定的节点node是否为 DecoratorNode 装饰节点$isTextNode(node)返回一个布尔值,以判断给定的节点node是否为 TextNode 文本节点$isRootNode(node)返回一个布尔值,以判断给定的节点node是否为 RootNode 根节点$isRootOrShadowRoot(node)返回一个布尔值,以判断给定的节点node是否为 RootNode 根节点或影子根节点说明
影子根节点 ShadowRoot 是指一些节点的行为类似于根节点,一般表示局部的节点树结构的终点/树根,例如表格的单元格节点
TableCellNode就是影子根节点$isParagraphNode(node)返回一个布尔值,以判断给定的节点node是否为 ParagraphNode 段落节点$isLineBreakNode(node)返回一个布尔值,以判断给定的节点node是否为 LineBreakNode 换行节点$isTabNode(node)返回一个布尔值,以判断给定的节点node是否为 TabNode 缩进节点$insertNodes(node, selectStart?)在光标位置插入给定的一系列节点nodes(数组),第二个(可选)参数selectStart是一个布尔值,以表示是否在插入完成时调整光标位置,将光标调整回插入节点之前(从视觉上就相当于将光标不动)提示
如果当前编辑器状态中没有选区(即选区为
null),则先创建一个位于编辑器最后位置的范围选区,所以给定的一系列节点nodes会插入到 append 编辑器的最后如果当前范围选区并不是光标状态,则先删除选中的文本,再插入给定的一系列节点
$splitNode(node, offset)在特定的偏移位置offset对给定的节点node(元素节点)进行「分割」,返回一个二元数组[elementNode | null, elementNode]它包含「分割」而得的两个节点$parseSerializedNode(serializedNode)将传入的(序列化为 JSON 对象格式的节点)serializedNode反序列化为相应的节点注意
需要在编辑器中注册相应的节点,并在节点中设置了
importJSON()方法调用
$parseSerializedNode(serializedNode)时并不需要显式声明传入的节点类型,该方法会自动从 JSON 对象中提出出相应的信息,并调用相应类型节点的方法进行反序列化
选区相关
$getSelection()获取当前的选区$getPreviousSelection()获取更新前的选区$createRangeSelection()创建一个 rangeSelection 范围选区(位于编辑器的开头,而且处于光标状态)$createNodeSelection()创建一个 nodeSelection 节点选区(但是并没有选中的节点,即选区为空)$isRangeSelection(x)返回一个布尔值,以判断传入的参数x是否为 RangeSelection 范围选区$isNodeSelection(x)返回一个布尔值,以判断传入的参数x是否为 NodeSelection 节点选区$normalizeSelection__EXPERIMENTAL(targetRangeSelection)标准化给定的targetRangeSelection范围选区(让选区的端点位于文本节点内 ❓ )$setSelection(targetSelection)将当前选区设置为传入的目标选区targetSelection$selectAll()全选编辑器的所有内容
文本内容相关
$getTextContent()获取选区中的文本内容
指令相关
createCommand(type?)创建一个自定义指令。(可选)参数type是一个字符串,作为指令的标识符
其他
isHTMLAnchorElement(domElement)返回一个布尔值,判断给定的 DOM 元素domElement是否为锚元素/链接元素isHTMLElement(x)返回一个布尔值,判断给定的参数x(可以是 DOM 元素或 EventTarget 事件对象)是否为 HTML 元素isSelectionCapturedInDecoratorInput(anchorDOM)返回一个布尔值,判断传入的 DOM 元素anchorDOM(一般是通过原生的 DOM 选区获取得到的锚点位置的 DOM 元素 ❓ )判断它是否位于一个 DecoratorNode 装饰节点中isSelectionWithinEditor(editor, anchorDOM, focusDOM)返回一个布尔值,判断传入的 DOM 元素anchorDOM和focusDOM(一般是通过原生的 DOM 选区获取得到的锚点和焦点位置的 DOM 元素 ❓ )是否在给定的编辑器editor内提示
这里所说的「在编辑器内」,并不包含在可编辑的 DecoratorNode 装饰节点内,或在内嵌的编辑器内的清空
因为这些场景下,输入编辑行为都是由其他的机制接管的,而不是给定编辑器
editor可以控制的
TypeScript 类型
Lexical 支持 Typescript,在该核心模块中导出了一些类型 type,便于开发者导入到项目中进行类型检查,具体参考官方文档