npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@libs-ui/components-drag-drop

v0.2.357-0

Published

> Bộ Angular directives hỗ trợ kéo thả (drag & drop) linh hoạt: sắp xếp trong container, kéo thả qua nhiều container, auto-scroll, custom boundary và tích hợp virtual scroll.

Downloads

3,435

Readme

@libs-ui/components-drag-drop

Bộ Angular directives hỗ trợ kéo thả (drag & drop) linh hoạt: sắp xếp trong container, kéo thả qua nhiều container, auto-scroll, custom boundary và tích hợp virtual scroll.

Giới thiệu

@libs-ui/components-drag-drop cung cấp bộ directives dựa trên mouse events (không dùng HTML5 Drag API) để xử lý kéo thả chính xác hơn trên mọi trình duyệt. Lib hỗ trợ hai chế độ: move (di chuyển item) và copy (sao chép item), đồng thời tự động cập nhật danh sách qua two-way binding [(items)]. Thiết kế theo mô hình Container + Item: directive container quản lý danh sách, directive item gắn lên từng phần tử có thể kéo.

Tính năng

  • Kéo thả để sắp xếp lại thứ tự item trong cùng một container
  • Kéo thả item qua nhiều container (cross-container) với kiểm soát group
  • Chế độ copy: sao chép item sang container đích thay vì di chuyển
  • Placeholder trực quan tại vị trí drop trong khi kéo
  • Auto-scroll khi kéo gần mép container (LibsUiComponentsDragScrollDirective)
  • Hỗ trợ virtual scroll (@iharbeck/ngx-virtual-scroller) cho danh sách lớn
  • Giới hạn vùng kéo trong boundary tùy chỉnh (dragBoundary)
  • Animation hướng kéo: horizontal hoặc vertical
  • Vô hiệu hóa kéo thả theo container hoặc từng item riêng lẻ
  • Two-way binding [(items)] — danh sách tự động cập nhật sau mỗi lần drop
  • Hỗ trợ ghi đè CSS styles qua [stylesOverride]

Khi nào sử dụng

  • Sắp xếp lại danh sách items bằng thao tác kéo thả
  • Triển khai Kanban board với nhiều cột kéo thả qua lại
  • Di chuyển hoặc sao chép items giữa hai danh sách (source/target)
  • Danh sách lớn cần kết hợp virtual scroll để tối ưu hiệu năng
  • Giới hạn vùng kéo thả trong một khu vực cụ thể (dashboard, canvas)
  • Bất kỳ UI nào cần thao tác kéo thả mượt mà không phụ thuộc HTML5 Drag API

Cài đặt

npm install @libs-ui/components-drag-drop

Import

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

// Interfaces (dùng trong handler)
import {
  IDragStart,
  IDragOver,
  IDragLeave,
  IDragEnd,
  IDrop,
  IItemDragInfo,
  IMousePosition,
  IDragDropFunctionControlEvent,
} from '@libs-ui/components-drag-drop';

Ví dụ sử dụng

Ví dụ 1 — Sắp xếp danh sách đơn giản

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

