完整指南:Tab 缩进原理、深度模型与默认样式解析)
Draft.js 嵌套列表Nested Lists完整指南Tab 缩进原理、深度模型与默认样式解析【免费下载链接】draft-jsA React framework for building text editors.项目地址: https://gitcode.com/gh_mirrors/dr/draft-jsDraft.js 在框架层面内置了对嵌套列表的原生支持允许像 Facebook Notes 编辑器那样通过Tab和ShiftTab快捷键为列表项增加或减少缩进层级。本文以 docs/Advanced-Topics-Nested-Lists.md 为核心骨架结合仓库源码深入讲解嵌套列表的启用方式、底层实现RichUtils.onTab与adjustBlockDepthForContentState、深度数据模型depth字段、渲染机制ul/ol包裹与 CSS 计数器以及默认样式DraftStyleDefault.css的细节帮助你在自己的编辑器中快速实现并深度定制多级列表。嵌套列表能力概述Draft.js 的嵌套列表能力已在 Facebook Notes 编辑器中被验证其核心交互是按下Tab增加当前列表项的缩进深度按下ShiftTab减少当前列表项的缩进深度。这一行为由RichUtils模块提供的onTab方法统一管理。对于绝大多数嵌套列表需求RichUtils.onTab已经足够你只需要把它挂到Editor的onTabprop 上即可。同时默认情况下Draft.js 会通过DraftStyleDefault.css为列表项应用合适的间距margin、padding和列表样式行为项目符号、编号计数器。需要特别注意的能力边界当前 Draft.js 只支持对ordered-list-item和unordered-list-item两种块类型进行深度depth调整其他块类型调用onTab不会产生任何效果。启用嵌套列表onTab prop在 React 组件中将RichUtils.onTab作为Editor的onTabprop 传入即可import {Editor, EditorState, RichUtils} from draft-js; class MyEditor extends React.Component { constructor(props) { super(props); this.state {editorState: EditorState.createEmpty()}; } onChange editorState { this.setState({editorState}); }; handleTab event { const {editorState} this.state; const newState RichUtils.onTab(event, editorState); if (newState ! editorState) { this.onChange(newState); } }; render() { return ( Editor editorState{this.state.editorState} onChange{this.onChange} onTab{this.handleTab} / ); } }onTab是Editor官方支持的 prop 之一在 DraftEditorProps.js 中被声明为onTab?: (e: SyntheticKeyboardEvent) void。RichUtils本身则通过 src/Draft.js 将RichTextEditorUtil导出为RichUtils。Tab 键事件在编辑器中如何流转在 editOnKeyDown.js 中Keys.TAB分支会先尝试调用被标记为 deprecated 的onTabhandlercase Keys.TAB: if (callDeprecatedHandler(onTab)) { return; } break;callDeprecatedHandler的逻辑是如果editor.props.onTab存在则调用它并返回true表示事件已处理。因此只要你通过onTabprop 传入RichUtils.onTabTab 键的默认跳焦行为就会被接管转而调整列表深度。注意RichUtils.onTab内部会调用event.preventDefault()见下文源码防止浏览器默认的焦点切换。底层原理RichUtils.onTab 源码剖析RichUtils.onTab定义在 RichTextEditorUtil.jsonTab(event, editorState) { const selection editorState.getSelection(); const key selection.getAnchorKey(); const content editorState.getCurrentContent(); const block content.getBlockForKey(key); const type block.getType(); if (type ! unordered-list-item type ! ordered-list-item) { return editorState; } event.preventDefault(); const withAdjustment adjustBlockDepthForContentState( content, selection, event.shiftKey ? -1 : 1, ); return EditorState.push(editorState, withAdjustment, adjust-depth); }关键逻辑解读类型守卫先取出当前光标所在块anchor block若其类型既不是unordered-list-item也不是ordered-list-item直接原样返回editorState——这就是「只支持两种列表类型」限制的来源。阻止默认行为仅当命中列表类型时才调用event.preventDefault()避免浏览器移动焦点。方向判定event.shiftKey为真时调整值为-1减少深度否则为1增加深度。状态提交通过EditorState.push(editorState, withAdjustment, adjust-depth)将深度调整作为一个可撤销的adjust-depth变更压入编辑历史。对应的类型签名可以在 RichTextUtils.js 中看到onTab: (event: SyntheticKeyboardEvent, editorState: EditorState) EditorState。adjustBlockDepthForContentState真正执行深度调整的函数深度调整的核心逻辑位于 adjustBlockDepthForContentState.jsfunction adjustBlockDepthForContentState(contentState, selectionState, adjustment, maxDepth) { const startKey selectionState.getStartKey(); const endKey selectionState.getEndKey(); let blockMap contentState.getBlockMap(); const blocks blockMap .toSeq() .skipUntil((_, k) k startKey) .takeUntil((_, k) k endKey) .concat([[endKey, blockMap.get(endKey)]]) .map(block { let depth block.getDepth() adjustment; depth Math.max(0, depth); if (maxDepth ! null) { depth Math.min(depth, maxDepth); } return block.set(depth, depth); }); blockMap blockMap.merge(blocks); return contentState.merge({ blockMap, selectionBefore: selectionState, selectionAfter: selectionState, }); }要点选区覆盖从startKey到endKey之间含两端的所有块都会被调整深度因此多块选区可以一次整体缩进。下限钳制depth Math.max(0, depth)保证深度永远不会为负ShiftTab在 0 深度时是空操作。可选上限maxDepth参数可限制最大嵌套深度RichUtils.onTab目前未传该参数即默认无上限但 CSS 默认样式只覆盖 5 层见下文。返回新 ContentStateonTab拿到新的contentState后通过EditorState.push入栈整个操作可被 CtrlZ 撤销。depth 在数据模型中如何存储每个内容块ContentBlock都携带一个depth字段。在 ContentBlock.js 中默认值被定义为depth: 0并提供getDepth()访问器见同文件 L83-L85。adjustBlockDepthForContentState正是通过block.set(depth, depth)修改这一字段。块类型集合定义在 DraftBlockType.js其中ordered-list-item和unordered-list-item是两种核心列表块类型。渲染机制li ul/ol 包裹与深度 class嵌套列表的视觉层级是「纯视觉」的——Draft.js 的块数据结构是扁平存储的每个列表项仍是一个独立的ContentBlock嵌套效果完全由渲染层的包裹元素和 CSS 缩进实现。块渲染映射DefaultDraftBlockRenderMap.js 定义了两类列表块的渲染方式unordered-list-item: { element: li, wrapper: UL_WRAP, // ul classNamepublic/DraftStyleDefault/ul / }, ordered-list-item: { element: li, wrapper: OL_WRAP, // ol classNamepublic/DraftStyleDefault/ol / },即每个列表项渲染为li连续的列表项会被包裹进ul或olUL_WRAP/OL_WRAP定义在同文件 L26-L27。深度 class 的拼接在 DraftEditorContents-core.react.js 中getListItemClasses根据块的类型、深度、计数器重置状态和文本方向拼出默认 classconst getListItemClasses (type, depth, shouldResetCount, direction) { return cx({ public/DraftStyleDefault/unorderedListItem: type unordered-list-item, public/DraftStyleDefault/orderedListItem: type ordered-list-item, public/DraftStyleDefault/reset: shouldResetCount, public/DraftStyleDefault/depth0: depth 0, public/DraftStyleDefault/depth1: depth 1, public/DraftStyleDefault/depth2: depth 2, public/DraftStyleDefault/depth3: depth 3, public/DraftStyleDefault/depth4: depth 4, public/DraftStyleDefault/listLTR: direction LTR, public/DraftStyleDefault/listRTL: direction RTL, }); };注意depth4的判定是depth 4即第 5 层及以上的所有深度都复用depth4这个 class。另外在 DraftEditorContents-core.react.js 中有一段注释 List items are special snowflakes, since we handle nesting and counters manually说明渲染层手动维护了嵌套包裹与计数器重置shouldResetCount逻辑当深度变化或包裹模板切换时需要重置对应层级的计数器。如果提供了blockStyleFn用户自定义 class 会通过joinClasses与默认 class 合并见 L195-L210不会覆盖默认列表样式。默认样式DraftStyleDefault.css 详解列表默认样式全部位于 DraftStyleDefault.css。以下是与嵌套列表直接相关的核心规则。列表容器与缩进.public/DraftStyleDefault/ul, .public/DraftStyleDefault/ol { margin: 16px 0; padding: 0; }ul/ol默认上下边距 16px、无内边距。缩进通过深度 class 实现每个层级递增 1.5emLTR 方向为 margin-leftRTL 方向为 margin-right深度 classLTR 缩进depth0margin-left: 1.5emdepth1margin-left: 3emdepth2margin-left: 4.5emdepth3margin-left: 6emdepth4margin-left: 7.5emCSS 文件注释明确说明L50-L55默认只提供五级嵌套的计数与样式如果需要超过五级的嵌套必须使用自己的 CSS 类例如通过blockStyleFn注入。如果关心 RTL 语言自定义规则也应参照这些noflip规则编写。无序列表的项目符号.public/DraftStyleDefault/unorderedListItem { list-style-type: square; position: relative; } .public/DraftStyleDefault/unorderedListItem.public/DraftStyleDefault/depth0 { list-style-type: disc; } .public/DraftStyleDefault/unorderedListItem.public/DraftStyleDefault/depth1 { list-style-type: circle; }规则是「前两层之后统一使用 square」depth0用实心圆点discdepth1用空心圆circledepth2及以上用方块square。有序列表的 CSS 计数器有序列表不使用原生ol的编号而是完全由 CSS counter 管理文件注释 L123-L126 明确说明 Ordered list item counters are managed with CSS, since all list nesting is purely visual。核心规则如下depth0content: counter(ol0) . 递增计数器ol0阿拉伯数字depth1content: counter(ol1, lower-alpha) . 递增ol1小写字母depth2content: counter(ol2, lower-roman) . 递增ol2小写罗马数字depth3content: counter(ol3) . 递增ol3depth4content: counter(ol4, lower-alpha) . 递增ol4。编号通过:before伪元素绝对定位渲染如left: -36px; width: 30px; text-align: rightRTL 方向则镜像到右侧。计数器的重置通过resetclass 完成depth0.reset { counter-reset: ol0 }、depth1.reset { counter-reset: ol1 }……每个层级都有对应的重置规则这正是渲染层shouldResetCount逻辑配合使用的部分——当进入更深层级或切换到新的列表包裹时该层计数器归零。因此如果默认的编号样式如首层数字、第二层字母、第三层罗马数字不符合需求直接覆盖这些:before规则即可超过五级则需自行添加计数器规则。列表类型切换把普通段落变成列表项嵌套列表的前提是块已经是列表类型。使用RichUtils.toggleBlockType可以在普通段落与列表项之间切换。仓库示例 examples/draft-0-10-0/rich/rich.html 展示了工具栏中UL/OL按钮的接入方式{label: UL, style: unordered-list-item}, {label: OL, style: ordered-list-item},toggleBlockType的实现位于 RichTextEditorUtil.js若起始块的类型已是目标块类型则切换回unstyled这是它的切换语义否则通过DraftModifier.setBlockType将选区内的块设为目标类型同时它会检测选区内是否包含atomic块若有则放弃操作。测试验证onTab 的深度行为仓库测试 RichTextEditorUtil-test.js 直接验证了onTab对两种列表类型的深度递增行为test(increases the depth of unordered-list-item, () { const contentState editorState.getCurrentContent(); const setListItem setListBlock(contentState, unordered-list-item); const withListItem changeBlockType(setListItem); const afterFirstTab addTab(withListItem); expect(getFirstBlockDepth(afterFirstTab)).toBe(1); const afterSecondTab addTab(afterFirstTab); expect(getFirstBlockDepth(afterSecondTab)).toBe(2); });测试中addTab传入的 mock 事件对象形如{preventDefault: () {}}无shiftKey对应adjustment 1从depth 0连续两次 Tab 后深度变为2。ordered-list-item有相同用例。此外EditorState-test.js 也在toggleBlockType之后调用onTab验证深度调整不破坏编辑状态。这些测试从侧面印证了深度调整作用于「选区覆盖的所有块」并且每次调整都生成一次新的EditorState可进入撤销栈。总结与定制建议回顾 Draft.js 嵌套列表的实现要点启用把RichUtils.onTab绑到Editor的onTabprop即可获得Tab/ShiftTab的缩进能力类型守卫保证只有两种列表块会被处理。数据嵌套层级存放在ContentBlock.depth默认 0中adjustBlockDepthForContentState负责对选区内的块做±1调整并钳制下限为 0。渲染列表项渲染为li并被包裹进ul/olDraftEditorContents根据depth拼接depth0–depth4class并维护每层计数器的重置。样式默认样式位于 DraftStyleDefault.css缩进每级 1.5em无序列表disc → circle → square有序列表用 CSS counter 实现「数字 → 小写字母 → 小写罗马数字」的分层编号。边界默认 CSS 只覆盖 5 层嵌套超过 5 层或需要自定义编号/项目符号样式时应通过blockStyleFn提供自己的 CSS 类并参照默认规则编写 RTL 兼容版本。如需更深入的配套知识可继续阅读仓库中的 APIReference-RichUtils.md、APIReference-ContentBlock.md 以及块渲染相关的 Advanced-Topics-Block-Components.md。【免费下载链接】draft-jsA React framework for building text editors.项目地址: https://gitcode.com/gh_mirrors/dr/draft-js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考