drag-and-drop functionality works on a basic level but fails when dealing with deeply nested...

Discussão em 'Angular' iniciado por Hung Pham, Fevereiro 15, 2025.

  1. Hung Pham

    Hung Pham Guest

    I'm working on a dynamic nested tree structure using Angular CDK Drag & Drop. The drag-and-drop feature works at the first level but fails when trying to drop elements deeper in the hierarchy. The cdkDropListDropped event is not triggered at deeper levels. (after lv12)

    demo: https://nested-tree-angular.netlify.app

    code source: https://github.com/phamhung075/nested-tree

    How can I ensure drag-and-drop works at all nested levels? Is there a limitation with CDK Drag & Drop for deep structures? Any suggestions for fixing or debugging this issue?

    // tree.component.ts
    import {
    CdkDrag,
    CdkDragDrop,
    CdkDropList,
    DragDropModule,
    moveItemInArray,
    } from '@angular/cdk/drag-drop';
    import { CommonModule } from '@angular/common';
    import {
    ChangeDetectionStrategy,
    ChangeDetectorRef,
    Component,
    EventEmitter,
    Input,
    OnInit,
    Output,
    } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { TreeNode } from '@shared/interfaces/tree-node.model';
    import { TreeService } from '@shared/services/tree/tree.service';

    @Component({
    selector: 'app-tree',
    standalone: true,
    imports: [CommonModule, FormsModule, DragDropModule],
    templateUrl: './branch-display.component.html',
    styleUrls: ['./branch-display.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class TreeComponent implements OnInit {
    @Input() node!: TreeNode;
    @Input() isLastChild = false;
    @Input() isRoot = true;
    @Input() dropListIds: string[] = [];
    @Output() onDelete = new EventEmitter<string>();
    @Output() registerDropList = new EventEmitter<string>();

    dropListId = `drop-list-${Math.random().toString(36).substring(2)}`;
    private hasBeenInitialized = false;

    constructor(
    private treeService: TreeService,
    private changeDetectionRef: ChangeDetectorRef
    ) {}

    ngOnInit() {
    if (!this.hasBeenInitialized) {
    this.registerDropList.emit(this.dropListId);
    console.log('Tree Component Initialized:', {
    nodeId: this.node.id,
    value: this.node.value,
    children: this.node.children.length,
    });
    this.hasBeenInitialized = true;
    }
    }

    canDrop = (drag: CdkDrag, drop: CdkDropList) => {
    const dragData = drag.data as TreeNode;
    const dropData = this.node; // Current node where we're trying to drop

    // Prevent dropping on itself or its descendants and root
    if (
    dragData.id === dropData.id ||
    this.isDescendant(dragData, dropData) ||
    dropData.id === 'root'
    ) {
    return false;
    }
    return true;
    };

    private isDescendant(dragNode: TreeNode, targetNode: TreeNode): boolean {
    return targetNode.children.some(
    (child: TreeNode) =>
    child.id === dragNode.id || this.isDescendant(dragNode, child)
    );
    }

    drop(event: CdkDragDrop<TreeNode[]>) {
    const draggedNode = event.item.data as TreeNode;

    if (event.previousContainer === event.container) {
    // Moving within the same container
    moveItemInArray(
    event.container.data,
    event.previousIndex,
    event.currentIndex
    );
    } else {
    // Moving to a different container
    const success = this.treeService.moveNode(
    draggedNode.id,
    this.node.id,
    'inside',
    event.currentIndex // Pass the current index for position-based insertion
    );

    if (success) {
    console.log('Node moved successfully to:', {
    targetNode: this.node.value,
    position: event.currentIndex,
    });
    }
    }
    }

    moveUpLevel() {
    const currentParent = this.treeService.getParentNode(this.node.id);
    if (!currentParent) {
    console.log('Cannot move up: No parent found');
    return;
    }

    const grandParent = this.treeService.getParentNode(currentParent.id);
    if (!grandParent) {
    console.log('Cannot move up: No grandparent found');
    return;
    }

    // Find the index where the current parent is in the grandparent's children
    const parentIndex = grandParent.children.findIndex(
    (child: TreeNode) => child.id === currentParent.id
    );

    if (parentIndex === -1) {
    console.log('Cannot move up: Parent index not found');
    return;
    }

    // Move the node one level up
    const success = this.treeService.moveNode(
    this.node.id,
    grandParent.id,
    'inside',
    parentIndex + 1 // Insert after the current parent
    );

    if (success) {
    console.log('Node moved up successfully:', {
    nodeId: this.node.id,
    newParentId: grandParent.id,
    position: parentIndex + 1,
    });
    }
    }

    // Update the tree service to include better logging
    removeChild(childId: string) {
    const index = this.node.children.findIndex(
    (child: TreeNode) => child.id === childId
    );
    if (index !== -1) {
    const removedNode = this.node.children[index];
    this.node.children.splice(index, 1);
    console.log('Removed child:', {
    childId,
    parentId: this.node.id,
    parentValue: this.node.value,
    });
    }
    }

    addChild() {
    const newNode: TreeNode = {
    id: Date.now().toString(),
    value: 'New Node',
    children: [],
    };
    this.treeService.updateNodeMaps(newNode, this.node.id);
    this.node.children.push(newNode);
    }

    deleteNode() {
    this.onDelete.emit(this.node.id);
    }

    onDragStarted() {
    document.body.classList.add('dragging');
    }

    onDragEnded() {
    document.body.classList.remove('dragging');
    }

    onRegisterDropList(childDropListId: string) {
    if (!this.dropListIds.includes(childDropListId)) {
    this.dropListIds.push(childDropListId);
    this.registerDropList.emit(childDropListId);
    }
    }
    }



    // tree.service.ts

    import { Injectable } from '@angular/core';
    import { TreeNode } from '@shared/interfaces/tree-node.model';

    @Injectable({
    providedIn: 'root',
    })
    export class TreeService {
    private nodeMap = new Map<string, TreeNode>();
    private parentMap = new Map<string, string>();

    getRegisteredNodes(): string[] {
    return Array.from(this.nodeMap.keys());
    }

    updateNodeMaps(node: TreeNode, parentId?: string) {
    console.log('Updating node maps:', {
    nodeId: node.id,
    parentId,
    nodeValue: node.value,
    });

    this.nodeMap.set(node.id, node);
    if (parentId) {
    this.parentMap.set(node.id, parentId);
    }

    console.log('Current maps after update:', {
    nodeMapSize: this.nodeMap.size,
    parentMapSize: this.parentMap.size,
    nodeMapKeys: Array.from(this.nodeMap.keys()),
    parentMapKeys: Array.from(this.parentMap.keys()),
    });

    node.children.forEach((child: TreeNode) =>
    this.updateNodeMaps(child, node.id)
    );
    }

    findNodeById(id: string): TreeNode | undefined {
    const node = this.nodeMap.get(id);
    console.log('Finding node by id:', {
    searchId: id,
    found: !!node,
    nodeValue: node?.value,
    });
    return node;
    }

    getParentNode(nodeId: string): TreeNode | undefined {
    const parentId = this.parentMap.get(nodeId);
    const parentNode = parentId ? this.nodeMap.get(parentId) : undefined;
    console.log('Getting parent node:', {
    childId: nodeId,
    parentId,
    foundParent: !!parentNode,
    parentValue: parentNode?.value,
    });
    return parentNode;
    }

    private isDescendant(nodeId: string, targetId: string): boolean {
    console.log('Checking if descendant:', {
    nodeId,
    targetId,
    });

    let currentNode = this.findNodeById(targetId);
    let depth = 0;

    while (currentNode && depth < 1000) {
    console.log('Traversing up the tree:', {
    currentNodeId: currentNode.id,
    currentNodeValue: currentNode.value,
    depth,
    });

    if (currentNode.id === nodeId) {
    console.log('Found ancestor match - would create circular reference');
    return true;
    }
    currentNode = this.getParentNode(currentNode.id);
    depth++;
    }

    console.log('No circular reference found');
    return false;
    }

    moveNode(
    nodeId: string,
    targetId: string,
    position: 'before' | 'after' | 'inside',
    insertIndex?: number
    ): boolean {
    console.log('Starting moveNode operation:', {
    nodeId,
    targetId,
    position,
    insertIndex,
    targetNodeValue: this.findNodeById(targetId)?.value,
    });

    const sourceNode = this.findNodeById(nodeId);
    const targetNode = this.findNodeById(targetId);

    if (!sourceNode || !targetNode) {
    console.log('Move failed: Source or target node not found');
    return false;
    }

    const sourceParent = this.getParentNode(nodeId);
    if (!sourceParent) {
    console.log('Move failed: Source parent not found');
    return false;
    }

    // Check for circular reference
    if (this.isDescendant(nodeId, targetId)) {
    console.log('Move failed: Would create circular reference');
    return false;
    }

    // Remove from old parent
    sourceParent.children = sourceParent.children.filter(
    (child: TreeNode) => child.id !== nodeId
    );

    // Add to new location
    if (position === 'inside') {
    if (typeof insertIndex === 'number' && insertIndex >= 0) {
    // Insert at specific position
    targetNode.children.splice(insertIndex, 0, sourceNode);
    } else {
    // Default behavior: append to end
    targetNode.children.push(sourceNode);
    }
    this.parentMap.set(nodeId, targetId);
    } else {
    const targetParent = this.getParentNode(targetId);
    if (!targetParent) {
    console.log('Move failed: Target parent not found');
    return false;
    }

    const targetIndex = targetParent.children.findIndex(
    (child: TreeNode) => child.id === targetId
    );
    const insertPosition =
    position === 'after' ? targetIndex + 1 : targetIndex;
    targetParent.children.splice(insertPosition, 0, sourceNode);
    this.parentMap.set(nodeId, targetParent.id);
    }

    console.log('Move completed successfully. New structure:', {
    movedNodeId: nodeId,
    newParentId: targetId,
    newParentValue: targetNode.value,
    insertPosition: insertIndex,
    });

    return true;
    }
    }



    // app-tree.component.ts (Parent component)
    import { CommonModule } from '@angular/common';
    import {
    ChangeDetectionStrategy,
    ChangeDetectorRef,
    Component,
    OnInit,
    } from '@angular/core';
    import { TreeComponent } from '../../components/branch-display/branch-display.component';
    import { TreeNode } from '@shared/interfaces/tree-node.model';
    import { TreeService } from '@shared/services/tree/tree.service';
    import { mockTreeData } from '../../components/branch-display/mock-data';

    @Component({
    selector: 'app-tree-container',
    standalone: true,
    imports: [TreeComponent, CommonModule],
    templateUrl: './branch-display-container.component.html',
    styleUrls: ['./branch-display-container.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class AppTreeContainer implements OnInit {
    treeData: TreeNode = mockTreeData;
    dropListIds: string[] = [];
    constructor(
    private treeService: TreeService,
    private changeDetectionRef: ChangeDetectorRef
    ) {}

    ngOnInit() {
    // Register all nodes in the tree with the service
    this.registerNodesRecursively(this.treeData);
    console.log('Tree Container Initialized');
    }

    private registerNodesRecursively(node: TreeNode, parentId?: string) {
    // Register the current node
    this.treeService.updateNodeMaps(node, parentId);
    console.log('Registered node:', { nodeId: node.id, parentId });

    // Register all children
    node.children.forEach((child: TreeNode) => {
    this.registerNodesRecursively(child, node.id);
    });
    }

    onRegisterDropList(id: string) {
    if (!this.dropListIds.includes(id)) {
    this.dropListIds = [...this.dropListIds, id];
    }
    }
    }

    Continue reading...

Compartilhe esta Página