@Component({
  selector: 'app-basic-drag',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div
      LibsUiComponentsDragContainerDirective
      [(items)]="items"
      [acceptDragSameGroup]="true"
      (outDroppedContainer)="handlerDrop($event)"
      class="min-h-[120px] p-2 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200">
      @for (item of items(); track item.id) {
        <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item">
          <div class="p-3 bg-white border border-gray-200 rounded-lg">
            {{ item.name }}
          </div>
        </div>
      }
    </div>
  `,
})
export class BasicDragComponent {
  items = signal([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' },
    { id: 4, name: 'Item 4' },
  ]);

  handlerDrop(event: IDrop): void {
    event.stopPropagation?.();
    // items đã tự động cập nhật qua [(items)] two-way binding
    console.log('Dropped:', event.itemDragInfo?.indexDrag, '->', event.itemDragInfo?.indexDrop);
  }
}

Ví dụ 2 — Kéo thả giữa hai container (Cross-Container)

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

@Component({
  selector: 'app-cross-container',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div class="flex gap-4">
      <!-- Container nguồn: source có thể drop sang target -->
      <div class="flex-1">
        <h4 class="text-sm font-medium mb-2">Source</h4>
        <div
          LibsUiComponentsDragContainerDirective
          [(items)]="sourceItems"
          [groupName]="'source'"
          [dropToGroupName]="['target']"
          [acceptDragSameGroup]="true"
          (outDroppedContainer)="handlerDrop($event)"
          class="min-h-[120px] p-2 bg-blue-50 rounded-lg border-2 border-dashed border-blue-200">
          @for (item of sourceItems(); track item) {
            <div class="pb-2 cursor-move" LibsUiDragItemDirective>
              <div class="p-3 bg-white border border-blue-200 rounded-lg">{{ item }}</div>
            </div>
          }
        </div>
      </div>

      <!-- Container đích: target có thể drop sang source -->
      <div class="flex-1">
        <h4 class="text-sm font-medium mb-2">Target</h4>
        <div
          LibsUiComponentsDragContainerDirective
          [(items)]="targetItems"
          [groupName]="'target'"
          [dropToGroupName]="['source']"
          [acceptDragSameGroup]="true"
          (outDroppedContainer)="handlerDrop($event)"
          class="min-h-[120px] p-2 bg-green-50 rounded-lg border-2 border-dashed border-green-200">
          @for (item of targetItems(); track item) {
            <div class="pb-2 cursor-move" LibsUiDragItemDirective>
              <div class="p-3 bg-white border border-green-200 rounded-lg">{{ item }}</div>
            </div>
          }
        </div>
      </div>
    </div>
  `,
})
export class CrossContainerComponent {
  sourceItems = signal(['Source A', 'Source B', 'Source C']);
  targetItems = signal(['Target X', 'Target Y']);

  handlerDrop(event: IDrop): void {
    event.stopPropagation?.();
    console.log('Dropped item info:', event.itemDragInfo);
  }
}

Ví dụ 3 — Kanban Board

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

interface KanbanTask {
  id: number;
  title: string;
  desc: string;
}

interface KanbanColumn {
  id: string;
  title: string;
  items: KanbanTask[];
}

