如何在Angular中用ng2-dragula实现跨层级拖拽(父/子/孙互转)
实现ng2-dragula跨层级嵌套拖拽
要让子级(如e)能拖拽成为父级,或孙级(如i)的层级,核心是打破原有的同组容器限制,允许元素在不同层级的容器间拖拽,并同步更新数据结构。以下是具体实现步骤:
1. 重构Dragula组配置
原代码分了3个独立组(parents/children/grandChildren),导致元素只能在同组容器内拖拽。我们需要合并为一个组,通过accepts配置允许跨层级容器接受拖拽元素:
if (this.dragulaService.find('nested-drag') === undefined) { this.dragulaService.createGroup('nested-drag', { removeOnSpill: false, // 保留原有的拖拽手柄判断逻辑 moves: (el, container, handle) => { return handle.classList.contains('parentHandle') || handle.classList.contains('childHandle') || handle.classList.contains('grandchildHandle'); // 孙级拖拽手柄 }, // 允许元素拖入任意层级容器,可根据需求添加限制 accepts: (el, target, source, sibling) => { return true; } }); }
2. 统一处理拖拽事件
移除原有的三组独立事件订阅,改为监听统一组的drag和drop事件,记录拖拽元素的原始层级、父ID和位置:
// 存储拖拽元素的原始信息 private dragOrigin: { element: HTMLElement, level: number, parentId: number | null, index: number } | null = null; // 监听拖拽开始,记录原始数据 this.dragulaService.drag('nested-drag').pipe(takeUntil(this.destroyed$)).subscribe(({ el, source }) => { const parentContainer = source.parentElement; let level = 1; let parentId: number | null = null; // 通过容器类识别层级 if (parentContainer?.classList.contains('child-container')) { level = 2; parentId = +parentContainer.parentElement?.id!; } else if (parentContainer?.classList.contains('grandchild-container')) { level = 3; parentId = +parentContainer.parentElement?.id!; } this.dragOrigin = { element: el, level, parentId, index: this.domIndexOf(el, source) }; }); // 监听拖拽完成,处理跨层级更新 this.dragulaService.drop('nested-drag').pipe(takeUntil(this.destroyed$)).subscribe(({ el, target, sibling }) => { if (!this.dragOrigin) return; // 识别目标容器层级 let targetLevel = 1; let targetParentId: number | null = null; const targetIndex = this.domIndexOf(el, target); if (target.classList.contains('child-container')) { targetLevel = 2; targetParentId = +target.parentElement?.id!; } else if (target.classList.contains('grandchild-container')) { targetLevel = 3; targetParentId = +target.parentElement?.id!; } // 根据原始层级调用对应更新逻辑 switch (this.dragOrigin.level) { case 1: this.handleParentToOther(this.dragOrigin, targetLevel, targetParentId, targetIndex); break; case 2: this.handleChildToOther(this.dragOrigin, targetLevel, targetParentId, targetIndex); break; case 3: this.handleGrandChildToOther(this.dragOrigin, targetLevel, targetParentId, targetIndex); break; } this.dragOrigin = null; });
3. 实现数据更新逻辑
假设你的节点数据模型如下:
interface TreeNode { id: number; name: string; parentId: number | null; level: number; children?: TreeNode[]; }
以子级元素(如e)跨层级拖拽为例,实现数据同步:
private handleChildToOther(origin: typeof this.dragOrigin, targetLevel: number, targetParentId: number | null, targetIndex: number) { // 1. 从原父节点中移除元素 const originalParent = this.treeNodes.find(node => node.id === origin.parentId); if (originalParent?.children) { originalParent.children.splice(origin.index, 1); } // 2. 根据目标层级添加元素 const draggedNode = this.treeNodes.flatMap(node => node.children || []).find(child => child.id === +origin.element.id! ); if (!draggedNode) return; if (targetLevel === 1) { // 拖为父级:加入根节点数组 draggedNode.parentId = null; draggedNode.level = 1; this.treeNodes.splice(targetIndex, 0, draggedNode); } else if (targetLevel === 3) { // 拖为孙级:加入目标子节点的children const targetParent = this.treeNodes.flatMap(node => node.children || []).find(child => child.id === targetParentId); if (targetParent?.children) { draggedNode.parentId = targetParentId; draggedNode.level = 3; targetParent.children.splice(targetIndex, 0, draggedNode); } } else if (targetLevel === 2) { // 拖为其他父级的子级 const targetParent = this.treeNodes.find(node => node.id === targetParentId); if (targetParent?.children) { draggedNode.parentId = targetParentId; targetParent.children.splice(targetIndex, 0, draggedNode); } } // 触发视图更新 this.treeNodes = [...this.treeNodes]; }
4. 调整DOM容器标识
给不同层级容器添加明确类名,方便识别层级:
<!-- 根容器(父级元素容器) --> <div class="parent-container"> <div *ngFor="let parent of treeNodes" id="{{parent.id}}" class="parent-item"> <span class="parentHandle">拖拽手柄</span> {{parent.name}} <!-- 子级容器 --> <div class="child-container"> <div *ngFor="let child of parent.children" id="{{child.id}}" class="child-item"> <span class="childHandle">拖拽手柄</span> {{child.name}} <!-- 孙级容器 --> <div class="grandchild-container"> <div *ngFor="let grandchild of child.children" id="{{grandchild.id}}" class="grandchild-item"> <span class="grandchildHandle">拖拽手柄</span> {{grandchild.name}} </div> </div> </div> </div> </div> </div>
关键注意事项
- 数据一致性:拖拽后必须同步更新节点的
parentId和level字段,确保视图与数据匹配。 - 索引准确性:
domIndexOf函数需正确计算元素在容器中的位置,避免插入错误。 - 性能优化:节点数量较多时,给
*ngFor添加trackBy提升渲染性能。
内容的提问来源于stack exchange,提问作者Vince Cyriac
相关产品推荐
相关产品推荐

