npm package: @libs-ui/components-drag-drop
Description: > Thư viện Angular directives hỗ trợ kéo thả (drag and drop) linh hoạt, hỗ trợ kéo thả giữa các container, virtual scrolling, auto-scroll và custom boundary.
Latest version: 0.2.356-23

# @libs-ui/components-drag-drop

> Thư viện Angular directives hỗ trợ kéo thả (drag and drop) linh hoạt, hỗ trợ kéo thả giữa các container, virtual scrolling, auto-scroll và custom boundary.

## Version

`0.2.355-14`

## Author

Mobio Dev Team

## Khi nào sử dụng

- Khi cần sắp xếp lại thứ tự các phần tử trong một danh sách bằng thao tác kéo thả.
- Khi cần di chuyển phần tử giữa nhiều container (cross-container drag and drop).
- Khi cần triển khai Kanban board với nhiều cột có thể kéo thả qua lại.
- Khi danh sách lớn cần kết hợp virtual scroll để tối ưu hiệu năng.
- Khi cần giới hạn vùng kéo thả trong một boundary cụ thể.
- Khi cần auto-scroll khi kéo phần tử gần mép container.

## Lưu ý quan trọng

⚠️ **Two-way binding**: `[(items)]` là bắt buộc trên Container directive. Danh sách items sẽ tự động cập nhật khi kéo thả.

⚠️ **Group Name**: Khi kéo thả giữa các container, cần thiết lập `[groupName]` và `[dropToGroupName]` để xác định nguồn và đích.

⚠️ **Virtual Scroll**: Khi sử dụng virtual scroll, cần bật `[itemInContainerVirtualScroll]="true"` trên Item directive và cung cấp `[fieldId]` để định danh item.

⚠️ **ViewEncapsulation**: Mặc định là `'emulated'`. Chuyển sang `'none'` nếu cần style xuyên qua shadow DOM.

⚠️ **Mode copy**: Khi `[mode]="'copy'"`, item sẽ được sao chép thay vì di chuyển sang container đích.

⚠️ **Performance**: Sử dụng `[throttleTimeHandlerDraggingEvent]` để throttle event dragging, giảm tải khi danh sách lớn.

⚠️ **Padding thay vì Margin**: Khoảng cách giữa các drag item **PHẢI** dùng `padding` (ví dụ `pb-2`), **KHÔNG** dùng `margin` (ví dụ `mb-2`). Khi dùng margin, vùng margin giữa các item không thuộc bất kỳ element nào, khiến thư viện không phát hiện được vị trí hover chính xác và sẽ thêm phần tử xuống cuối danh sách thay vì vị trí đang hover.

## Cài đặt & Import

```typescript
import {
  LibsUiComponentsDragContainerDirective,
  LibsUiDragItemDirective,
  LibsUiComponentsDragScrollDirective,
  LibsUiDragItemInContainerVirtualScrollDirective,
} from '@libs-ui/components-drag-drop';
```

## Ví dụ sử dụng

### Basic - Sắp xếp danh sách đơn giản