@Component({
  selector: 'app-kanban-board',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div class="flex gap-4">
      @for (column of columns(); track column.id) {
        <div class="flex-1 bg-gray-50 rounded-lg p-3">
          <h4 class="text-sm font-semibold text-gray-700 mb-3 pb-2 border-b border-gray-200">
            {{ column.title }}
          </h4>
          <div
            LibsUiComponentsDragContainerDirective
            [(items)]="column.items"
            [groupName]="column.id"
            [dropToGroupName]="allColumnIds"
            [acceptDragSameGroup]="true"
            (outDragStartContainer)="handlerDragStart($event)"
            (outDroppedContainer)="handlerDrop($event)"
            class="min-h-[80px]">
            @for (task of column.items; track task.id) {
              <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="task" [fieldId]="'id'">
                <div class="p-3 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow">
                  <div class="font-medium text-gray-800 text-sm">{{ task.title }}</div>
                  <div class="text-xs text-gray-500 mt-1">{{ task.desc }}</div>
                </div>
              </div>
            }
          </div>
        </div>
      }
    </div>
  `,
})
export class KanbanBoardComponent {
  columns = signal<KanbanColumn[]>([
    {
      id: 'todo',
      title: 'To Do',
      items: [
        { id: 1, title: 'Design UI', desc: 'Thiết kế giao diện người dùng' },
        { id: 2, title: 'Setup API', desc: 'Cấu hình REST API' },
      ],
    },
    {
      id: 'progress',
      title: 'In Progress',
      items: [{ id: 3, title: 'Implement Auth', desc: 'Xây dựng xác thực JWT' }],
    },
    {
      id: 'done',
      title: 'Done',
      items: [{ id: 4, title: 'Project Setup', desc: 'Khởi tạo project Angular' }],
    },
  ]);

  allColumnIds = ['todo', 'progress', 'done'];

  handlerDragStart(event: IDragStart): void {
    event.stopPropagation?.();
    console.log('Drag started:', event.mousePosition);
  }

  handlerDrop(event: IDrop): void {
    event.stopPropagation?.();
    console.log('Task moved:', event.itemDragInfo?.item);
  }
}

Ví dụ 4 — Chế độ Copy (sao chép item)

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

@Component({
  selector: 'app-copy-mode',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div class="flex gap-4">
      <!-- Nguồn: mode copy — item không bị xóa khỏi nguồn -->
      <div class="flex-1">
        <h4 class="text-sm font-medium mb-2">Nguồn (copy mode)</h4>
        <div
          LibsUiComponentsDragContainerDirective
          [(items)]="palette"
          [groupName]="'palette'"
          [dropToGroupName]="['canvas']"
          [mode]="'copy'">
          @for (item of palette(); track item.id) {
            <div class="pb-2 cursor-copy" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
              <div class="p-3 bg-blue-100 border border-blue-300 rounded-lg">{{ item.label }}</div>
            </div>
          }
        </div>
      </div>

      <!-- Canvas: item được sao chép vào đây -->
      <div class="flex-1">
        <h4 class="text-sm font-medium mb-2">Canvas</h4>
        <div
          LibsUiComponentsDragContainerDirective
          [(items)]="canvas"
          [groupName]="'canvas'"
          [acceptDragSameGroup]="true"
          class="min-h-[200px] p-2 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
          @for (item of canvas(); track item.id) {
            <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
              <div class="p-3 bg-white border border-gray-200 rounded-lg">{{ item.label }}</div>
            </div>
          }
        </div>
      </div>
    </div>
  `,
})
export class CopyModeComponent {
  palette = signal([
    { id: 'btn-primary', label: 'Button Primary' },
    { id: 'btn-secondary', label: 'Button Secondary' },
    { id: 'input-text', label: 'Text Input' },
  ]);

  canvas = signal<Array<{ id: string; label: string }>>([]);
}

Ví dụ 5 — Auto-scroll với LibsUiComponentsDragScrollDirective

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

@Component({
  selector: 'app-auto-scroll',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    LibsUiComponentsDragContainerDirective,
    LibsUiDragItemDirective,
    LibsUiComponentsDragScrollDirective,
  ],
  template: `
    <!-- rootElementScroll trỏ đến element có overflow scroll -->
    <div #scrollContainer class="h-[300px] overflow-y-auto border border-gray-200 rounded-lg">
      <div
        LibsUiComponentsDragContainerDirective
        LibsUiComponentsDragScrollDirective
        [(items)]="longList"
        [acceptDragSameGroup]="true"
        [rootElementScroll]="scrollContainer"
        [widthZoneDetect]="24"
        [movementLength]="8"
        class="p-2">
        @for (item of longList(); track item.id) {
          <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
            <div class="p-3 bg-white border border-gray-200 rounded-lg">{{ item.name }}</div>
          </div>
        }
      </div>
    </div>
  `,
})
export class AutoScrollComponent {
  longList = signal(
    Array.from({ length: 50 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))
  );
}

@Input() — LibsUiComponentsDragContainerDirective

Selector: [LibsUiComponentsDragContainerDirective]

