@libs-ui/components-drag-drop 0.2.356-42 → 0.2.356-43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,41 +1,41 @@
1
1
  # @libs-ui/components-drag-drop
2
2
 
3
- > 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.
3
+ > 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 tích hợp virtual scroll.
4
4
 
5
- ## Version
5
+ ## Giới thiệu
6
6
 
7
- `0.2.355-14`
7
+ `@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.
8
8
 
9
- ## Author
9
+ ## Tính năng
10
10
 
11
- Dev Team
11
+ - Kéo thả để sắp xếp lại thứ tự item trong cùng một container
12
+ - Kéo thả item qua nhiều container (cross-container) với kiểm soát group
13
+ - Chế độ `copy`: sao chép item sang container đích thay vì di chuyển
14
+ - Placeholder trực quan tại vị trí drop trong khi kéo
15
+ - Auto-scroll khi kéo gần mép container (`LibsUiComponentsDragScrollDirective`)
16
+ - Hỗ trợ virtual scroll (`@iharbeck/ngx-virtual-scroller`) cho danh sách lớn
17
+ - Giới hạn vùng kéo trong boundary tùy chỉnh (`dragBoundary`)
18
+ - Animation hướng kéo: `horizontal` hoặc `vertical`
19
+ - Vô hiệu hóa kéo thả theo container hoặc từng item riêng lẻ
20
+ - Two-way binding `[(items)]` — danh sách tự động cập nhật sau mỗi lần drop
21
+ - Hỗ trợ ghi đè CSS styles qua `[stylesOverride]`
12
22
 
13
23
  ## Khi nào sử dụng
14
24
 
15
- - 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ả.
16
- - Khi cần di chuyển phần tử giữa nhiều container (cross-container drag and drop).
17
- - Khi cần triển khai Kanban board với nhiều cột thể kéo thả qua lại.
18
- - Khi danh sách lớn cần kết hợp virtual scroll để tối ưu hiệu năng.
19
- - Khi cần giới hạn vùng kéo thả trong một boundary cụ thể.
20
- - Khi cần auto-scroll khi kéo phần tử gần mép container.
25
+ - Sắp xếp lại danh sách items bằng thao tác kéo thả
26
+ - Triển khai Kanban board với nhiều cột kéo thả qua lại
27
+ - Di chuyển hoặc sao chép items giữa hai danh sách (source/target)
28
+ - Danh sách lớn cần kết hợp virtual scroll để tối ưu hiệu năng
29
+ - Giới hạn vùng kéo thả trong một khu vực cụ thể (dashboard, canvas)
30
+ - Bất kỳ UI nào cần thao tác kéo thả mượt không phụ thuộc HTML5 Drag API
21
31
 
22
- ## Lưu ý quan trọng
23
-
24
- ⚠️ **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ả.
25
-
26
- ⚠️ **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.
27
-
28
- ⚠️ **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.
32
+ ## Cài đặt
29
33
 
30
- ⚠️ **ViewEncapsulation**: Mặc định là `'emulated'`. Chuyển sang `'none'` nếu cần style xuyên qua shadow DOM.
31
-
32
- ⚠️ **Mode copy**: Khi `[mode]="'copy'"`, item sẽ được sao chép thay vì di chuyển sang container đích.
33
-
34
- ⚠️ **Performance**: Sử dụng `[throttleTimeHandlerDraggingEvent]` để throttle event dragging, giảm tải khi danh sách lớn.
35
-
36
- ⚠️ **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.
34
+ ```bash
35
+ npm install @libs-ui/components-drag-drop
36
+ ```
37
37
 
38
- ## Cài đặt & Import
38
+ ## Import
39
39
 
40
40
  ```typescript