```typescript
import { Component } from '@angular/core';
import {
  LibsUiComponentsDragContainerDirective,
  LibsUiDragItemDirective,
} from '@libs-ui/components-drag-drop';

@Component({
  selector: 'app-basic-drag-drop',
  standalone: true,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div
      LibsUiComponentsDragContainerDirective
      [(items)]="items">
      <div
        *ngFor="let item of items"
        LibsUiDragItemDirective
        [item]="item">
        {{ item.name }}
      </div>
    </div>
  `,
})
export class BasicDragDropComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' },
  ];
}
```

### Cross Container - Kéo thả giữa hai container

```typescript
@Component({
  standalone: true,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div class="flex gap-4">
      <!-- Container nguồn -->
      <div
        LibsUiComponentsDragContainerDirective
        [(items)]="sourceItems"
        [groupName]="'source'"
        [dropToGroupName]="['target']"
        (outDroppedContainer)="onDrop($event)">
        <div
          *ngFor="let item of sourceItems"
          LibsUiDragItemDirective
          [item]="item">
          {{ item.name }}
        </div>
      </div>

      <!-- Container đích -->
      <div
        LibsUiComponentsDragContainerDirective
        [(items)]="targetItems"
        [groupName]="'target'"
        [dropToGroupName]="['source']"
        (outDroppedContainer)="onDrop($event)">
        <div
          *ngFor="let item of targetItems"
          LibsUiDragItemDirective
          [item]="item">
          {{ item.name }}
        </div>
      </div>
    </div>
  `,
})
export class CrossContainerComponent {
  sourceItems = [
    { id: 1, name: 'Source 1' },
    { id: 2, name: 'Source 2' },
  ];
  targetItems = [
    { id: 3, name: 'Target 1' },
    { id: 4, name: 'Target 2' },
  ];

  onDrop(event: IDrop) {
    console.log('Dropped:', event);
  }
}
```

### Kanban Board

```typescript
@Component({
  standalone: true,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div class="flex gap-4">
      @for (column of columns; track column.id) {
        <div class="flex-1 bg-gray-100 p-4 rounded-lg">
          <h3>{{ column.title }}</h3>
          <div
            LibsUiComponentsDragContainerDirective
            [(items)]="column.items"
            [groupName]="column.id"
            [dropToGroupName]="allGroupNames"
            [directionDrag]="'vertical'">
            @for (item of column.items; track item.id) {
              <div
                class="bg-white p-3 mb-2 rounded shadow-sm"
                LibsUiDragItemDirective
                [item]="item">
                {{ item.title }}
              </div>
            }
          </div>
        </div>
      }
    </div>
  `,
})
export class KanbanBoardComponent {
  columns = [
    { id: 'todo', title: 'To Do', items: [{ id: 1, title: 'Task 1' }] },
    { id: 'doing', title: 'Doing', items: [{ id: 2, title: 'Task 2' }] },
    { id: 'done', title: 'Done', items: [{ id: 3, title: 'Task 3' }] },
  ];
  allGroupNames = ['todo', 'doing', 'done'];
}
```

### Virtual Scroll

```typescript
@Component({
  standalone: true,
  imports: [
    LibsUiComponentsDragContainerDirective,
    LibsUiDragItemDirective,
    LibsUiComponentsDragScrollDirective,
    LibsUiDragItemInContainerVirtualScrollDirective,
  ],
  template: `
    <virtual-scroller #scroll [items]="items">
      <div
        LibsUiComponentsDragContainerDirective
        LibsUiComponentsDragScrollDirective
        [(items)]="items">
        <div
          *ngFor="let item of scroll.viewPortItems"
          LibsUiDragItemDirective
          [item]="item"
          [itemInContainerVirtualScroll]="true"
          [fieldId]="'id'">
          {{ item.name }}
        </div>
      </div>
    </virtual-scroller>
  `,
})
export class VirtualScrollDragDropComponent {
  items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
}
```

### Custom Boundary - Giới hạn vùng kéo thả

```typescript
@Component({
  standalone: true,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div class="relative w-[500px] h-[500px] border-2 border-gray-800">
      <div
        LibsUiComponentsDragContainerDirective
        [(items)]="items">
        <div
          *ngFor="let item of items"
          LibsUiDragItemDirective
          [item]="item"
          [dragBoundary]="true">
          {{ item.name }}
        </div>
      </div>
    </div>
  `,
})
export class CustomBoundaryComponent {
  items = [
    { id: 1, name: 'Bounded Item 1' },
    { id: 2, name: 'Bounded Item 2' },
  ];
}
```

## API Reference

### LibsUiComponentsDragContainerDirective

Selector: `[LibsUiComponentsDragContainerDirective]`

#### Inputs

| Property | Type | Default | Mô tả |
|---|---|---|---|
| `[acceptDragSameGroup]` | `boolean` | `false` | Cho phép kéo thả trong cùng group. |
| `[directionDrag]` | `'horizontal' \| 'vertical'` | `undefined` | Hướng kéo thả (ngang hoặc dọc). |
| `[disableDragContainer]` | `boolean` | `undefined` | Vô hiệu hóa toàn bộ kéo thả trong container. |
| `[dropToGroupName]` | `Array<string> \| null` | `null` | Danh sách group name được phép drop vào container này. |
| `[groupName]` | `string` | `'groupDragAndDropDefault'` | Tên group để định danh container. |
| `[(items)]` | `Array<unknown>` | *bắt buộc* | Danh sách items (two-way binding). |
| `[mode]` | `'move' \| 'copy'` | `'move'` | Chế độ kéo thả: di chuyển hoặc sao chép. |
| `[placeholder]` | `boolean` | `true` | Hiển thị placeholder tại vị trí drop. |
| `[stylesOverride]` | `Array<{className, styles}>` | `undefined` | Ghi đè styles cho container. |
| `[viewEncapsulation]` | `'emulated' \| 'none'` | `'emulated'` | Chế độ View Encapsulation cho styles. |

#### Outputs

| Property | Type | Mô tả |
|---|---|---|
| `(outDragEndContainer)` | `IDragEnd` | Emit khi kết thúc kéo phần tử trong container. |
| `(outDragLeaveContainer)` | `IDragLeave` | Emit khi phần tử kéo rời khỏi container. |
| `(outDragOverContainer)` | `IDragOver` | Emit khi phần tử kéo di chuyển qua container. |
| `(outDragStartContainer)` | `IDragStart` | Emit khi bắt đầu kéo phần tử trong container. |
| `(outDroppedContainer)` | `IDrop` | Emit khi thả phần tử vào container. |
| `(outDroppedContainerEmpty)` | `IDrop` | Emit khi thả phần tử vào container rỗng. |
| `(outFunctionControl)` | `IDragDropFunctionControlEvent` | Emit object chứa các hàm điều khiển container. |

### LibsUiDragItemDirective

Selector: `[LibsUiDragItemDirective]`

#### Inputs

| Property | Type | Default | Mô tả |
|---|---|---|---|
| `[disable]` | `boolean` | `undefined` | Vô hiệu hóa kéo thả cho item này. |
| `[dragBoundary]` | `boolean` | `undefined` | Giới hạn kéo trong vùng boundary của container. |
| `[dragBoundaryAcceptMouseLeaveContainer]` | `boolean` | `undefined` | Cho phép chuột rời container khi đang giới hạn boundary. |
| `[dragRootElement]` | `boolean` | `undefined` | Sử dụng root element làm gốc kéo. |
| `[elementContainer]` | `HTMLElement` | `undefined` | Element container tùy chỉnh cho item. |
| `[fieldId]` | `string` | `''` | Tên field dùng làm ID định danh item. |
| `[ignoreStopEvent]` | `boolean` | `undefined` | Bỏ qua stop event mặc định. |
| `[ignoreUserSelectNone]` | `boolean` | `undefined` | Bỏ qua việc set `user-select: none` khi kéo. |
| `[item]` | `any` | `undefined` | Dữ liệu của item. |
| `[itemInContainerVirtualScroll]` | `boolean` | `undefined` | Đánh dấu item nằm trong virtual scroll container. |
| `[onlyMouseDownStopEvent]` | `boolean` | `undefined` | Chỉ stop event khi mousedown. |
| `[throttleTimeHandlerDraggingEvent]` | `number` | `0` | Thời gian throttle (ms) cho dragging event. |
| `[zIndex]` | `number` | `1300` | z-index của phần tử khi đang kéo. |

#### Outputs

| Property | Type | Mô tả |
|---|---|---|
| `(outDragEnd)` | `IDragEnd` | Emit khi kết thúc kéo item. |
| `(outDragLeave)` | `IDragLeave` | Emit khi item rời khỏi vùng drop. |
| `(outDragOver)` | `IDragOver` | Emit khi item di chuyển qua vùng drop. |
| `(outDragStart)` | `IDragStart` | Emit khi bắt đầu kéo item. |
| `(outDropped)` | `IDrop` | Emit khi item được thả. |

### LibsUiComponentsDragScrollDirective

Selector: `[LibsUiComponentsDragScrollDirective]`

Directive tự động scroll container khi phần tử kéo di chuyển gần mép.

#### Inputs

| Property | Type | Default | Mô tả |
|---|---|---|---|
| `[ignoreAutoScroll]` | `boolean` | `undefined` | Tắt tính năng auto scroll. |
| `[widthZoneDetect]` | `number` | `16` | Chiều rộng vùng phát hiện (px) gần mép để kích hoạt scroll. |
| `[movementLength]` | `number` | `6` | Khoảng cách scroll mỗi lần (px). |
| `[rootElementScroll]` | `HTMLElement` | `undefined` | Element gốc để scroll (mặc định là host element). |
| `[virtualScrollerComponent]` | `VirtualScrollerComponent` | `undefined` | Tham chiếu đến VirtualScrollerComponent. |

### LibsUiDragItemInContainerVirtualScrollDirective

Selector: `[LibsUiDragItemInContainerVirtualScrollDirective]`

Directive hỗ trợ kéo thả item trong container sử dụng virtual scroll (`@iharbeck/ngx-virtual-scroller`).

## Types & Interfaces

```typescript
export interface IDragging {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  elementKeepContainer?: boolean;
  itemDragInfo?: IItemDragInfo;
}

export interface IDragStart {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

export interface IDragOver {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  elementDragOver: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

export interface IDragLeave {
  elementDrag: HTMLElement;
  elementDragLeave: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

export interface IDragEnd {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

export interface IDrop {
  elementDrag: HTMLElement;
  elementDrop: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

export interface IItemDragInfo {
  item: object;
  itemsMove?: WritableSignal<Array<unknown>>;
  indexDrag?: number;
  indexDrop?: number;
  itemsDrag: WritableSignal<Array<unknown>>;
  itemsDrop?: WritableSignal<Array<unknown>>;
  containerDrag?: HTMLElement;
  containerDrop?: HTMLElement;
}

export interface IDragItemInContainerVirtualScroll {
  itemDragInfo?: IItemDragInfo;
  elementDrag: HTMLElement;
  distanceStartElementAndMouseTop: number;
  distanceStartElementAndMouseLeft: number;
  elementContainer?: HTMLElement;
  dragBoundary?: boolean;
  dragBoundaryAcceptMouseLeaveContainer?: boolean;
  ignoreStopEvent?: boolean;
  ignoreUserSelectNone: boolean;
}

export interface IMousePosition {
  clientX: number;
  clientY: number;
}

export interface IDragDropFunctionControlEvent {
  setAttributeElementAndItemDrag: () => Promise<void>;
}
```

## Tùy chỉnh giao diện (Styling)

### CSS Classes mặc định

| Class | Mô tả |
|---|---|
| `.libs-ui-drag-drop-container` | Class cho container kéo thả. |
| `.libs-ui-drag-drop-item` | Class cho mỗi item kéo thả. |
| `.libs-ui-drag-drop-item-dragging` | Class được thêm vào item đang được kéo. |
| `.libs-ui-drag-drop-item-placeholder` | Class cho placeholder tại vị trí drop. |

### Sử dụng stylesOverride

Ghi đè styles thông qua input `[stylesOverride]` trên Container directive:

```typescript
@Component({
  standalone: true,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div
      LibsUiComponentsDragContainerDirective
      [(items)]="items"
      [stylesOverride]="customStyles">
      <div
        *ngFor="let item of items"
        LibsUiDragItemDirective
        [item]="item">
        {{ item.name }}
      </div>
    </div>
  `,
})
export class CustomStyledComponent {
  items = [{ id: 1, name: 'Item 1' }];

  customStyles = [
    {
      className: 'custom-drag-container',
      styles: `
        .custom-drag-container {
          background: #f5f5f5;
          border-radius: 8px;
          padding: 16px;
        }
      `,
    },
    {
      className: 'custom-drag-item-dragging',
      styles: `
        .custom-drag-item-dragging {
          opacity: 0.6;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }
      `,
    },
  ];
}
```

## Demo

- Local: http://localhost:4500/drag-drop