| Input | Type | Default | Mô tả | Ví dụ | |---|---|---|---|---| | [(items)] | Array<unknown> | bắt buộc | Danh sách items (two-way binding). Tự động cập nhật sau mỗi lần drop. | [(items)]="myList" | | [acceptDragSameGroup] | boolean | false | Cho phép kéo thả và sắp xếp lại trong cùng container/group. | [acceptDragSameGroup]="true" | | [directionDrag] | 'horizontal' \| 'vertical' | undefined | Hướng kéo thả để thêm animation translate cho item bị đè qua. | [directionDrag]="'vertical'" | | [disableDragContainer] | boolean | undefined | Vô hiệu hóa toàn bộ chức năng kéo thả trong container này. | [disableDragContainer]="isLocked" | | [dropToGroupName] | Array<string> \| null | null | Danh sách groupName của các container khác mà item trong container này được phép drop vào. | [dropToGroupName]="['target', 'archive']" | | [groupName] | string | 'groupDragAndDropDefault' | Tên định danh của container/group. Các container cùng tên có thể drop qua lại. | [groupName]="'todo'" | | [mode] | 'move' \| 'copy' | 'move' | Chế độ kéo thả: move di chuyển item, copy sao chép item sang container đích. | [mode]="'copy'" | | [placeholder] | boolean | true | Hiển thị placeholder (vị trí mờ) tại chỗ item sẽ được thả vào trong khi đang kéo. | [placeholder]="false" | | [stylesOverride] | Array<{ className: string; styles: string }> | undefined | Ghi đè CSS styles mặc định của container và items. | [stylesOverride]="customStyles" | | [viewEncapsulation] | 'emulated' \| 'none' | 'emulated' | Chế độ View Encapsulation cho styles inject. Dùng 'none' nếu cần styles xuyên shadow DOM. | [viewEncapsulation]="'none'" |

@Output() — LibsUiComponentsDragContainerDirective

| Output | Type | Mô tả | Handler TS | Binding HTML | |---|---|---|---|---| | (outDragStartContainer) | IDragStart | Emit khi bắt đầu kéo một item thuộc container này. | handlerDragStart(e: IDragStart): void { e.stopPropagation?.(); } | (outDragStartContainer)="handlerDragStart($event)" | | (outDragOverContainer) | IDragOver | Emit khi item đang kéo di chuyển vào trong vùng container (enter). | handlerDragOver(e: IDragOver): void { e.stopPropagation?.(); } | (outDragOverContainer)="handlerDragOver($event)" | | (outDragLeaveContainer) | IDragLeave | Emit khi item đang kéo rời khỏi vùng container (leave). | handlerDragLeave(e: IDragLeave): void { e.stopPropagation?.(); } | (outDragLeaveContainer)="handlerDragLeave($event)" | | (outDragEndContainer) | IDragEnd | Emit khi kết thúc thao tác kéo (mouseup) trong container này. | handlerDragEnd(e: IDragEnd): void { e.stopPropagation?.(); } | (outDragEndContainer)="handlerDragEnd($event)" | | (outDroppedContainer) | IDrop | Emit khi item được thả vào container này (bao gồm từ container khác). | handlerDrop(e: IDrop): void { e.stopPropagation?.(); } | (outDroppedContainer)="handlerDrop($event)" | | (outDroppedContainerEmpty) | IDrop | Emit khi item được thả vào container khi container đang rỗng (không có item). | handlerDropEmpty(e: IDrop): void { e.stopPropagation?.(); } | (outDroppedContainerEmpty)="handlerDropEmpty($event)" | | (outFunctionControl) | IDragDropFunctionControlEvent | Emit object chứa hàm điều khiển container từ bên ngoài khi ngAfterViewInit. | handlerFnControl(e: IDragDropFunctionControlEvent): void { this.containerFn = e; } | (outFunctionControl)="handlerFnControl($event)" |

@Input() — LibsUiDragItemDirective

Selector: [LibsUiDragItemDirective]