41
41
  import {
@@ -44,124 +44,188 @@ import {
44
44
  LibsUiComponentsDragScrollDirective,
45
45
  LibsUiDragItemInContainerVirtualScrollDirective,
46
46
  } from '@libs-ui/components-drag-drop';
47
+
48
+ // Interfaces (dùng trong handler)
49
+ import {
50
+ IDragStart,
51
+ IDragOver,
52
+ IDragLeave,
53
+ IDragEnd,
54
+ IDrop,
55
+ IItemDragInfo,
56
+ IMousePosition,
57
+ IDragDropFunctionControlEvent,
58
+ } from '@libs-ui/components-drag-drop';
47
59
  ```
48
60
 
49
61
  ## Ví dụ sử dụng
50
62
 
51
- ### Basic - Sắp xếp danh sách đơn giản
63
+ ### dụ 1 — Sắp xếp danh sách đơn giản
52
64
 
53
65
  ```typescript
54
- import { Component } from '@angular/core';
66
+ import { Component, signal } from '@angular/core';
55
67
  import {
56
68
  LibsUiComponentsDragContainerDirective,
57
69
  LibsUiDragItemDirective,
70
+ IDrop,
58
71
  } from '@libs-ui/components-drag-drop';
59
72
 
60
73
  @Component({
61
- selector: 'app-basic-drag-drop',
74
+ selector: 'app-basic-drag',
62
75
  standalone: true,
76
+ changeDetection: ChangeDetectionStrategy.OnPush,
63
77
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
64
78
  template: `
65
79
  <div
66
80
  LibsUiComponentsDragContainerDirective
67
- [(items)]="items">
68
- <div
69
- *ngFor="let item of items"
70
- LibsUiDragItemDirective
71
- [item]="item">
72
- {{ item.name }}
73
- </div>
81
+ [(items)]="items"
82
+ [acceptDragSameGroup]="true"
83
+ (outDroppedContainer)="handlerDrop($event)"
84
+ class="min-h-[120px] p-2 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200">
85
+ @for (item of items(); track item.id) {
86
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item">
87
+ <div class="p-3 bg-white border border-gray-200 rounded-lg">
88
+ {{ item.name }}
89
+ </div>
90
+ </div>
91
+ }
74
92
  </div>
75
93
  `,
76
94
  })
77
- export class BasicDragDropComponent {
78
- items = [
95
+ export class BasicDragComponent {
96
+ items = signal([
79
97
  { id: 1, name: 'Item 1' },
80
98
  { id: 2, name: 'Item 2' },
81
99
  { id: 3, name: 'Item 3' },
82
- ];
100
+ { id: 4, name: 'Item 4' },
101
+ ]);
102
+
103
+ handlerDrop(event: IDrop): void {
104
+ event.stopPropagation?.();
105
+ // items đã tự động cập nhật qua [(items)] two-way binding
106
+ console.log('Dropped:', event.itemDragInfo?.indexDrag, '->', event.itemDragInfo?.indexDrop);
107
+ }
83
108
  }
84
109
  ```
85
110
 
86
- ### Cross Container - Kéo thả giữa hai container
111
+ ### dụ 2 Kéo thả giữa hai container (Cross-Container)
87
112
 
88
113
  ```typescript
114
+ import { Component, signal } from '@angular/core';
115
+ import {
116
+ LibsUiComponentsDragContainerDirective,
117
+ LibsUiDragItemDirective,
118
+ IDrop,
119
+ } from '@libs-ui/components-drag-drop';
120
+
89
121
  @Component({
122
+ selector: 'app-cross-container',
90
123
  standalone: true,
124
+ changeDetection: ChangeDetectionStrategy.OnPush,
91
125
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
92
126
  template: `
93
127
  <div class="flex gap-4">
94
- <!-- Container nguồn -->
95
- <div
96
- LibsUiComponentsDragContainerDirective
97
- [(items)]="sourceItems"
98
- [groupName]="'source'"
99
- [dropToGroupName]="['target']"
100
- (outDroppedContainer)="onDrop($event)">
128
+ <!-- Container nguồn: source có thể drop sang target -->
129
+ <div class="flex-1">
130
+ <h4 class="text-sm font-medium mb-2">Source</h4>
101
131
  <div
102
- *ngFor="let item of sourceItems"
103
- LibsUiDragItemDirective
104
- [item]="item">
105
- {{ item.name }}
132
+ LibsUiComponentsDragContainerDirective
133
+ [(items)]="sourceItems"
134
+ [groupName]="'source'"
135
+ [dropToGroupName]="['target']"
136
+ [acceptDragSameGroup]="true"
137
+ (outDroppedContainer)="handlerDrop($event)"
138
+ class="min-h-[120px] p-2 bg-blue-50 rounded-lg border-2 border-dashed border-blue-200">
139
+ @for (item of sourceItems(); track item) {
140
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective>
141
+ <div class="p-3 bg-white border border-blue-200 rounded-lg">{{ item }}</div>
142
+ </div>
143
+ }
106
144
  </div>
107
145
  </div>
108
146
 
109
- <!-- Container đích -->
110
- <div
111
- LibsUiComponentsDragContainerDirective
112
- [(items)]="targetItems"
113
- [groupName]="'target'"
114
- [dropToGroupName]="['source']"
115
- (outDroppedContainer)="onDrop($event)">
147
+ <!-- Container đích: target có thể drop sang source -->
148
+ <div class="flex-1">
149
+ <h4 class="text-sm font-medium mb-2">Target</h4>
116
150
  <div
117
- *ngFor="let item of targetItems"
118
- LibsUiDragItemDirective
119
- [item]="item">
120
- {{ item.name }}
151
+ LibsUiComponentsDragContainerDirective
152
+ [(items)]="targetItems"
153
+ [groupName]="'target'"
154
+ [dropToGroupName]="['source']"
155
+ [acceptDragSameGroup]="true"
156
+ (outDroppedContainer)="handlerDrop($event)"
157
+ class="min-h-[120px] p-2 bg-green-50 rounded-lg border-2 border-dashed border-green-200">
158
+ @for (item of targetItems(); track item) {
159
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective>
160
+ <div class="p-3 bg-white border border-green-200 rounded-lg">{{ item }}</div>
161
+ </div>
162
+ }
121
163
  </div>
122
164
  </div>
123
165
  </div>
124
166
  `,
125
167
  })
126
168
  export class CrossContainerComponent {
127
- sourceItems = [
128
- { id: 1, name: 'Source 1' },
129
- { id: 2, name: 'Source 2' },
130
- ];
131
- targetItems = [
132
- { id: 3, name: 'Target 1' },
133
- { id: 4, name: 'Target 2' },
134
- ];
169
+ sourceItems = signal(['Source A', 'Source B', 'Source C']);
170
+ targetItems = signal(['Target X', 'Target Y']);
135
171
 
136
- onDrop(event: IDrop) {
137
- console.log('Dropped:', event);
172
+ handlerDrop(event: IDrop): void {
173
+ event.stopPropagation?.();
174
+ console.log('Dropped item info:', event.itemDragInfo);
138
175
  }
139
176
  }
140
177
  ```
141
178
 
142
- ### Kanban Board
179
+ ### Ví dụ 3 — Kanban Board
143
180
 
144
181
  ```typescript
182
+ import { Component, signal } from '@angular/core';
183
+ import {
184
+ LibsUiComponentsDragContainerDirective,
185
+ LibsUiDragItemDirective,
186
+ IDragStart,
187
+ IDrop,
188
+ } from '@libs-ui/components-drag-drop';
189
+
190
+ interface KanbanTask {
191
+ id: number;
192
+ title: string;
193
+ desc: string;
194
+ }
195
+
196
+ interface KanbanColumn {
197
+ id: string;
198
+ title: string;
199
+ items: KanbanTask[];
200
+ }
201
+
145
202
  @Component({
203
+ selector: 'app-kanban-board',
146
204
  standalone: true,
205
+ changeDetection: ChangeDetectionStrategy.OnPush,
147
206
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
148
207
  template: `
149
208
  <div class="flex gap-4">
150
- @for (column of columns; track column.id) {
151
- <div class="flex-1 bg-gray-100 p-4 rounded-lg">
152
- <h3>{{ column.title }}</h3>
209
+ @for (column of columns(); track column.id) {
210
+ <div class="flex-1 bg-gray-50 rounded-lg p-3">
211
+ <h4 class="text-sm font-semibold text-gray-700 mb-3 pb-2 border-b border-gray-200">
212
+ {{ column.title }}
213
+ </h4>
153
214
  <div
154
215
  LibsUiComponentsDragContainerDirective
155
216
  [(items)]="column.items"
156
217
  [groupName]="column.id"
157
- [dropToGroupName]="allGroupNames"
158
- [directionDrag]="'vertical'">
159
- @for (item of column.items; track item.id) {
160
- <div
161
- class="bg-white p-3 mb-2 rounded shadow-sm"
162
- LibsUiDragItemDirective
163
- [item]="item">
164
- {{ item.title }}
218
+ [dropToGroupName]="allColumnIds"
219
+ [acceptDragSameGroup]="true"
220
+ (outDragStartContainer)="handlerDragStart($event)"
221
+ (outDroppedContainer)="handlerDrop($event)"
222
+ class="min-h-[80px]">
223
+ @for (task of column.items; track task.id) {
224
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="task" [fieldId]="'id'">
225
+ <div class="p-3 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow">
226
+ <div class="font-medium text-gray-800 text-sm">{{ task.title }}</div>
227
+ <div class="text-xs text-gray-500 mt-1">{{ task.desc }}</div>
228
+ </div>
165
229
  </div>
166
230
  }
167
231
  </div>
@@ -171,293 +235,375 @@ export class CrossContainerComponent {
171
235
  `,
172
236
  })
173
237
  export class KanbanBoardComponent {
174
- columns = [
175
- { id: 'todo', title: 'To Do', items: [{ id: 1, title: 'Task 1' }] },
176
- { id: 'doing', title: 'Doing', items: [{ id: 2, title: 'Task 2' }] },
177
- { id: 'done', title: 'Done', items: [{ id: 3, title: 'Task 3' }] },
178
- ];
179
- allGroupNames = ['todo', 'doing', 'done'];
238
+ columns = signal<KanbanColumn[]>([
239
+ {
240
+ id: 'todo',
241
+ title: 'To Do',
242
+ items: [
243
+ { id: 1, title: 'Design UI', desc: 'Thiết kế giao diện người dùng' },
244
+ { id: 2, title: 'Setup API', desc: 'Cấu hình REST API' },
245
+ ],
246
+ },
247
+ {
248
+ id: 'progress',
249
+ title: 'In Progress',
250
+ items: [{ id: 3, title: 'Implement Auth', desc: 'Xây dựng xác thực JWT' }],
251
+ },
252
+ {
253
+ id: 'done',
254
+ title: 'Done',
255
+ items: [{ id: 4, title: 'Project Setup', desc: 'Khởi tạo project Angular' }],
256
+ },
257
+ ]);
258
+
259
+ allColumnIds = ['todo', 'progress', 'done'];
260
+
261
+ handlerDragStart(event: IDragStart): void {
262
+ event.stopPropagation?.();
263
+ console.log('Drag started:', event.mousePosition);
264
+ }
265
+
266
+ handlerDrop(event: IDrop): void {
267
+ event.stopPropagation?.();
268
+ console.log('Task moved:', event.itemDragInfo?.item);
269
+ }
180
270
  }
181
271
  ```
182
272
 
183
- ### Virtual Scroll
273
+ ### dụ 4 — Chế độ Copy (sao chép item)
184
274
 
185
275
  ```typescript
276
+ import { Component, signal } from '@angular/core';
277
+ import {
278
+ LibsUiComponentsDragContainerDirective,
279
+ LibsUiDragItemDirective,
280
+ } from '@libs-ui/components-drag-drop';
281
+
186
282
  @Component({
283
+ selector: 'app-copy-mode',
187
284
  standalone: true,
188
- imports: [
189
- LibsUiComponentsDragContainerDirective,
190
- LibsUiDragItemDirective,
191
- LibsUiComponentsDragScrollDirective,
192
- LibsUiDragItemInContainerVirtualScrollDirective,
193
- ],
285
+ changeDetection: ChangeDetectionStrategy.OnPush,
286
+ imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
194
287
  template: `
195
- <virtual-scroller #scroll [items]="items">
196
- <div
197
- LibsUiComponentsDragContainerDirective
198
- LibsUiComponentsDragScrollDirective
199
- [(items)]="items">
288
+ <div class="flex gap-4">
289
+ <!-- Nguồn: mode copy — item không bị xóa khỏi nguồn -->
290
+ <div class="flex-1">
291
+ <h4 class="text-sm font-medium mb-2">Nguồn (copy mode)</h4>
292
+ <div
293
+ LibsUiComponentsDragContainerDirective
294
+ [(items)]="palette"
295
+ [groupName]="'palette'"
296
+ [dropToGroupName]="['canvas']"
297
+ [mode]="'copy'">
298
+ @for (item of palette(); track item.id) {
299
+ <div class="pb-2 cursor-copy" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
300
+ <div class="p-3 bg-blue-100 border border-blue-300 rounded-lg">{{ item.label }}</div>
301
+ </div>
302
+ }
303
+ </div>
304
+ </div>
305
+
306
+ <!-- Canvas: item được sao chép vào đây -->
307
+ <div class="flex-1">
308
+ <h4 class="text-sm font-medium mb-2">Canvas</h4>
200
309
  <div
201
- *ngFor="let item of scroll.viewPortItems"
202
- LibsUiDragItemDirective
203
- [item]="item"
204
- [itemInContainerVirtualScroll]="true"
205
- [fieldId]="'id'">
206
- {{ item.name }}
310
+ LibsUiComponentsDragContainerDirective
311
+ [(items)]="canvas"
312
+ [groupName]="'canvas'"
313
+ [acceptDragSameGroup]="true"
314
+ class="min-h-[200px] p-2 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
315
+ @for (item of canvas(); track item.id) {
316
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
317
+ <div class="p-3 bg-white border border-gray-200 rounded-lg">{{ item.label }}</div>
318
+ </div>
319
+ }
207
320
  </div>
208
321
  </div>
209
- </virtual-scroller>
322
+ </div>
210
323
  `,
211
324
  })
212
- export class VirtualScrollDragDropComponent {
213
- items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
325
+ export class CopyModeComponent {
326
+ palette = signal([
327
+ { id: 'btn-primary', label: 'Button Primary' },
328
+ { id: 'btn-secondary', label: 'Button Secondary' },
329
+ { id: 'input-text', label: 'Text Input' },
330
+ ]);
331
+
332
+ canvas = signal<Array<{ id: string; label: string }>>([]);
214
333
  }
215
334
  ```
216
335
 
217
- ### Custom Boundary - Giới hạn vùng kéo thả
336
+ ### dụ 5 Auto-scroll với LibsUiComponentsDragScrollDirective
218
337
 
219
338
  ```typescript
339
+ import { Component, signal } from '@angular/core';
340
+ import {
341
+ LibsUiComponentsDragContainerDirective,
342
+ LibsUiDragItemDirective,
343
+ LibsUiComponentsDragScrollDirective,
344
+ } from '@libs-ui/components-drag-drop';
345
+
220
346
  @Component({
347
+ selector: 'app-auto-scroll',
221
348
  standalone: true,
222
- imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
349
+ changeDetection: ChangeDetectionStrategy.OnPush,
350
+ imports: [
351
+ LibsUiComponentsDragContainerDirective,
352
+ LibsUiDragItemDirective,
353
+ LibsUiComponentsDragScrollDirective,
354
+ ],
223
355
  template: `
224
- <div class="relative w-[500px] h-[500px] border-2 border-gray-800">
356
+ <!-- rootElementScroll trỏ đến element có overflow scroll -->
357
+ <div #scrollContainer class="h-[300px] overflow-y-auto border border-gray-200 rounded-lg">
225
358
  <div
226
359
  LibsUiComponentsDragContainerDirective
227
- [(items)]="items">
228
- <div
229
- *ngFor="let item of items"
230
- LibsUiDragItemDirective
231
- [item]="item"
232
- [dragBoundary]="true">
233
- {{ item.name }}
234
- </div>
360
+ LibsUiComponentsDragScrollDirective
361
+ [(items)]="longList"
362
+ [acceptDragSameGroup]="true"
363
+ [rootElementScroll]="scrollContainer"
364
+ [widthZoneDetect]="24"
365
+ [movementLength]="8"
366
+ class="p-2">
367
+ @for (item of longList(); track item.id) {
368
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
369
+ <div class="p-3 bg-white border border-gray-200 rounded-lg">{{ item.name }}</div>
370
+ </div>
371
+ }
235
372
  </div>
236
373
  </div>
237
374
  `,
238
375
  })
239
- export class CustomBoundaryComponent {
240
- items = [
241
- { id: 1, name: 'Bounded Item 1' },
242
- { id: 2, name: 'Bounded Item 2' },
243
- ];
376
+ export class AutoScrollComponent {
377
+ longList = signal(
378
+ Array.from({ length: 50 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))
379
+ );
244
380
  }
245
381
  ```
246
382
 
247
- ## API Reference
248
-
249
- ### LibsUiComponentsDragContainerDirective
383
+ ## @Input() — LibsUiComponentsDragContainerDirective
250
384
 
251
385
  Selector: `[LibsUiComponentsDragContainerDirective]`
252
386
 
253
- #### Inputs
254
-
255
- | Property | Type | Default | tả |
256
- |---|---|---|---|
257
- | `[acceptDragSameGroup]` | `boolean` | `false` | Cho phép kéo thả trong cùng group. |
258
- | `[directionDrag]` | `'horizontal' \| 'vertical'` | `undefined` | Hướng kéo thả (ngang hoặc dọc). |
259
- | `[disableDragContainer]` | `boolean` | `undefined` | hiệu hóa toàn bộ kéo thả trong container. |
260
- | `[dropToGroupName]` | `Array<string> \| null` | `null` | Danh sách group name được phép drop vào container này. |
261
- | `[groupName]` | `string` | `'groupDragAndDropDefault'` | Tên group để định danh container. |
262
- | `[(items)]` | `Array<unknown>` | *bắt buộc* | Danh sách items (two-way binding). |
263
- | `[mode]` | `'move' \| 'copy'` | `'move'` | Chế độ kéo thả: di chuyển hoặc sao chép. |
264
- | `[placeholder]` | `boolean` | `true` | Hiển thị placeholder tại vị trí drop. |
265
- | `[stylesOverride]` | `Array<{className, styles}>` | `undefined` | Ghi đè styles cho container. |
266
- | `[viewEncapsulation]` | `'emulated' \| 'none'` | `'emulated'` | Chế độ View Encapsulation cho styles. |
267
-
268
- #### Outputs
269
-
270
- | Property | Type | tả |
271
- |---|---|---|
272
- | `(outDragEndContainer)` | `IDragEnd` | Emit khi kết thúc kéo phần tử trong container. |
273
- | `(outDragLeaveContainer)` | `IDragLeave` | Emit khi phần tử kéo rời khỏi container. |
274
- | `(outDragOverContainer)` | `IDragOver` | Emit khi phần tử kéo di chuyển qua container. |
275
- | `(outDragStartContainer)` | `IDragStart` | Emit khi bắt đầu kéo phần tử trong container. |
276
- | `(outDroppedContainer)` | `IDrop` | Emit khi thả phần tử vào container. |
277
- | `(outDroppedContainerEmpty)` | `IDrop` | Emit khi thả phần tử vào container rỗng. |
278
- | `(outFunctionControl)` | `IDragDropFunctionControlEvent` | Emit object chứa các hàm điều khiển container. |
279
-
280
- ### LibsUiDragItemDirective
387
+ | Input | Type | Default | Mô tả | Ví dụ |
388
+ |---|---|---|---|---|
389
+ | `[(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"` |
390
+ | `[acceptDragSameGroup]` | `boolean` | `false` | Cho phép kéo thả và sắp xếp lại trong cùng container/group. | `[acceptDragSameGroup]="true"` |
391
+ | `[directionDrag]` | `'horizontal' \| 'vertical'` | `undefined` | Hướng kéo thả để thêm animation translate cho item bị đè qua. | `[directionDrag]="'vertical'"` |
392
+ | `[disableDragContainer]` | `boolean` | `undefined` | hiệu hóa toàn bộ chức năng kéo thả trong container này. | `[disableDragContainer]="isLocked"` |
393
+ | `[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']"` |
394
+ | `[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'"` |
395
+ | `[mode]` | `'move' \| 'copy'` | `'move'` | Chế độ kéo thả: `move` di chuyển item, `copy` sao chép item sang container đích. | `[mode]="'copy'"` |
396
+ | `[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"` |
397
+ | `[stylesOverride]` | `Array<{ className: string; styles: string }>` | `undefined` | Ghi đè CSS styles mặc định của container và items. | `[stylesOverride]="customStyles"` |
398
+ | `[viewEncapsulation]` | `'emulated' \| 'none'` | `'emulated'` | Chế độ View Encapsulation cho styles inject. Dùng `'none'` nếu cần styles xuyên shadow DOM. | `[viewEncapsulation]="'none'"` |
399
+
400
+ ## @Output() LibsUiComponentsDragContainerDirective
401
+
402
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
403
+ |---|---|---|---|---|
404
+ | `(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)"` |
405
+ | `(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)"` |
406
+ | `(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)"` |
407
+ | `(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)"` |
408
+ | `(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)"` |
409
+ | `(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)"` |
410
+ | `(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)"` |
411
+
412
+ ## @Input() LibsUiDragItemDirective
281
413
 
282
414
  Selector: `[LibsUiDragItemDirective]`
283
415
 
284
- #### Inputs
285
-
286
- | Property | Type | Default | tả |
287
- |---|---|---|---|
288
- | `[disable]` | `boolean` | `undefined` | hiệu hóa kéo thả cho item này. |
289
- | `[dragBoundary]` | `boolean` | `undefined` | Giới hạn kéo trong vùng boundary của container. |
290
- | `[dragBoundaryAcceptMouseLeaveContainer]` | `boolean` | `undefined` | Cho phép chuột rời container khi đang giới hạn boundary. |
291
- | `[dragRootElement]` | `boolean` | `undefined` | Sử dụng root element làm gốc kéo. |
292
- | `[elementContainer]` | `HTMLElement` | `undefined` | Element container tùy chỉnh cho item. |
293
- | `[fieldId]` | `string` | `''` | Tên field dùng làm ID định danh item. |
294
- | `[ignoreStopEvent]` | `boolean` | `undefined` | Bỏ qua stop event mặc định. |
295
- | `[ignoreUserSelectNone]` | `boolean` | `undefined` | Bỏ qua việc set `user-select: none` khi kéo. |
296
- | `[item]` | `any` | `undefined` | Dữ liệu của item. |
297
- | `[itemInContainerVirtualScroll]` | `boolean` | `undefined` | Đánh dấu item nằm trong virtual scroll container. |
298
- | `[onlyMouseDownStopEvent]` | `boolean` | `undefined` | Chỉ stop event khi mousedown. |
299
- | `[throttleTimeHandlerDraggingEvent]` | `number` | `0` | Thời gian throttle (ms) cho dragging event. |
300
- | `[zIndex]` | `number` | `1300` | z-index của phần tử khi đang kéo. |
301
-
302
- #### Outputs
303
-
304
- | Property | Type | tả |
305
- |---|---|---|
306
- | `(outDragEnd)` | `IDragEnd` | Emit khi kết thúc kéo item. |
307
- | `(outDragLeave)` | `IDragLeave` | Emit khi item rời khỏi vùng drop. |
308
- | `(outDragOver)` | `IDragOver` | Emit khi item di chuyển qua vùng drop. |
309
- | `(outDragStart)` | `IDragStart` | Emit khi bắt đầu kéo item. |
310
- | `(outDropped)` | `IDrop` | Emit khi item được thả. |
311
-
312
- ### LibsUiComponentsDragScrollDirective
416
+ | Input | Type | Default | Mô tả | Ví dụ |
417
+ |---|---|---|---|---|
418
+ | `[disable]` | `boolean` | `undefined` | hiệu hóa kéo thả cho riêng item này. | `[disable]="item.locked"` |
419
+ | `[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"` |
420
+ | `[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"` |
421
+ | `[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"` |
422
+ | `[elementContainer]` | `HTMLElement` | `undefined` | Chỉ định element container tùy chỉnh để tính toán vị trí khi `dragBoundary` bật. | `[elementContainer]="containerRef"` |
423
+ | `[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'"` |
424
+ | `[ignoreStopEvent]` | `boolean` | `undefined` | Bỏ qua `preventDefault()` `stopPropagation()` trên mouse events khi kéo. | `[ignoreStopEvent]="true"` |
425
+ | `[ignoreUserSelectNone]` | `boolean` | `undefined` | Không thêm class `select-none` vào item khi đang kéo. | `[ignoreUserSelectNone]="true"` |
426
+ | `[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"` |
427
+ | `[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"` |
428
+ | `[onlyMouseDownStopEvent]` | `boolean` | `undefined` | Chỉ gọi `stopPropagation()` trên sự kiện mousedown, không chặn mousemove/mouseup. | `[onlyMouseDownStopEvent]="true"` |
429
+ | `[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"` |
430
+ | `[zIndex]` | `number` | `1300` | Giá trị z-index của phần tử clone khi đang kéo. | `[zIndex]="2000"` |
431
+
432
+ ## @Output() LibsUiDragItemDirective
433
+
434
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
435
+ |---|---|---|---|---|
436
+ | `(outDragStart)` | `IDragStart` | Emit khi bắt đầu kéo item này. | `handlerItemDragStart(e: IDragStart): void { e.stopPropagation?.(); }` | `(outDragStart)="handlerItemDragStart($event)"` |
437
+ | `(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)"` |
438
+ | `(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)"` |
439
+ | `(outDragEnd)` | `IDragEnd` | Emit khi kết thúc kéo item này (mouseup). | `handlerItemDragEnd(e: IDragEnd): void { e.stopPropagation?.(); }` | `(outDragEnd)="handlerItemDragEnd($event)"` |
440
+ | `(outDropped)` | `IDrop` | Emit khi item khác được thả lên item này. | `handlerItemDropped(e: IDrop): void { e.stopPropagation?.(); }` | `(outDropped)="handlerItemDropped($event)"` |
441
+
442
+ ## @Input() LibsUiComponentsDragScrollDirective
313
443
 
314
444
  Selector: `[LibsUiComponentsDragScrollDirective]`
315
445
 
316
- Directive tự động scroll container khi phần tử kéo di chuyển gần mép.
317
-
318
- #### Inputs
446
+ 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.
319
447
 
320
- | Property | Type | Default | Mô tả |
321
- |---|---|---|---|
322
- | `[ignoreAutoScroll]` | `boolean` | `undefined` | Tắt tính năng auto scroll. |
323
- | `[widthZoneDetect]` | `number` | `16` | Chiều rộng vùng phát hiện (px) gần mép để kích hoạt scroll. |
324
- | `[movementLength]` | `number` | `6` | Khoảng cách scroll mỗi lần (px). |
325
- | `[rootElementScroll]` | `HTMLElement` | `undefined` | Element gốc để scroll (mặc định host element). |
326
- | `[virtualScrollerComponent]` | `VirtualScrollerComponent` | `undefined` | Tham chiếu đến VirtualScrollerComponent. |
327
-
328
- ### LibsUiDragItemInContainerVirtualScrollDirective
329
-
330
- Selector: `[LibsUiDragItemInContainerVirtualScrollDirective]`
331
-
332
- Directive hỗ trợ kéo thả item trong container sử dụng virtual scroll (`@iharbeck/ngx-virtual-scroller`).
448
+ | Input | Type | Default | Mô tả | Ví dụ |
449
+ |---|---|---|---|---|
450
+ | `[ignoreAutoScroll]` | `boolean` | `undefined` | Tắt hoàn toàn tính năng auto-scroll. | `[ignoreAutoScroll]="true"` |
451
+ | `[movementLength]` | `number` | `6` | Số pixel scroll mỗi tick khi item đang gần mép container. | `[movementLength]="10"` |
452
+ | `[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"` |
453
+ | `[virtualScrollerComponent]` | `VirtualScrollerComponent` | `undefined` | Tham chiếu đến `VirtualScrollerComponent` khi dùng `@iharbeck/ngx-virtual-scroller`. | `[virtualScrollerComponent]="vsRef"` |
454
+ | `[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"` |
333
455
 
334
456
  ## Types & Interfaces
335
457
 
336
458
  ```typescript
337
- export interface IDragging {
459
+ import {
460
+ IDragging,
461
+ IDragStart,
462
+ IDragOver,
463
+ IDragLeave,
464
+ IDragEnd,
465
+ IDrop,
466
+ IItemDragInfo,
467
+ IMousePosition,
468
+ IDragDropFunctionControlEvent,
469
+ IDragItemInContainerVirtualScroll,
470
+ } from '@libs-ui/components-drag-drop';
471
+
472
+ /** Event khi đang kéo (mousemove) */
473
+ interface IDragging {
338
474
  mousePosition: IMousePosition;
339
- elementDrag: HTMLElement;
340
- elementKeepContainer?: boolean;
475
+ elementDrag: HTMLElement; // Clone element đang được kéo
476
+ elementKeepContainer?: boolean; // true nếu chuột nằm ngoài mọi container
341
477
  itemDragInfo?: IItemDragInfo;
342
478
  }
343
479
 
344
- export interface IDragStart {
480
+ /** Event khi bắt đầu kéo */
481
+ interface IDragStart {
345
482
  mousePosition: IMousePosition;
346
483
  elementDrag: HTMLElement;
347
484
  itemDragInfo?: IItemDragInfo;
348
485
  }
349
486
 
350
- export interface IDragOver {
487
+ /** Event khi item đang kéo di chuyển qua element khác */
488
+ interface IDragOver {
351
489
  mousePosition: IMousePosition;
352
490
  elementDrag: HTMLElement;
353
- elementDragOver: HTMLElement;
491
+ elementDragOver: HTMLElement; // Element đang bị hover qua
354
492
  itemDragInfo?: IItemDragInfo;
355
493
  }
356
494
 
357
- export interface IDragLeave {
495
+ /** Event khi item đang kéo rời khỏi element */
496
+ interface IDragLeave {
358
497
  elementDrag: HTMLElement;
359
- elementDragLeave: HTMLElement;
498
+ elementDragLeave: HTMLElement; // Element vừa bị rời khỏi
360
499
  itemDragInfo?: IItemDragInfo;
361
500
  }
362
501
 
363
- export interface IDragEnd {
502
+ /** Event khi kết thúc kéo (mouseup) */
503
+ interface IDragEnd {
364
504
  mousePosition: IMousePosition;
365
505
  elementDrag: HTMLElement;
366
506
  itemDragInfo?: IItemDragInfo;
367
507
  }
368
508
 
369
- export interface IDrop {
509
+ /** Event khi drop item vào container/item khác */
510
+ interface IDrop {
370
511
  elementDrag: HTMLElement;
371
- elementDrop: HTMLElement;
512
+ elementDrop: HTMLElement; // Container hoặc item đích nhận drop
372
513
  itemDragInfo?: IItemDragInfo;
373
514
  }
374
515
 
375
- export interface IItemDragInfo {
376
- item: object;
377
- itemsMove?: WritableSignal<Array<unknown>>;
378
- indexDrag?: number;
379
- indexDrop?: number;
380
- itemsDrag: WritableSignal<Array<unknown>>;
381
- itemsDrop?: WritableSignal<Array<unknown>>;
382
- containerDrag?: HTMLElement;
383
- containerDrop?: HTMLElement;
384
- }
385
-
386
- export interface IDragItemInContainerVirtualScroll {
387
- itemDragInfo?: IItemDragInfo;
388
- elementDrag: HTMLElement;
389
- distanceStartElementAndMouseTop: number;
390
- distanceStartElementAndMouseLeft: number;
391
- elementContainer?: HTMLElement;
392
- dragBoundary?: boolean;
393
- dragBoundaryAcceptMouseLeaveContainer?: boolean;
394
- ignoreStopEvent?: boolean;
395
- ignoreUserSelectNone: boolean;
516
+ /** Thông tin chi tiết về item đang/vừa được kéo */
517
+ interface IItemDragInfo {
518
+ item: object; // Dữ liệu của item
519
+ itemsMove?: WritableSignal<Array<unknown>>; // Danh sách nguồn sau khi đã xóa item kéo (dùng cho move mode)
520
+ indexDrag?: number; // Vị trí (index) ban đầu của item trong container nguồn
521
+ indexDrop?: number; // Vị trí (index) cuối cùng của item sau khi drop
522
+ itemsDrag: WritableSignal<Array<unknown>>; // Signal danh sách của container nguồn
523
+ itemsDrop?: WritableSignal<Array<unknown>>; // Signal danh sách của container đích
524
+ containerDrag?: HTMLElement; // HTMLElement của container nguồn
525
+ containerDrop?: HTMLElement; // HTMLElement của container đích
396
526
  }
397
527
 
398
- export interface IMousePosition {
528
+ /** Vị trí con trỏ chuột */
529
+ interface IMousePosition {
399
530
  clientX: number;
400
531
  clientY: number;
401
532
  }
402
533
 
403
- export interface IDragDropFunctionControlEvent {
534
+ /** Object functions điều khiển container emit qua (outFunctionControl) */
535
+ interface IDragDropFunctionControlEvent {
536
+ /** Đồng bộ lại attributes và index cho tất cả items trong container */
404
537
  setAttributeElementAndItemDrag: () => Promise<void>;
405
538
  }
406
539
  ```
407
540
 
408
- ## Tùy chỉnh giao diện (Styling)
409
-
410
- ### CSS Classes mặc định
541
+ ## CSS Classes mặc định
411
542
 
412
543
  | Class | Mô tả |
413
544
  |---|---|
414
- | `.libs-ui-drag-drop-container` | Class cho container kéo thả. |
415
- | `.libs-ui-drag-drop-item` | Class cho mỗi item kéo thả. |
416
- | `.libs-ui-drag-drop-item-dragging` | Class được thêm vào item đang được kéo. |
417
- | `.libs-ui-drag-drop-item-placeholder` | Class cho placeholder tại vị trí drop. |
418
-
419
- ### Sử dụng stylesOverride
420
-
421
- Ghi đè styles thông qua input `[stylesOverride]` trên Container directive:
545
+ | `.libs-ui-drag-drop-container` | Tự động được thêm vào element có `LibsUiComponentsDragContainerDirective` |
546
+ | `.libs-ui-drag-drop-container-dragover` | Được thêm khi item đang kéo vào trong container |
547
+ | `.libs-ui-drag-drop-item` | Tự động được thêm vào element `LibsUiDragItemDirective` |
548
+ | `.libs-ui-drag-drop-item-dragging` | Được thêm vào clone element đang được kéo (cursor: move) |
549
+ | `.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) |
550
+ | `.libs-ui-drag-drop-item-origin-placeholder` | Item gốc đang bị kéo (trong container nguồn) |
551
+ | `.libs-ui-drag-drop-item-drop-placeholder` | Item tạm thời (ghost) hiển thị tại vị trí drop đích |
552
+ | `.libs-ui-drag-drop-item-translate-top` | Animation: item dịch chuyển lên khi kéo theo chiều vertical |
553
+ | `.libs-ui-drag-drop-item-translate-bottom` | Animation: item dịch chuyển xuống khi kéo theo chiều vertical |
554
+ | `.libs-ui-drag-drop-item-translate-left` | Animation: item dịch chuyển sang trái khi kéo theo chiều horizontal |
555
+ | `.libs-ui-drag-drop-item-translate-right` | Animation: item dịch chuyển sang phải khi kéo theo chiều horizontal |
556
+
557
+ ## Tùy chỉnh giao diện qua stylesOverride
422
558
 
423
559
  ```typescript
560
+ import { Component, signal } from '@angular/core';
561
+ import {
562
+ LibsUiComponentsDragContainerDirective,
563
+ LibsUiDragItemDirective,
564
+ } from '@libs-ui/components-drag-drop';
565
+
424
566
  @Component({
567
+ selector: 'app-custom-styled',
425
568
  standalone: true,
569
+ changeDetection: ChangeDetectionStrategy.OnPush,
426
570
  imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
427
571
  template: `
428
572
  <div
429
573
  LibsUiComponentsDragContainerDirective
430
574
  [(items)]="items"
575
+ [acceptDragSameGroup]="true"
431
576
  [stylesOverride]="customStyles">
432
- <div
433
- *ngFor="let item of items"
434
- LibsUiDragItemDirective
435
- [item]="item">
436
- {{ item.name }}
437
- </div>
577
+ @for (item of items(); track item.id) {
578
+ <div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
579
+ <div class="p-3 bg-white border rounded-lg">{{ item.name }}</div>
580
+ </div>
581
+ }
438
582
  </div>
439
583
  `,
440
584
  })
441
- export class CustomStyledComponent {
442
- items = [{ id: 1, name: 'Item 1' }];
585
+ export class CustomStyledDragComponent {
586
+ items = signal([
587
+ { id: 1, name: 'Item 1' },
588
+ { id: 2, name: 'Item 2' },
589
+ ]);
443
590
 
444
591
  customStyles = [
445
592
  {
446
- className: 'custom-drag-container',
593
+ className: 'libs-ui-drag-drop-item-origin-placeholder',
447
594
  styles: `
448
- .custom-drag-container {
449
- background: #f5f5f5;
450
- border-radius: 8px;
451
- padding: 16px;
595
+ .libs-ui-drag-drop-item-origin-placeholder {
596
+ opacity: 0.3;
597
+ background: #e0f2fe;
452
598
  }
453
599
  `,
454
600
  },
455
601
  {
456
- className: 'custom-drag-item-dragging',
602
+ className: 'libs-ui-drag-drop-item-drop-placeholder',
457
603
  styles: `
458
- .custom-drag-item-dragging {
459
- opacity: 0.6;
460
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
604
+ .libs-ui-drag-drop-item-drop-placeholder {
605
+ border: 2px dashed #3b82f6;
606
+ background: #eff6ff;
461
607
  }
462
608
  `,
463
609
  },
@@ -465,6 +611,26 @@ export class CustomStyledComponent {
465
611
  }
466
612
  ```
467
613
 
614
+ ## Lưu ý quan trọng
615
+
616
+ ⚠️ **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.
617
+
618
+ ⚠️ **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í.
619
+
620
+ ⚠️ **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.
621
+
622
+ ⚠️ **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.
623
+
624
+ ⚠️ **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.
625
+
626
+ ⚠️ **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.
627
+
628
+ ⚠️ **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.
629
+
468
630
  ## Demo
469
631
 
470
- - Local: http://localhost:4500/drag-drop
632
+ ```bash
633
+ npx nx serve core-ui
634
+ ```
635
+
636
+ Truy cập: http://localhost:4500/drag-drop