@libs-ui/components-drag-drop 0.2.356-41 → 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 +432 -266
- package/drag-drop.service.d.ts +6 -0
- package/esm2022/drag-drop.directive.mjs +13 -27
- package/esm2022/drag-drop.service.mjs +22 -1
- package/esm2022/drag-item.directive.mjs +18 -4
- package/esm2022/drag-scroll.directive.mjs +4 -2
- package/esm2022/index.mjs +1 -2
- package/fesm2022/libs-ui-components-drag-drop.mjs +53 -860
- package/fesm2022/libs-ui-components-drag-drop.mjs.map +1 -1
- package/index.d.ts +0 -1
- package/package.json +4 -6
- package/demo/demo.component.d.ts +0 -56
- package/esm2022/demo/demo.component.mjs +0 -833
package/README.md
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
# @libs-ui/components-drag-drop
|
|
2
2
|
|
|
3
|
-
>
|
|
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 và tích hợp virtual scroll.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Giới thiệu
|
|
6
6
|
|
|
7
|
-
`
|
|
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
|
-
##
|
|
9
|
+
## Tính năng
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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 mà không phụ thuộc HTML5 Drag API
|
|
21
31
|
|
|
22
|
-
##
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
##
|
|
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
|
-
###
|
|
63
|
+
### Ví 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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
###
|
|
111
|
+
### Ví 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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
[
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
[
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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-
|
|
152
|
-
<
|
|
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]="
|
|
158
|
-
[
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
{
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
###
|
|
273
|
+
### Ví 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
|
-
|
|
189
|
-
|
|
190
|
-
LibsUiDragItemDirective,
|
|
191
|
-
LibsUiComponentsDragScrollDirective,
|
|
192
|
-
LibsUiDragItemInContainerVirtualScrollDirective,
|
|
193
|
-
],
|
|
285
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
286
|
+
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
|
|
194
287
|
template: `
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
[
|
|
204
|
-
[
|
|
205
|
-
[
|
|
206
|
-
|
|
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
|
-
</
|
|
322
|
+
</div>
|
|
210
323
|
`,
|
|
211
324
|
})
|
|
212
|
-
export class
|
|
213
|
-
|
|
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
|
-
###
|
|
336
|
+
### Ví 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
|
-
|
|
349
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
350
|
+
imports: [
|
|
351
|
+
LibsUiComponentsDragContainerDirective,
|
|
352
|
+
LibsUiDragItemDirective,
|
|
353
|
+
LibsUiComponentsDragScrollDirective,
|
|
354
|
+
],
|
|
223
355
|
template: `
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
{ id: 1, name:
|
|
242
|
-
|
|
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
|
-
##
|
|
248
|
-
|
|
249
|
-
### LibsUiComponentsDragContainerDirective
|
|
383
|
+
## @Input() — LibsUiComponentsDragContainerDirective
|
|
250
384
|
|
|
251
385
|
Selector: `[LibsUiComponentsDragContainerDirective]`
|
|
252
386
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
|
256
|
-
|
|
257
|
-
| `[
|
|
258
|
-
| `[
|
|
259
|
-
| `[
|
|
260
|
-
| `[
|
|
261
|
-
| `[
|
|
262
|
-
| `[
|
|
263
|
-
| `[
|
|
264
|
-
| `[
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
|
271
|
-
|
|
272
|
-
| `(
|
|
273
|
-
| `(
|
|
274
|
-
| `(
|
|
275
|
-
| `(
|
|
276
|
-
| `(
|
|
277
|
-
|
|
278
|
-
|
|
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` | Vô 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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
|
287
|
-
|
|
288
|
-
| `[
|
|
289
|
-
| `[
|
|
290
|
-
| `[
|
|
291
|
-
| `[
|
|
292
|
-
| `[
|
|
293
|
-
| `[
|
|
294
|
-
| `[
|
|
295
|
-
| `[
|
|
296
|
-
| `[
|
|
297
|
-
| `[
|
|
298
|
-
| `[
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
|
305
|
-
|
|
306
|
-
| `(
|
|
307
|
-
| `(
|
|
308
|
-
| `(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
### LibsUiComponentsDragScrollDirective
|
|
416
|
+
| Input | Type | Default | Mô tả | Ví dụ |
|
|
417
|
+
|---|---|---|---|---|
|
|
418
|
+
| `[disable]` | `boolean` | `undefined` | Vô 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()` và `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
|
|
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
|
-
|
|
|
321
|
-
|
|
322
|
-
| `[ignoreAutoScroll]` | `boolean` | `undefined` | Tắt tính năng auto
|
|
323
|
-
| `[
|
|
324
|
-
| `[
|
|
325
|
-
| `[
|
|
326
|
-
| `[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
528
|
+
/** Vị trí con trỏ chuột */
|
|
529
|
+
interface IMousePosition {
|
|
399
530
|
clientX: number;
|
|
400
531
|
clientY: number;
|
|
401
532
|
}
|
|
402
533
|
|
|
403
|
-
|
|
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
|
-
##
|
|
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` |
|
|
415
|
-
| `.libs-ui-drag-drop-
|
|
416
|
-
| `.libs-ui-drag-drop-item
|
|
417
|
-
| `.libs-ui-drag-drop-item-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 có item đang kéo vào trong container |
|
|
547
|
+
| `.libs-ui-drag-drop-item` | Tự động được thêm vào element có `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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
442
|
-
items = [
|
|
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: '
|
|
593
|
+
className: 'libs-ui-drag-drop-item-origin-placeholder',
|
|
447
594
|
styles: `
|
|
448
|
-
.
|
|
449
|
-
|
|
450
|
-
|
|
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: '
|
|
602
|
+
className: 'libs-ui-drag-drop-item-drop-placeholder',
|
|
457
603
|
styles: `
|
|
458
|
-
.
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
632
|
+
```bash
|
|
633
|
+
npx nx serve core-ui
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Truy cập: http://localhost:4500/drag-drop
|