| Input | Type | Default | Mô tả | Ví dụ | |---|---|---|---|---| | [disable] | boolean | undefined | Vô hiệu hóa kéo thả cho riêng item này. | [disable]="item.locked" | | [dragBoundary] | boolean | undefined | Giới hạn vùng kéo trong boundary của container (không cho kéo ra ngoài). | [dragBoundary]="true" | | [dragBoundaryAcceptMouseLeaveContainer] | boolean | undefined | Khi bật dragBoundary, cho phép con trỏ chuột rời khỏi container mà item vẫn cập nhật vị trí theo boundary. | [dragBoundaryAcceptMouseLeaveContainer]="true" | | [dragRootElement] | boolean | undefined | Kéo trực tiếp element gốc (không tạo clone). Element gốc sẽ ẩn đi trong khi kéo và hiện lại khi thả. | [dragRootElement]="true" | | [elementContainer] | HTMLElement | undefined | Chỉ định element container tùy chỉnh để tính toán vị trí khi dragBoundary bật. | [elementContainer]="containerRef" | | [fieldId] | string | '' | Tên field dùng làm ID định danh item. Bắt buộc khi dùng với virtual scroll. | [fieldId]="'id'" | | [ignoreStopEvent] | boolean | undefined | Bỏ qua preventDefault()stopPropagation() trên mouse events khi kéo. | [ignoreStopEvent]="true" | | [ignoreUserSelectNone] | boolean | undefined | Không thêm class select-none vào item khi đang kéo. | [ignoreUserSelectNone]="true" | | [item] | any | undefined | Dữ liệu của item. Bắt buộc khi dùng với virtual scroll hoặc khi cần IItemDragInfo trong event. | [item]="task" | | [itemInContainerVirtualScroll] | boolean | undefined | Đánh dấu item nằm trong virtual scroll container. Bắt buộc bật khi dùng @iharbeck/ngx-virtual-scroller. | [itemInContainerVirtualScroll]="true" | | [onlyMouseDownStopEvent] | boolean | undefined | Chỉ gọi stopPropagation() trên sự kiện mousedown, không chặn mousemove/mouseup. | [onlyMouseDownStopEvent]="true" | | [throttleTimeHandlerDraggingEvent] | number | 0 | Thời gian throttle (ms) cho sự kiện dragging. Tăng giá trị để giảm tải khi list lớn. | [throttleTimeHandlerDraggingEvent]="16" | | [zIndex] | number | 1300 | Giá trị z-index của phần tử clone khi đang kéo. | [zIndex]="2000" |

@Output() — LibsUiDragItemDirective

| Output | Type | Mô tả | Handler TS | Binding HTML | |---|---|---|---|---| | (outDragStart) | IDragStart | Emit khi bắt đầu kéo item này. | handlerItemDragStart(e: IDragStart): void { e.stopPropagation?.(); } | (outDragStart)="handlerItemDragStart($event)" | | (outDragOver) | IDragOver | Emit khi một item khác đang được kéo qua item này. | handlerItemDragOver(e: IDragOver): void { e.stopPropagation?.(); } | (outDragOver)="handlerItemDragOver($event)" | | (outDragLeave) | IDragLeave | Emit khi item đang kéo rời khỏi vùng của item này. | handlerItemDragLeave(e: IDragLeave): void { e.stopPropagation?.(); } | (outDragLeave)="handlerItemDragLeave($event)" | | (outDragEnd) | IDragEnd | Emit khi kết thúc kéo item này (mouseup). | handlerItemDragEnd(e: IDragEnd): void { e.stopPropagation?.(); } | (outDragEnd)="handlerItemDragEnd($event)" | | (outDropped) | IDrop | Emit khi item khác được thả lên item này. | handlerItemDropped(e: IDrop): void { e.stopPropagation?.(); } | (outDropped)="handlerItemDropped($event)" |

@Input() — LibsUiComponentsDragScrollDirective

Selector: [LibsUiComponentsDragScrollDirective]

Directive bổ sung, dùng chung với LibsUiComponentsDragContainerDirective để bật tính năng tự động cuộn container khi kéo item đến gần mép.

