open-pencil SDK 进阶:useLayerDrag 实现图层树拖拽重排与跨容器移动的完整指南
发布时间:2026/9/27 10:12:42 作者:尧图编辑部 阅读量:1,286

前端桌面应用AI 应用MCP 服务【免费下载链接】open-pencilAI-native design editor. Open-source Figma alternative.项目地址https://gitcode.com/gh_mirrors/op/open-pencil点击查看免费下载useLayerDrag是 open-pencil Vue SDK 中负责图层树Layer Tree拖拽交互的底层接线工具它基于 pragmatic-drag-and-drop 把拖到哪、怎么放解析为具体的编辑器重排指令。本文以 packages/docs/fr/programmable/sdk/api/advanced/use-layer-drag.md 为骨架结合 useLayerDrag.ts 源码与核心编辑器重排实现带你掌握如何在自定义图层树中接入拖拽重排、跨容器移动和自动展开等能力。一、useLayerDrag 是什么useLayerDrag(options)为图层树的每一行提供拖拽所需的状态与动作。它计算三个关键信息拖放目标drop target指针当前悬停在哪一行上插入位置position落在目标行的之前 / 内部 / 之后before / inside / after变更有效性validity该次拖放是否合法例如不能把容器拖入自身的后代。同时它负责把拖拽结果转换为editor的重排操作。值得注意的是当一个对象跨容器移动时其视觉位置会被保留——这是由底层reorderChildWithUndo实现的稍后详述。在 API 层级上useLayerDrag属于 Navigation editing 一组的进阶 API见 packages/docs/fr/programmable/sdk/api/advanced/index.md服务于那些标准组件无法满足定制需求的场景。二、签名与返回值函数签名export function useLayerDrag( editor: Editor, indentPerLevel 16, onMakeChildDrop?: (targetId: string) void )参数说明参数类型默认值说明editorEditor—来自open-pencil/core/editor的编辑器实例提供graph、state与重排操作indentPerLevelnumber16每一层缩进的像素数用于计算命中区域的宽度决定上方 / 内部 / 下方判定onMakeChildDrop(targetId: string) void—当元素被放成某容器子节点时回调可用来展开该容器返回值{ draggingId: Refstring | null // 当前正在拖拽的行 id instruction: RefTreeInstruction | null // 当前的放置指令 instructionTargetId: Refstring | null // 指令作用的目标行 id setupItem: (el: RefHTMLElement | null, item: () DragItem) void // 把某行接上拖拽/放置能力 }其中TreeInstruction即LayerDragInstruction只有三种取值定义于 packages/vue/src/primitives/LayerTree/context.tsexport interface LayerDragInstruction { type: reorder-above | reorder-below | make-child }DragItem是行数据的形状interface DragItem { id: string level: number // 当前层级供缩进命中计算 hasChildren: boolean // 是否有子节点决定 expanded / standard 模式 parentId: string | null }三、内部工作流从拖拽到重排整个流程由setupItem注册的若干 pragmatic-drag-and-drop 适配器以及一个全局monitorForElements共同完成。1. 行级接线setupItemsetupItem(el, item)接收目标 DOM 元素与一个返回行数据的函数通过watchEffect在元素可用时自动接线useLayerDrag.tsfunction setupItem(el: RefHTMLElement | null, item: () DragItem) { watchEffect((onCleanup) { const element el.value if (!element) return const data item() const isContainer editor.graph.isContainer(data.id) const mode: ItemMode data.hasChildren ? expanded : standard // ... draggable dropTargetForElements onCleanup(cleanup) }) }draggable让行可被拖起getInitialData提供源节点 id拖起/落下时维护draggingIddropTargetForElements让行可作为放置目标其getData用attachInstruction根据指针位置、缩进宽度indentPerLevel、当前层级与模式计算放置指令。放置模式与合法性由block数组控制这是本工具最关键的一处策略block: isContainer ? [reparent] : [make-child, reparent]含义是目标是容器时允许把它作为父容器放子节点进去但禁止把别的节点重定位到它之上/之下reparent被 block目标是普通叶子节点时既不允许做成它的子节点也不允许跨级重定位只能插到它上方或下方。同时canDrop禁止拖到自身canDrop: ({ source }) source.data.id ! data.idonDrag中通过extractInstruction取出指令若为instruction-blocked被 block 的非法操作则清空指令与目标。getIsSticky: () true保证拖拽悬停时目标行保持粘性。2. 全局落点解析monitorForElements拖放结束时全局 monitor 统一处理useLayerDrag.tsconst cleanupMonitor monitorForElements({ onDrop: ({ source, location }) { const target location.current.dropTargets.at(0) if (!target) return // 解析 sourceId / targetId / rawInstruction ... if (editor.graph.isDescendant(targetId, sourceId)) return // 防拖入自身后代 const targetParentId targetNode.parentId ?? editor.state.currentPageId const targetIndex targetParent.childIds.indexOf(targetId) if (inst.type reorder-above) { editor.reorderChildWithUndo(sourceId, targetParentId, targetIndex) } else if (inst.type reorder-below) { editor.reorderChildWithUndo(sourceId, targetParentId, targetIndex 1) } else { const container editor.graph.getNode(targetId) if (!container || !editor.graph.isContainer(targetId)) return editor.reorderChildWithUndo(sourceId, targetId, container.childIds.length) onMakeChildDrop?.(targetId) } } }) onScopeDispose(cleanupMonitor)三种指令的落点换算指令换算后的重排调用reorder-above插入到目标行在其父节点的当前位置targetIndexreorder-below插入到目标行之后targetIndex 1make-child插入到目标容器的末尾childIds.length并触发onMakeChildDrop回调两条安全校验值得注意editor.graph.isDescendant(targetId, sourceId)直接拦截把节点拖进自己或自己后代的非法操作make-child分支再次用isContainer复核目标确实是容器。四、视觉位置保留的底层原理文档强调对象更换容器时视觉位置被保留这是核心编辑器层reorderChildWithUndo的职责实现在 packages/core/src/editor/structure/reorder.tsfunction reorderChildWithUndo(nodeId: string, newParentId: string, insertIndex: number) { assertNodeEditable(ctx.graph, nodeId) assertNodeEditable(ctx.graph, newParentId) const node ctx.graph.getNode(nodeId) if (!node) return const origParentId node.parentId ?? ctx.state.currentPageId const origIndex ctx.graph.getNode(origParentId)?.childIds.indexOf(nodeId) ?? 0 const origX node.x const origY node.y ctx.graph.reorderChild(nodeId, newParentId, insertIndex) ctx.runLayoutForNode(newParentId) if (origParentId ! newParentId) ctx.runLayoutForNode(origParentId) // ... 压入 undo 栈forward/inverse 分别记录重做与撤销 }关键点它记录origParentId、origIndex、origX、origY并连同forward/inverse一起压入撤销栈因此每次拖拽重排都可撤销同时注意同文件中的doReorderChild使用了getAbsolutePosition差值换算坐标而reorderInAutoLayout专门处理自动布局layoutMode ! NONE父容器内的重排。useLayerDrag统一走reorderChildWithUndo由内部runLayoutForNode负责新旧父容器的布局重算从而维持画布上视觉位置的连续性与稳定性。五、与 LayerTreeRoot / useLayerTree 的配合useLayerDrag不是孤立 API它被 LayerTreeRoot.vue 直接消费并与上下文 API 深度绑定。1. LayerTreeRoot 中的使用LayerTreeRoot是 SDK 的无头结构原语headless structural primitive负责渲染图层树并提供树模型、展开状态、选择/可见性/锁定/重命名等交互接线同时把useLayerDrag的结果注入上下文const { draggingId, instruction, instructionTargetId, setupItem } useLayerDrag( editor, indentPerLevel, expandNode // 作为 onMakeChildDrop拖成子节点时自动展开容器 )注意这里第三个参数传的是expandNode——当节点被拖成某容器的子节点时该容器会被自动展开这正是文档所说onMakeChildDrop回调的典型用途。随后整个树在编辑器事件驱动下增量重建const unsubscribe [ editor.onEditorEvent(graph:replaced, rebuildTree), editor.onEditorEvent(page:changed, rebuildTree), editor.onEditorEvent(node:created, scheduleTreeRebuild), editor.onEditorEvent(node:deleted, scheduleTreeRebuild), editor.onEditorEvent(node:reparented, scheduleTreeRebuild), editor.onEditorEvent(node:reordered, scheduleTreeRebuild), editor.onEditorEvent(node:updated, patchTreeNode), editor.onEditorEvent(selection:changed, onSelectionChanged) ]node:reparented与node:reordered都会触发树的微任务级重建scheduleTreeRebuild合并同一轮改动保证拖拽导致的层级变化立即反映在界面上。2. 通过上下文提供LayerTreeRoot把setupDrag: setupItem、draggingId、instruction、instructionTargetId等一并provideLayerTree(...)进上下文见 context.ts 中的LayerTreeContext接口。自定义行组件内部可用useLayerTree()对应 use-layer-tree 文档读取这些状态并调用ctx.setupDrag(el, item)完成行级接线const ctx useLayerTree() ctx.setupDrag(rowEl, () ({ id: node.id, level: row.level, hasChildren: row.hasChildren, parentId: node.parentId }))3. 与 LayerTreeItem 的分工LayerTreeRoot提供树结构与交互接线的无头根组件LayerTreeItem渲染单行并暴露选择、展开、可见性、锁定、重命名处理器通过默认插槽useLayerDrag提供拖拽状态与指令解析useLayerTree读取根组件注入的上下文。因此自定义拖拽 UI 的推荐做法是在LayerTreeRoot的默认插槽内渲染自有行结构行上调用setupItem或上下文setupDrag接线拖拽状态从draggingId/instruction/instructionTargetId读取以驱动高亮与占位样式。六、在自定义图层树中接入拖拽最小示例假设你要替换默认图层树行的拖拽样式自定义行组件可以这样写script setup langts import { useLayerTree } from #vue/primitives/LayerTree/context const props defineProps{ nodeId: string; level: number; hasChildren: boolean }() const ctx useLayerTree() const rowEl refHTMLElement | null(null) ctx.setupDrag(rowEl, () ({ id: props.nodeId, level: props.level, hasChildren: props.hasChildren, parentId: ctx.editor.graph.getNode(props.nodeId)?.parentId ?? null })) /script template div refrowEl :class{ drag-source: ctx.draggingId nodeId, drop-above: ctx.instruction?.type reorder-above ctx.instructionTargetId nodeId, drop-below: ctx.instruction?.type reorder-below ctx.instructionTargetId nodeId, drop-inside: ctx.instruction?.type make-child ctx.instructionTargetId nodeId } slot / /div /template要点必须处于LayerTreeRoot内部否则useLayerTree会抛出[open-pencil] useLayerTree() called outside LayerTreeRoot见 context.tssetupDrag的item回调应返回实时的id / level / hasChildren / parentId拖拽命中计算依赖level与缩进宽度用draggingId标记正在拖拽的源行用instructioninstructionTargetId组合渲染上方 / 下方 / 内部三种放置指示器若希望在拖成子节点时展开目标容器把展开函数作为useLayerDrag的onMakeChildDrop传入LayerTreeRoot正是这样做的。七、注意事项与最佳实践合法性与自防呆block策略与canDrop禁止拖到自身、isDescendant禁止拖入自身后代三重防护共同保证了非法放置不会落库缩进宽度影响判定indentPerLevel决定above / inside / below三种区域划分的宽度默认 16px若你的行高或缩进不同应显式传入匹配值可撤销所有落点最终都进入reorderChildWithUndo的撤销栈reorder.ts无需在 UI 层额外处理 undo事件驱动刷新拖拽引起的结构变化会触发node:reparented/node:reordered事件由LayerTreeRoot增量重建树无需手动刷新生命周期setupItem在元素卸载时通过onCleanup(cleanup)释放监听全局 monitor 通过onScopeDispose清理避免内存泄漏。八、相关 API 导航LayerTreeRoot无头树结构原语负责上下文注入与模型构建LayerTreeItem单行渲染原语暴露选择/展开/可见性/锁定/重命名处理器useLayerTree读取LayerTreeRoot注入的上下文源码useLayerDrag.ts、LayerTreeRoot.vue、context.ts、reorder.ts。赞分享前端桌面应用AI 应用MCP 服务【免费下载链接】open-pencilAI-native design editor. Open-source Figma alternative.项目地址https://gitcode.com/gh_mirrors/op/open-pencil点击查看免费下载相关推荐Open-Pencil 图层树拖拽排序useLayerDrag 组合式 API 原理与二次开发指南Open Pencil 图层树拖拽排序useLayerDrag 组合式 API 原理与二次开发指南 useLayerDrag 是 Open Pencil 设计前端桌面应用AI 应用MCP 服务Open Pencil useLayerDrag 解析把 pragmatic-drag-and-drop 拖放指令翻译成图层重排操作Open Pencil useLayerDrag 解析把 pragmatic drag and drop 拖放指令翻译成图层重排操作 useLayerDrag前端桌面应用AI 应用MCP 服务Draggable 实战指南使用 Sortable 实现列表拖拽排序与跨容器重排Draggable 实战指南使用 Sortable 实现列表拖拽排序与跨容器重排 本篇技术指南聚焦 Shopify Draggable 项目仓库路径 gh_前端UI组件上一篇3分钟掌握B站会员购抢票神器免费开源工具完整指南下一篇PotPlayer字幕翻译插件让外语视频瞬间变中文的神器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考