ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Lexical 列表功能全解:@lexical/list 包的节点、命令与主题定制指南

Lexical 列表功能全解:@lexical/list 包的节点、命令与主题定制指南 Lexical 列表功能全解lexical/list 包的节点、命令与主题定制指南【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical本篇技术指南以 packages/lexical-list/README.md 为骨架结合仓库内packages/lexical-list/src的完整源码与单元测试系统讲解 Lexical 中列表bullet / number / check 三种类型的原语实现$insertList/$removeList两个核心函数、ListNode/ListItemNode两个节点类、四组便捷命令及其注册方式以及EditorTheme中完整的列表主题配置。读完你将掌握如何在自建 Lexical 编辑器中以原生方式实现列表的插入、移除、缩进与复选交互并能按需定制列表的样式类名。包定位lexical/list是什么lexical/list暴露了在 Lexical 中实现列表所需的全部原语primitives。它并不绑定任何 UI 框架而是由两部分组成见 packages/lexical-list/src/index.tsLexical 节点ListNode列表容器与ListItemNode列表项负责列表状态的存储、DOM 渲染与序列化一组函数$insertList、$removeList等封装了触发典型列表操作插入、移除、缩进、回车拆项的算法。如果你使用 React 构建编辑器且希望实现的是传统列表官方推荐直接使用lexical/react暴露的ListPlugin——它将这些原语封装成了可放入任意LexicalComposer的组件。本指南则深入底层讲解这些原语本身的工作原理方便你在非 React 环境或需要深度定制时直接使用。包的版本信息可参考 packages/lexical-list/package.json当前仓库中为0.50.0MIT 协议其依赖仅为lexical及lexical/extension、lexical/html、lexical/internal、lexical/utils说明它是一层轻量的列表能力封装。核心函数$insertList与$removeListREADME 指出lexical/list的 API 主要由封装列表行为的节点和触发典型列表操作的一组函数组成。两个最重要的函数定义在 packages/lexical-list/src/formatList.ts。$insertList(listType)$insertList会根据当前 Selection 的状态用一套算法决定如何以最合理的方式插入指定类型的列表其签名与语义如下$insertList(listType: number | bullet | check): void从 源码实现 可以梳理出它的分支决策逻辑选区锚点在根节点Root/ShadowRoot若根节点无子节点先补建一个ParagraphNode再执行插入选区锚点是一个空的ListItemNode若该空项直接挂在根节点下则直接将其替换为新ListNode并补一个空ListItemNode继承原项的 format 与 indent若其父级是ListNode则用$newListFrom复制父列表并改换列表类型随后把整棵父列表替换为新类型普通选区例如选中了一段文本遍历选区内的节点把空元素块直接转成列表或沿节点向上寻找最近的ListNode/ 根节点执行创建新列表或合并相邻同类型列表$createListOrMerge。特别注意当选区选中文本时$insertList会尽量把这段文本移入列表的第一个ListItemNode这正是 README 所说的 may try to move it into the first item in the list。而$createListOrMerge的合并逻辑见 formatList.ts#L190-L246会检查前/后兄弟节点若相邻兄弟已是同类型列表则直接复用并追加列表项若两侧都是同类型列表还会合并它们并删除重复节点。$removeList()$removeList依据一组符合常规编辑器行为的启发式规则尝试移除当前选区内的列表。其关键行为在 formatList.ts#L286-L367 中实现若选区锚点落在空的ListItemNode上整棵顶层列表通过$getTopListNode找到的祖先ListNode都会被移除对列表内每个ListItemNode会生成新的ParagraphNode作为替身——这正是 README 强调的 it converts empty ListItemNodes into empty ParagraphNodes 的底层逻辑。$removeList创建段落时会继承列表项的 format、indent、direction 与 style并把原ListItemNode的所有子节点追加进新段落若选区锚点/焦点锚定在被删除的ListItemNode上还会把选区手动迁移到新生成的段落上保证删除后光标位置正确。由于$removeList会以insertionPoint.insertAfter(paragraph)逐个在列表内插入段落因此本质上是一次列表 → 段落的结构解构任何列表项内嵌套的复杂内容都会被完整保留在新的段落节点中。与列表操作相关的其他函数formatList.ts与utils.ts还提供了大量与列表操作配套的内部函数理解它们有助于读懂$insertList/$removeList的行为边界函数作用源码位置mergeLists(list1, list2)递归合并两个列表包括嵌套子列表的同类型合并formatList.ts#L254-L278mergeNextSiblingListIfSameType(list)仅当相邻兄弟列表类型一致如ul对ul而非ul对ol时合并formatList.ts#L398-L406updateChildrenListItemValue(list)根据列表start与嵌套情况重算每个ListItemNode的value非check列表会清空checkedformatList.ts#L375-L391$handleIndent(item)/$handleOutdent(item)对列表项执行缩进/反缩进缩进会创建嵌套的ListNode链反缩进会把项提升到祖父列表formatList.ts#L414-L541$handleListInsertParagraph(restoreNumbering)在空列表项内按 Enter 时插入ParagraphNode并拆出新的列表支持restoreNumbering保留序号formatList.ts#L552-L631$getListDepth(list)计算列表从根节点起的嵌套深度顶层为 1utils.ts#L29-L49$getTopListNode(item)向上寻找最近的祖先ListNode列表的最顶层容器utils.ts#L56-L75$isNestedListNode(node)判断某ListItemNode的第一个子节点是否为ListNode即该列表项是否携带嵌套列表utils.ts#L140-L147节点ListNode与ListItemNodeListNodeListNode继承自ElementNode是列表的容器节点实现于 packages/lexical-list/src/LexicalListNode.ts。它维护三个关键内部状态__listType: ListType取值number | bullet | check__tag: ListNodeTagType取值ul | ol其中number对应ol其余对应ul__start: number有序列表的起始序号默认1非 1 时会在 DOM 上输出start属性。// 创建节点与类型守卫导出自 lexical/list $createListNode(listType: ListType number, start 1): ListNode $isListNode(node: LexicalNode | null | undefined): node is ListNodeListNode的若干行为约束值得注意canBeEmpty()返回false、canIndent()返回false即列表容器不允许为空、也不能被整体缩进缩进应作用于列表项重写的splice会自动把非ListItemNode子节点包装成ListItemNode块级节点会被拆解为子节点后放入新的列表项行内节点则直接装入列表项确保ListNode的子节点结构永远合法createDOM会给 DOM 元素挂上内部标记__lexicalListType并在start ! 1时输出start属性见 LexicalListNode.ts#L124-L136$convertListNodeHTML 导入转换支持ol→number读取start、检测为清单见下→check、其余ul→bullet。其中清单检测isDomChecklist支持三类来源带__lexicalListTypecheck属性的列表、GitHub 风格的.contains-task-list、Joplin 风格的data-is-checklist1以及子元素带aria-checked的 Google Docs 清单粘贴场景。ListNode的序列化 JSON 形状为{ listType, start, tag }叠加ElementNode的字段见exportJSON供持久化使用。ListItemNodeListItemNode同样继承ElementNode表示单个列表项实现于 packages/lexical-list/src/LexicalListItemNode.ts。它维护__value: number该列表项在当前有序列表中的序号配合start使用嵌套列表项不推进计数__checked?: boolean仅当父列表为check类型时有意义undefined表示非复选项。// 创建节点与类型守卫 $createListItemNode(checked?: boolean): ListItemNode $isListItemNode(node: LexicalNode | null | undefined): node is ListItemNodeListItemNode的关键行为toggleChecked()/getChecked()/setChecked()复选状态的读写切换getChecked只有在父列表是check类型时才返回布尔值否则恒为undefinedgetIndent()/setIndent()缩进级别通过祖先ListItemNode的层数计算setIndent内部循环调用$handleIndent/$handleOutdent逐步增减层级collapseAtStart(selection)光标在列表项开头按退格时的行为——嵌套项执行$handleOutdent顶层项则生成ParagraphNode并把后续兄弟拆入新列表replace/insertAfter当用非列表项节点替换/插入在列表项之后时会执行拆分列表逻辑把剩余兄弟列表项复制进一个新ListNode复制时会通过$getNewListStart修正新列表的起始序号避免序号重置相关回归测试见 Issue7032Repro.test.tsDOM 输出对于check类型的叶子列表项会渲染rolecheckbox、tabIndex-1与aria-checked属性从而支持无障碍访问。命令INSERT_* 与 REMOVE_LIST_COMMAND为了方便把列表功能接到 UI如工具栏按钮lexical/list提供了一组命令常量定义于 packages/lexical-list/src/registerList.ts#L38-L49INSERT_UNORDERED_LIST_COMMAND——插入无序列表bulletINSERT_ORDERED_LIST_COMMAND——插入有序列表numberINSERT_CHECK_LIST_COMMAND——插入清单checkREMOVE_LIST_COMMAND——移除列表UPDATE_LIST_START_COMMAND——更新有序列表的起始序号载荷为{ listNodeKey, newStart }README 特别强调这些命令本身不包含任何功能它们只是约定的信号常量必须由你在编辑器中注册对应的命令处理器才能真正改变编辑器状态。README 给出的标准接线方式如下原样保留可直接复制使用// MyListPlugin.ts editor.registerCommand(INSERT_UNORDERED_LIST_COMMAND, () { $insertList(editor, bullet); return true; }, COMMAND_PRIORITY_LOW); // MyInsertListToolbarButton.ts function onButtonClick(e: MouseEvent) { editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); }命令处理器中必须返回true以表示命令已被消费并注意$insertList需要处于editor.update之类的可写上下文中调用。更省事的做法registerList一键注册如果不想手写四个命令处理器可以调用registerList(editor, options?)一次性完成注册实现见 registerList.ts#L55-L183。它内部通过mergeRegister批量注册INSERT_ORDERED_LIST_COMMAND→$insertList(number)INSERT_UNORDERED_LIST_COMMAND→$insertList(bullet)REMOVE_LIST_COMMAND→$removeList()UPDATE_LIST_START_COMMAND→ 更新ListNode的start并重算子项valueINSERT_PARAGRAPH_COMMAND→ 交由$handleListInsertParagraph处理空列表项内的回车选项restoreNumbering为true时拆分出的新列表会继续沿用被拆项的序号KEY_BACKSPACE_COMMAND→ 在列表项开头按退格时调用collapseAtStart优先级为COMMAND_PRIORITY_BEFORE_EDITOR两个节点变换registerNodeTransformListItemNode与其首个文本子节点之间的文本样式/格式同步保证列表项级的加粗、斜体等状态与内容一致// 完整注册含保留序号的选项 const unregister registerList(editor, { restoreNumbering: true }); // 组件卸载时调用 unregister() 清理监听此外还有registerListStrictIndentTransform(editor)严格缩进变换通过ListNode节点变换限制列表项缩进必须与上一个兄弟项的深度对齐以及registerCheckList(editor, options?)清单的键盘与鼠标交互见下节。清单的交互实现registerCheckListINSERT_CHECK_LIST_COMMAND的处理器以及清单checkbox的全部交互逻辑实现在 packages/lexical-list/src/checkList.ts点击清单的复选标记命中测试会读取::before伪元素宽度来界定可点击区域触屏设备会额外加 32px 点击区并处理缩放时toggleChecked切换勾选状态键盘支持Space 切换勾选、上下方向键在复选项之间移动焦点、Escape 把焦点交还编辑器根、左方向键把焦点移到复选框上移动端处理拦截touchstart防止弹出键盘并通过pointerup 500ms 去重窗口解决 iOS Safari / Android Chrome 上preventDefault吞掉合成 click 的问题对应测试见 checkList.test.tsx可选配置disableTakeFocusOnClick为true时点击复选项不会把焦点移入编辑器适合移动端场景。主题定制Theming列表可以通过编辑器初始化配置中传给编辑器的EditorTheme进行样式定制。README 给出的完整主题类型定义如下原样保留{ list?: { // Applies to all lists of type bullet ul?: EditorThemeClassName; // Used to apply specific styling to nested levels of bullet lists // e.g., [ bullet-list-level-one, bullet-list-level-two ] ulDepth?: ArrayEditorThemeClassName; // Applies to all lists of type number ol?: EditorThemeClassName; // Used to apply specific styling to nested levels of number lists // e.g., [ number-list-level-one, number-list-level-two ] olDepth?: ArrayEditorThemeClassName; // Applies to all list items listitem?: EditorThemeClassName; // Applies to all list items with checked property set to true listitemChecked?: EditorThemeClassName; // Applies to all list items with checked property set to false listitemUnchecked?: EditorThemeClassName; // Applies only to list and list items that are not at the top level. nested?: { list?: EditorThemeClassName; listitem?: EditorThemeClassName; }; }; }主题类名是如何被应用的源码视角从源码可以看到这些主题字段的真实作用机制ul/ol与ulDepth/olDepthListNode.createDOM调用$setListThemeClassNames见 LexicalListNode.ts#L245-L303。其中深度类名通过$getListDepth(node) - 1计算当前列表深度再用listDepth % ulDepth.length取模从而在多层嵌套时循环复用你提供的深度类数组。源码还额外支持listTheme.checklistcheck列表的专属类名与listTheme.nested.list深度大于 1 时追加的嵌套列表类名这两个字段 README 未列出但源码已实现可按需使用listitem/listitemChecked/listitemUnchecked/nested.listitemListItemNode.updateListItemDOM调用$setListItemThemeClassNames见 LexicalListItemNode.ts#L559-L613。它会在渲染前先移除旧的变量类名保证类名字符串顺序规范再依据父列表是否为check类型 当前checked值决定追加listitemChecked还是listitemUnchecked同时若列表项携带嵌套ListNode子节点会追加nested.listitem。一个实用的最小主题示例import type {EditorThemeClasses} from lexical; const theme: EditorThemeClasses { list: { ul: list-ul, ulDepth: [list-ul-level-one, list-ul-level-two, list-ul-level-three], ol: list-ol, olDepth: [list-ol-level-one, list-ol-level-two], listitem: list-item, listitemChecked: list-item-checked, listitemUnchecked: list-item-unchecked, nested: { list: list-nested, listitem: list-item-nested, }, }, };由于深度类名采用取模循环即使嵌套层级超过数组长度样式也能正确回环到第一档。从原语到扩展ListExtension与CheckListExtension除节点与函数外本仓库还为lexical/list提供了基于扩展体系的集成方式见 packages/lexical-list/src/LexicalListExtension.tsListExtension注册ListNode/ListItemNode节点依赖CoreImportExtension与DOMImportExtension以支持 HTML 导入管线并自动调用registerList。其配置项ListConfig包含hasStrictIndent: boolean默认false为true时启用registerListStrictIndentTransform严格缩进shouldPreserveNumbering: boolean默认false对应registerList的restoreNumbering选项控制拆分列表时是否保留序号。CheckListExtension依赖ListExtension内部调用registerCheckList配置项disableTakeFocusOnClick默认false。对应的 HTML 导入规则定义于 packages/lexical-list/src/ListImportExtension.ts它通过ListImportRules含 GitHubtask-list-item与 Joplincheckbox-wrapper的专有规则、ol/ul与li通用规则以及ListSchema只接受ListItemNode与直接嵌套的ListNode其余子节点自动包装成列表项保证粘贴的 HTML 能规范地还原成列表结构。测试佐证行为有据可依lexical/list的每个核心行为在 packages/lexical-list/src/tests/unit/ 下都有对应测试覆盖阅读这些用例可以快速验证上文描述的语义LexicalListNode.test.ts 与 LexicalListItemNode.test.ts节点创建、序列化、DOM 输出与类型守卫formatList.test.ts$insertList/$removeList的插入、合并与转换为段落的完整流程InsertListKeepsListState.test.ts 与 RemoveListKeepsItemState.test.ts插入/移除列表时对原有 format、indent 等状态的保留checkList.test.tsx复选清单的点击命中测试与移动端触屏切换含::before宽度的 jsdom stub 技巧ListNodeDepthThemeClass.test.ts嵌套深度主题类名的取模应用Issue7032Repro.test.ts列表拆分时序号保留的回归用例。小结组合使用路径要在自己的 Lexical 编辑器中启用完整列表能力可以按两条路径组合React 快速路径直接使用lexical/react的ListPluginlexical/react/LexicalListPlugin开箱即用原生深度路径将ListNode/ListItemNode注册进editorConfig.nodes调用registerList(editor)如需清单交互再叠加registerCheckList(editor)或使用本仓库的ListExtension/CheckListExtension通过扩展管线一键装配随后在工具栏按钮中dispatchCommand(INSERT_ORDERED_LIST_COMMAND | INSERT_UNORDERED_LIST_COMMAND | INSERT_CHECK_LIST_COMMAND | REMOVE_LIST_COMMAND, undefined)并在主题中按上文结构定制list样式。这样即可获得与官方 Playground 一致的列表插入、回车拆项、退格出列、缩进与复选交互体验。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表