| Input | Type | Default | Mô tả | Ví dụ | |---|---|---|---|---| | [ignoreAutoScroll] | boolean | undefined | Tắt hoàn toàn tính năng auto-scroll. | [ignoreAutoScroll]="true" | | [movementLength] | number | 6 | Số pixel scroll mỗi tick khi item đang gần mép container. | [movementLength]="10" | | [rootElementScroll] | HTMLElement | undefined | Trỏ đến element có overflow: scroll/auto để thực hiện scroll. Nếu không có, scroll trên host element. | [rootElementScroll]="scrollEl" | | [virtualScrollerComponent] | VirtualScrollerComponent | undefined | Tham chiếu đến VirtualScrollerComponent khi dùng @iharbeck/ngx-virtual-scroller. | [virtualScrollerComponent]="vsRef" | | [widthZoneDetect] | number | 16 | Chiều rộng (px) của vùng phát hiện gần mép để kích hoạt auto-scroll. | [widthZoneDetect]="24" |

Types & Interfaces

import {
  IDragging,
  IDragStart,
  IDragOver,
  IDragLeave,
  IDragEnd,
  IDrop,
  IItemDragInfo,
  IMousePosition,
  IDragDropFunctionControlEvent,
  IDragItemInContainerVirtualScroll,
} from '@libs-ui/components-drag-drop';

/** Event khi đang kéo (mousemove) */
interface IDragging {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;        // Clone element đang được kéo
  elementKeepContainer?: boolean;  // true nếu chuột nằm ngoài mọi container
  itemDragInfo?: IItemDragInfo;
}

/** Event khi bắt đầu kéo */
interface IDragStart {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

/** Event khi item đang kéo di chuyển qua element khác */
interface IDragOver {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  elementDragOver: HTMLElement;    // Element đang bị hover qua
  itemDragInfo?: IItemDragInfo;
}

/** Event khi item đang kéo rời khỏi element */
interface IDragLeave {
  elementDrag: HTMLElement;
  elementDragLeave: HTMLElement;   // Element vừa bị rời khỏi
  itemDragInfo?: IItemDragInfo;
}

/** Event khi kết thúc kéo (mouseup) */
interface IDragEnd {
  mousePosition: IMousePosition;
  elementDrag: HTMLElement;
  itemDragInfo?: IItemDragInfo;
}

/** Event khi drop item vào container/item khác */
interface IDrop {
  elementDrag: HTMLElement;
  elementDrop: HTMLElement;        // Container hoặc item đích nhận drop
  itemDragInfo?: IItemDragInfo;
}

/** Thông tin chi tiết về item đang/vừa được kéo */
interface IItemDragInfo {
  item: object;                                    // Dữ liệu của item
  itemsMove?: WritableSignal<Array<unknown>>;      // Danh sách nguồn sau khi đã xóa item kéo (dùng cho move mode)
  indexDrag?: number;                              // Vị trí (index) ban đầu của item trong container nguồn
  indexDrop?: number;                              // Vị trí (index) cuối cùng của item sau khi drop
  itemsDrag: WritableSignal<Array<unknown>>;       // Signal danh sách của container nguồn
  itemsDrop?: WritableSignal<Array<unknown>>;      // Signal danh sách của container đích
  containerDrag?: HTMLElement;                     // HTMLElement của container nguồn
  containerDrop?: HTMLElement;                     // HTMLElement của container đích
}

/** Vị trí con trỏ chuột */
interface IMousePosition {
  clientX: number;
  clientY: number;
}

/** Object functions điều khiển container emit qua (outFunctionControl) */
interface IDragDropFunctionControlEvent {
  /** Đồng bộ lại attributes và index cho tất cả items trong container */
  setAttributeElementAndItemDrag: () => Promise<void>;
}

CSS Classes mặc định

| Class | Mô tả | |---|---| | .libs-ui-drag-drop-container | Tự động được thêm vào element có LibsUiComponentsDragContainerDirective | | .libs-ui-drag-drop-container-dragover | Được thêm khi có item đang kéo vào trong container | | .libs-ui-drag-drop-item | Tự động được thêm vào element có LibsUiDragItemDirective | | .libs-ui-drag-drop-item-dragging | Được thêm vào clone element đang được kéo (cursor: move) | | .libs-ui-drag-drop-item-placeholder | Được thêm vào item gốc khi nó đang bị kéo (dùng để ẩn bằng CSS) | | .libs-ui-drag-drop-item-origin-placeholder | Item gốc đang bị kéo (trong container nguồn) | | .libs-ui-drag-drop-item-drop-placeholder | Item tạm thời (ghost) hiển thị tại vị trí drop đích | | .libs-ui-drag-drop-item-translate-top | Animation: item dịch chuyển lên khi kéo theo chiều vertical | | .libs-ui-drag-drop-item-translate-bottom | Animation: item dịch chuyển xuống khi kéo theo chiều vertical | | .libs-ui-drag-drop-item-translate-left | Animation: item dịch chuyển sang trái khi kéo theo chiều horizontal | | .libs-ui-drag-drop-item-translate-right | Animation: item dịch chuyển sang phải khi kéo theo chiều horizontal |

Tùy chỉnh giao diện qua stylesOverride

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

@Component({
  selector: 'app-custom-styled',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
  template: `
    <div
      LibsUiComponentsDragContainerDirective
      [(items)]="items"
      [acceptDragSameGroup]="true"
      [stylesOverride]="customStyles">
      @for (item of items(); track item.id) {
        <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
          <div class="p-3 bg-white border rounded-lg">{{ item.name }}</div>
        </div>
      }
    </div>
  `,
})
export class CustomStyledDragComponent {
  items = signal([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
  ]);

