You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 12:23:16