ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

tldraw 富文本多图形批量格式化:用 StylePanel 与 richText 文档遍历实现一键加粗

tldraw 富文本多图形批量格式化:用 StylePanel 与 richText 文档遍历实现一键加粗 tldraw 富文本多图形批量格式化用 StylePanel 与 richText 文档遍历实现一键加粗【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw在 tldraw SDK 中文本型图形的富文本内容以 TipTapProseMirrorJSON 文档形式存放在shape.props.richText中本指南将基于官方示例 Format rich text on multiple shapes位于 README.md讲解如何定制样式面板对当前选中的所有文本、几何、便签、箭头图形实现一键加粗/取消加粗的批处理能力。读完本文你将掌握richText文档的数据结构与遍历方式、通过ExtractShapeByProps收窄图形类型、利用editor.updateShapes原子化回写数据以及用useValue保持 UI 状态实时同步的完整套路。案例要解决的问题单个面板按钮影响所有选中图形的全部文本默认的 tldraw 样式面板StylePanel中的粗体等格式只作用于当前正在编辑的文本。而本示例的目标是提供一次批量操作用户框选多个图形可以是文字框、几何图形、便签或带标签的箭头混合选择亦可点击面板中的 Bold all text 按钮所有选中图形中凡是包含文本的其每一个文本节点都会被加上粗体标记再次点击则统一去除。这一能力的关键点在于不必逐个进入文本编辑态而是直接以编程方式programmatic改写每个图形的富文本props面板按钮的可用/激活状态随选区实时变化只要有任一选中图形含富文本即可点击当所有文本节点都已是粗体时按钮呈激活态此时点击执行的是取消粗体。示例主文件为 RichTextFormatOnMultipleShapesExample.tsx下面按实现链路逐层拆解。理解底层数据结构shape.props.richText就是一份 JSON 文档要批处理富文本必须先弄清它的存储形态。在 TLRichText.ts 中富文本校验器richTextValidator定义了它的顶层结构export const richTextValidator T.object({ type: T.string, content: T.arrayOf(T.unknown), attrs: T.any.optional(), })即一个TLRichText对象至少包含type与content两个字段。真实的文档形如 ProseMirror/TipTap 的嵌套树{ type: doc, content: [ { type: paragraph, content: [ { type: text, text: Hello world! }, { type: text, text: bold text, marks: [{ type: bold }] }, ], }, ], }规则归纳为根节点是doc其content是一组块节点block如paragraphparagraph内部是内联节点inline node其中type: text的节点用text字段存放字符串内容每个文本节点可以通过可选的marks数组携带行内样式粗体即{ type: bold }marks不存在的文本节点视为无格式多个 mark 可以共存如同时bold与italic。同文件中还提供了官方工具函数toRichText(text: string): TLRichText它把普通字符串逐行拆分成独立段落方便快速构造富文本上述文档结构可直接用于校验器.check()验证。哪些图形持有 richText在 tlschema 的图形定义中richText由以下形状的props承载仓库源码分别给出字段声明并复用richTextValidator文本图形TLTextShape.ts 中的richText: TLRichText几何图形矩形/椭圆/菱形等TLGeoShape.ts便签TLNoteShape.ts箭头用于标签文本TLArrowShape.ts。这正好解释了为什么示例能对文本 几何 便签 箭头的混合选区生效——它们共享同名字段、同一套 JSON 文档格式。类型收窄用ExtractShapeByProps把选区过滤成含富文本的图形示例第一步是定义只有带richText的图形才是处理对象的类型type ShapeWithRichText ExtractShapeByProps{ richText: TLRichText } function isShapeWithRichText(shape: TLShape | null | undefined): shape is ShapeWithRichText { return !!(shape richText in shape.props) }ExtractShapeByProps是编辑器包导出tldraw/editor的工具类型用于从全部形状类型中提取出包含某组 props 的子类型。这里传{ richText: TLRichText }得到的结果恰好就是 text / geo / note / arrow 四类在 tldraw 包内部文本渲染组件 RichTextLabel.tsx 与useEditableRichText.ts也以同样方式收窄类型。isShapeWithRichText是配套的运行时守卫把TLShape | null | undefined收窄为ShapeWithRichText之后访问shape.props.richText就是类型安全的。三个遍历助手在 JSON 文档上递归加粗、判断、去粗示例没有引入解析器而是直接手工遍历上述 JSON 文档写成三个小助手。它们共用同一递归模式遇到type text的节点则处理marks否则若节点有content数组就逐子递归叶子节点如硬换行保持原样。1.makeAllTextBold给每个缺失 bold mark 的文本节点补 markfunction makeAllTextBold(richText: TLRichText): TLRichText { if (!richText || !richText.content) { return richText } const processNode (node: any): any { if (node.type text) { const hasBold node.marks?.some((mark: any) mark.type bold) if (hasBold) { return node } return { ...node, marks: [...(node.marks || []), { type: bold }], } } if (node.content Array.isArray(node.content)) { return { ...node, content: node.content.map(processNode), } } return node } return { ...richText, content: richText.content.map(processNode), } }实现要点用展开运算符...node保留原节点其余字段只增改marks从而保证文档其余结构原封不动已带粗体的节点直接原样返回避免重复添加{ type: bold }该函数是纯函数返回全新对象绝不原地修改入参。2.isAllTextBold判断整篇文档是否已全部加粗function isAllTextBold(richText: TLRichText): boolean { if (!richText || !richText.content) { return false } const checkNode (node: any): boolean { if (node.type text) { return node.marks?.some((mark: any) mark.type bold) ?? false } if (node.content Array.isArray(node.content)) { return node.content.every(checkNode) } // Leaf nodes without text (hard breaks, etc.) have nothing to bold return true } return richText.content.every(checkNode) }注意语义上的细节空文本节点、硬换行等不含文字的叶子节点无需加粗因此直接返回true不计入未加粗状态文本节点则要求至少含一个boldmark 才算通过。整体用every表达所有文本都已粗体这一条件作为按钮激活态的判定依据。3.removeBoldFromAllText过滤掉所有 bold markfunction removeBoldFromAllText(richText: TLRichText): TLRichText { if (!richText || !richText.content) { return richText } const processNode (node: any): any { if (node.type text) { const marks node.marks?.filter((mark: any) mark.type ! bold) || [] return { ...node, marks: marks.length 0 ? marks : undefined, } } if (node.content Array.isArray(node.content)) { return { ...node, content: node.content.map(processNode), } } return node } return { ...richText, content: richText.content.map(processNode), } }通过filter去掉所有bold类型的 mark其余 mark如 italic、链接等都会被保留过滤后若marks为空数组则显式置为undefined让 JSON 回到无 marks 字段的干净形态。由此批量切换粗体在逻辑上等价于当前不全粗体 → 用makeAllTextBold已全粗体 → 用removeBoldFromAllText。回写编辑器run 历史标记点 updateShapes原子化提交三个助手只负责构造新文档真正落库要靠编辑器 API。示例中的切换逻辑如下editor.run(() { editor.markHistoryStoppingPoint(toggle bold) editor.updateShapes( shapesWithRichText.map((shape) ({ id: shape.id, type: shape.type, props: { richText: allBold ? removeBoldFromAllText(shape.props.richText) : makeAllTextBold(shape.props.richText), }, })) ) })这里有三层语义对应编辑器底层的三条纪律不直接 mutation图形对象由编辑器统一管理任何改动都必须通过editor.updateShapes提交局部更新这里只带id、type、props.richText其余字段不传即保持不变updateShapes会把传入的数组一次性地应用到所有目标图形上markHistoryStoppingPoint(toggle bold)在批操作前设置历史停止点使得这次批量修改在撤销/重做历史中被记录为单一步骤——用户按一次 Ctrl/CmdZ 即可整体撤销本次全选加粗而不是回退 N 次editor.run把以上逻辑包进一个原子事务让这批更新以及任何衍生副作用在运行时保持一致避免中间状态外泄。以上做法在示例文件源码注释中被明确总结Shapes are never mutated directly; we always pass a newrichTextobject through the editor。自定义 StylePanel包装默认面板、按选区注入按钮按钮的位置在样式面板顶部。示例用组合而非替换的思路自定义CustomStylePanel接收TLUiStylePanelProps该 props 定义于 DefaultStylePanel.tsx含isMobile、styles、children渲染DefaultStylePanel并在其内部插入自己的节最后仍然渲染DefaultStylePanelContent /保住默认样式控件function CustomStylePanel(props: TLUiStylePanelProps) { const editor useEditor() const shapesWithRichText useValue( shapes with rich text, () editor.getSelectedShapes().filter(isShapeWithRichText), [editor] ) const hasRichTextSelection shapesWithRichText.length 0 const allBold hasRichTextSelection shapesWithRichText.every((shape) isAllTextBold(shape.props.richText)) // ... return ( DefaultStylePanel {...props} div classNametlui-style-panel__section TldrawUiButton typemenu isActive{allBold} onClick{handleToggleBold} titleBold all text in selected shapes disabled{!hasRichTextSelection} TldrawUiButtonIcon iconbold / TldrawUiButtonLabelBold all text/TldrawUiButtonLabel /TldrawUiButton /div DefaultStylePanelContent / /DefaultStylePanel ) }值得单独强调的是useValue的作用它来自tldraw/editor的响应式状态原语传入计算函数 依赖数组。这里把读取当前选中图形并过滤出含富文本者放进useValue的回调中意味着只要选区变化、或图形文本内容被编辑按钮状态都会自动重新计算并驱动界面更新——这正是它和一次性useState的关键区别也让hasRichTextSelection、allBold永远与画布当前状态一致。由此得出按钮的两个状态规则disabled{!hasRichTextSelection}没有任何选中图形含富文本时置灰isActive{allBold}全部文本节点已粗体时高亮且此时点击走取消粗体分支。UI 使用 tldraw 自带的TldrawUiButton/TldrawUiButtonIconicon 传内置的bold/TldrawUiButtonLabel组合保持与默认面板一致的观感与 a11y 行为。集成到编辑器通过components.StylePanel覆盖默认面板最后把自定义面板挂到Tldraw组件上仅需覆盖components中的StylePanel一个键其余 UI工具栏、画布、上下文菜单等全部保持默认const components: TLComponents { StylePanel: CustomStylePanel, } export default function RichTextFormatOnMultipleShapesExample() { return ( div classNametldraw__editor Tldraw components{components} / /div ) }在 examples 工程中示例采用占满父容器高度的tldraw__editor容器包裹Tldraw并全局引入tldraw/tldraw.css样式。替换 StylePanel 的机制在所有 tldraw 应用里通用因此这段代码可直接迁移到基于 tldraw SDK 的 React 应用中。延伸与 Text mass style updates 示例的取舍原文档最后提到一个互补方案。仓库内的 TextMassStyleUpdates.tsx其 README 同处 examples/ui 目录实现了更强大的批量格式化同样基于富文本是 TipTap/ProseMirror JSON 文档这一前提但改用ProseMirror 的Node.fromJSON官方解析 API配合基于tipTapDefaultExtensions构建的 schema把 JSON 解析成真实文档树后统一加/去 mark支持 bold、italic、highlight 多种 mark通过editor.getSortedChildIdsForParent递归展开frame / group 内的嵌套文本用editor.getShapeAndDescendantIds扩展选区因此能处理框选一个装满文字的 Frame这类场景界面同样是包装DefaultStylePanel的自定义 StylePanel改动通过editor.updateShape回写。取舍可以这样理解本文方案直接遍历 JSON零依赖、代码量小、纯数据结构操作适合粗体开关这类简单 mark 与扁平选区mass style updates 方案依托 ProseMirror 文档模型更接近文本编辑器内部语义扩展 mark 类型、嵌套容器时更稳健。两者共享同一套editor.run/ 历史停止点 /updateShapes回写纪律。关键要点速查数据模型富文本 { type: doc, content: [...] }嵌套 JSON存于 text / geo / note / arrow 图形的props.richTextschema 见 TLRichText.ts粗体标记文本节点marks数组中含{ type: bold }即视为粗体mark 可叠加遍历策略文本节点处理 marks含content的容器节点递归无文本叶子硬换行等直接放行三个助手分别负责加粗 / 判定全粗 / 去粗均为纯函数写回纪律绝不直接改 shape统一走editor.updateShapes局部更新props.richText用markHistoryStoppingPoint使整批操作成为单次撤销步骤并包在editor.run原子事务内类型收窄ExtractShapeByProps{ richText: TLRichText } 运行时守卫把混合选区安全过滤为可处理的形状集合UI 联动在useValue回调里读取editor.getSelectedShapes()让按钮的disabled与isActive状态随选区/文本实时更新集成方式自定义组件包装DefaultStylePanel追加按钮与DefaultStylePanelContent /再经TLComponents.StylePanel注入Tldraw能力边界本例处理顶层图形文档需要覆盖 frame/group 内嵌套文本或更多 mark 时参考 text-mass-style-updates 的 ProseMirrorNodeAPI 方案。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表