  customStyles = [
    {
      className: 'libs-ui-drag-drop-item-origin-placeholder',
      styles: `
        .libs-ui-drag-drop-item-origin-placeholder {
          opacity: 0.3;
          background: #e0f2fe;
        }
      `,
    },
    {
      className: 'libs-ui-drag-drop-item-drop-placeholder',
      styles: `
        .libs-ui-drag-drop-item-drop-placeholder {
          border: 2px dashed #3b82f6;
          background: #eff6ff;
        }
      `,
    },
  ];
}

Lưu ý quan trọng

⚠️ Two-way binding bắt buộc: [(items)] là bắt buộc trên LibsUiComponentsDragContainerDirective. Danh sách items tự động cập nhật sau mỗi lần drop — không cần cập nhật thủ công trong handler.

⚠️ Dùng padding thay vì margin giữa các item: Khoảng cách giữa các drag item PHẢI dùng padding (ví dụ: class pb-2), TUYỆT ĐỐI KHÔNG dùng margin (ví dụ: mb-2). Lý do: khi dùng margin, vùng margin giữa hai 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 item xuống cuối thay vì vào đúng vị trí.

⚠️ Group Name cho cross-container: Khi kéo thả giữa các container, phải thiết lập [groupName] để định danh container và [dropToGroupName] để chỉ định danh sách container đích được phép nhận drop. Hai container muốn kéo qua lại cần cài [dropToGroupName] chỉ vào nhau.

⚠️ Virtual scroll: Khi dùng với @iharbeck/ngx-virtual-scroller, bắt buộc thêm [itemInContainerVirtualScroll]="true" trên LibsUiDragItemDirective, cung cấp [fieldId] (tên field ID), [item] (dữ liệu item), và thêm LibsUiDragItemInContainerVirtualScrollDirective lên component cha.

⚠️ Mode copy: Với [mode]="'copy'", item được sao chép (deep clone) sang container đích, không bị xóa khỏi container nguồn. Đảm bảo các item trong nguồn có field ID duy nhất khi dùng [fieldId] để tránh xung đột.

⚠️ ViewEncapsulation: Mặc định là 'emulated'. Đổi sang 'none' nếu component dùng ViewEncapsulation.None hoặc cần styles của placeholder/placeholder xuyên qua shadow DOM.

⚠️ outFunctionControl: Nếu cần gọi setAttributeElementAndItemDrag() từ bên ngoài (ví dụ: sau khi thêm item thủ công vào list), lắng nghe (outFunctionControl) để lưu reference object hàm điều khiển.

Demo

npx nx serve core-ui

Truy cập: http://localhost:4500/drag-drop