@libs-ui/services-grid-layout 0.2.357-19
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 +109 -0
- package/canvas/grid-layout-canvas.component.d.ts +107 -0
- package/defines/gridster-config.define.d.ts +21 -0
- package/esm2022/canvas/grid-layout-canvas.component.mjs +193 -0
- package/esm2022/defines/gridster-config.define.mjs +123 -0
- package/esm2022/grid-layout.service.mjs +433 -0
- package/esm2022/index.mjs +10 -0
- package/esm2022/interfaces/grid-layout.interface.mjs +2 -0
- package/esm2022/libs-ui-services-grid-layout.mjs +5 -0
- package/esm2022/tree.service.mjs +180 -0
- package/fesm2022/libs-ui-services-grid-layout.mjs +933 -0
- package/fesm2022/libs-ui-services-grid-layout.mjs.map +1 -0
- package/grid-layout.service.d.ts +157 -0
- package/index.d.ts +3 -0
- package/interfaces/grid-layout.interface.d.ts +171 -0
- package/package.json +29 -0
- package/tree.service.d.ts +59 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, DestroyRef, signal, input, output, viewChild, viewChildren, ViewContainerRef, computed, effect, Component, ChangeDetectionStrategy } from '@angular/core';
|
|
3
|
+
import { uuid, cloneDeep } from '@libs-ui/utils';
|
|
4
|
+
import { take } from 'rxjs';
|
|
5
|
+
import * as i1 from 'gridster18';
|
|
6
|
+
import { GridType, CompactType, DisplayGrid, GridsterComponent, GridsterModule } from 'gridster18';
|
|
7
|
+
import { LibsUiComponentsButtonsButtonComponent } from '@libs-ui/components-buttons-button';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cấu hình gridster cho lưới CẤP TRANG.
|
|
11
|
+
*
|
|
12
|
+
* Mọi giá trị dưới đây đều đến từ một lỗi đã đo được trên sản phẩm thật, không phải chọn theo cảm
|
|
13
|
+
* tính. Đổi bất kỳ dòng nào có dấu 🔴 sẽ làm sống lại đúng lỗi ghi trong comment.
|
|
14
|
+
*/
|
|
15
|
+
const gridLayoutRootConfig = () => ({
|
|
16
|
+
// 🔴 VerticalFixed — CẤM đổi sang Fit hay ScrollVertical.
|
|
17
|
+
// `Fit` chia đều chiều cao khung cho số hàng đang dùng, nên card khai `rows: 2` phình từ 124px lên
|
|
18
|
+
// 247px. `ScrollVertical` KHÔNG đọc `fixedRowHeight` (mã lib: chỉ `Fixed` và `VerticalFixed` đọc).
|
|
19
|
+
gridType: GridType.VerticalFixed,
|
|
20
|
+
// 🔴 None — CẤM quay lại CompactUp.
|
|
21
|
+
// `CompactUp` hút mọi khối lên sát nhau NGAY sau mỗi thao tác, nên:
|
|
22
|
+
// · kéo mép dưới cho khối cao thêm → chỗ khối dưới vừa nhường bị hút lấp lại tức thì, cú kéo bị
|
|
23
|
+
// `checkGridCollision` báo va chạm rồi HOÀN NGUYÊN (khối đứng im dù kéo);
|
|
24
|
+
// · kéo thả đổi vị trí → khối vừa nhường chỗ bị hút về ngay, khối đang kéo không có chỗ đặt và
|
|
25
|
+
// treo lơ lửng giữa hai khối.
|
|
26
|
+
// Muốn dồn khít thì gọi `sapGon()` của service.
|
|
27
|
+
compactType: CompactType.None,
|
|
28
|
+
displayGrid: DisplayGrid.OnDragAndResize,
|
|
29
|
+
// 🔴 pushItems: true — CẤM để false.
|
|
30
|
+
// `swap` của lib CHỈ hoán vị được HAI KHỐI CÙNG KÍCH THƯỚC (mã lib `checkSwap`: tính diffX/diffY
|
|
31
|
+
// rồi hoán vị, lệch kích thước thì `checkCollision` báo va chạm và hoàn nguyên). Khối 3x2 gặp
|
|
32
|
+
// container 7x5 nên không swap được, mà `pushItems: false` cũng không cho container nhường đường
|
|
33
|
+
// → BẾ TẮC: khối đang kéo không đặt được ở đâu.
|
|
34
|
+
pushItems: true,
|
|
35
|
+
// chỉ đẩy XUỐNG: đẩy ngang làm xô lệch bố cục cột người dùng vừa đặt
|
|
36
|
+
pushDirections: { north: false, east: false, south: true, west: false },
|
|
37
|
+
pushResizeItems: true,
|
|
38
|
+
swap: true,
|
|
39
|
+
swapWhileDragging: true,
|
|
40
|
+
draggable: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
// 🔴 KHÔNG được bỏ. Không có nó thì gridster vào chế độ kéo ngay từ `mousedown`, nên chỉ bấm
|
|
43
|
+
// CHỌN một khối là nó đã nhích theo dù chuột đứng yên. Lib tự huỷ kéo khi nhả chuột trước mốc
|
|
44
|
+
// này nên cú bấm chọn vẫn ăn nguyên vẹn.
|
|
45
|
+
delayStart: 160,
|
|
46
|
+
// 🔴 `checkDragHandleClass` duyệt DOM từ chỗ bấm NGƯỢC LÊN CHA, và trên MỖI node nó kiểm
|
|
47
|
+
// `dragHandleClass` TRƯỚC `ignoreContentClass`. Vì vậy một phần tử KHÔNG được mang cả hai class
|
|
48
|
+
// cùng lúc — mang cả hai thì lib gặp tay cầm trước, trả `true`, class chặn không tới lượt.
|
|
49
|
+
ignoreContentClass: 'libs-ui-grid-layout-block-parent-drag',
|
|
50
|
+
ignoreContent: true,
|
|
51
|
+
dragHandleClass: 'libs-ui-grid-layout-drag-handle',
|
|
52
|
+
},
|
|
53
|
+
resizable: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
// đủ 8 hướng như cửa sổ thường; chỉ có góc dưới-phải thì quá khó trúng
|
|
56
|
+
handles: { s: true, e: true, n: true, w: true, se: true, ne: true, sw: true, nw: true },
|
|
57
|
+
},
|
|
58
|
+
margin: 12,
|
|
59
|
+
outerMargin: true,
|
|
60
|
+
outerMarginBottom: 40,
|
|
61
|
+
minCols: 12,
|
|
62
|
+
maxCols: 12,
|
|
63
|
+
minRows: 2,
|
|
64
|
+
// 🔴 KHÔNG giới hạn chiều cao khối. Lib có BA trần mặc định phải vô hiệu hoá CÙNG LÚC, thiếu một
|
|
65
|
+
// cái là vẫn bị chặn: `maxItemRows: 50` · `maxItemCols: 50` · `maxItemArea: 2500` (trần DIỆN TÍCH
|
|
66
|
+
// cols × rows — khó thấy nhất: khối 12 cột chỉ cần 209 hàng là đụng trần dù hai trần kia còn dư).
|
|
67
|
+
// `maxRows` là trần của CẢ LƯỚI, cũng phải nới theo.
|
|
68
|
+
maxRows: 5000,
|
|
69
|
+
maxItemRows: 5000,
|
|
70
|
+
maxItemCols: 12,
|
|
71
|
+
maxItemArea: 60000,
|
|
72
|
+
// 🔴 Trần LỚP CHỒNG. Mặc định của lib là 2 — quá thấp: khi bật chồng khối, thư viện gán mỗi khối
|
|
73
|
+
// một lớp riêng để chúng khác nhau, nên chỉ 3 khối là đã đụng trần và các khối sau lại rơi về
|
|
74
|
+
// cùng lớp, tức chồng nhau bị chặn trở lại.
|
|
75
|
+
maxLayerIndex: 5000,
|
|
76
|
+
setGridSize: true,
|
|
77
|
+
fixedRowHeight: 56,
|
|
78
|
+
defaultItemCols: 3,
|
|
79
|
+
defaultItemRows: 2,
|
|
80
|
+
minItemRows: 1,
|
|
81
|
+
mobileBreakpoint: -1,
|
|
82
|
+
});
|
|
83
|
+
/**
|
|
84
|
+
* Cấu hình cho lưới LỒNG (ruột một container).
|
|
85
|
+
*
|
|
86
|
+
* Kế thừa lưới trang rồi ghi đè phần khác biệt. Ba điểm quan trọng nhất: `setGridSize` phải TẮT,
|
|
87
|
+
* tay cầm phải mang TÊN RIÊNG, và tự-cuộn-khi-kéo phải TẮT.
|
|
88
|
+
*/
|
|
89
|
+
const gridLayoutNestedConfig = () => ({
|
|
90
|
+
...gridLayoutRootConfig(),
|
|
91
|
+
// 🔴 PHẢI tắt cho lưới lồng. `setGridSize` làm gridster tự ghi style width/height lên CHÍNH nó,
|
|
92
|
+
// sinh vòng lặp đo-rồi-ghi với ResizeObserver của khung ngoài (khung đổi → onResize xoá style →
|
|
93
|
+
// setGridSize ghi lại) → lưới con co dần về 64x64px trong khung 868px.
|
|
94
|
+
setGridSize: false,
|
|
95
|
+
// khe 2px: khối con là các dòng thông tin của CÙNG một card, khe rộng làm chúng trông như card rời
|
|
96
|
+
margin: 2,
|
|
97
|
+
// Lưới lồng KHÔNG tự chừa lề — lề là của VỎ container (`containerPadding`), không phải của lưới.
|
|
98
|
+
// Đặt ở vỏ thì ruột co lại đúng bằng phần còn lại, tay cầm resize của khối con luôn nằm trong tầm
|
|
99
|
+
// chuột (người dùng chốt 28/08/2026: "lưới cha có padding chứ ko phải con").
|
|
100
|
+
outerMargin: false,
|
|
101
|
+
outerMarginBottom: 0,
|
|
102
|
+
// hàng 16px cho bước kéo mịn, và sàn 1 hàng = 16px nên khối co được nhỏ hơn nhiều
|
|
103
|
+
fixedRowHeight: 16,
|
|
104
|
+
minRows: 1,
|
|
105
|
+
minItemRows: 1,
|
|
106
|
+
defaultItemCols: 2,
|
|
107
|
+
defaultItemRows: 2,
|
|
108
|
+
// 🔴 Tay cầm RIÊNG — CẤM dùng chung tên với lưới trang.
|
|
109
|
+
// `checkDragHandleClass` trả `true` ở node ĐẦU TIÊN có `dragHandleClass`. Dùng chung một tên thì
|
|
110
|
+
// lưới CHA cũng nhận ra tay cầm của khối con và nhấc luôn container.
|
|
111
|
+
draggable: {
|
|
112
|
+
...gridLayoutRootConfig().draggable,
|
|
113
|
+
dragHandleClass: 'libs-ui-grid-layout-drag-handle-child',
|
|
114
|
+
},
|
|
115
|
+
// 🔴 KÉO MÉP thì khối dưới ĐỨNG YÊN, chỉ khoảng trống giữa hai khối co giãn. Bật (kế thừa từ lưới
|
|
116
|
+
// trang) thì mỗi cú kéo đẩy dồn cả cột xuống — người dùng chỉ muốn một khối to hơn một chút mà cả
|
|
117
|
+
// card dài ra. `pushItems` vẫn giữ: nó lo việc KÉO THẢ đổi chỗ, khác việc đổi kích thước.
|
|
118
|
+
pushResizeItems: false,
|
|
119
|
+
// 🔴 Tắt tự cuộn khi kéo tới sát mép. Lib thấy chuột gần biên là cuộn lưới theo
|
|
120
|
+
// (`scrollSensitivity`), nhưng lưới con nằm trong card có `overflow: hidden` — cuộn ở đây đẩy khối
|
|
121
|
+
// con ra NGOÀI khung container rồi kẹt luôn ở đó.
|
|
122
|
+
disableScrollHorizontal: true,
|
|
123
|
+
disableScrollVertical: true,
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* Cây rỗng dùng khi chưa có dữ liệu — mặt phẳng vẽ ra một lưới trống thay vì vỡ.
|
|
127
|
+
* Xảy ra trong nhịp giữa lúc component dựng và lúc `init()` chạy.
|
|
128
|
+
*/
|
|
129
|
+
const emptyRootNode = () => ({ id: 'root', x: 0, y: 0, cols: 12, rows: 0, children: [] });
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Thao tác trên cây khối: tìm, thêm, xoá, dồn khít, chặn lồng cấp quá sâu.
|
|
133
|
+
*
|
|
134
|
+
* 🔴 NỘI BỘ thư viện — không xuất ra ngoài. Nơi dùng đi qua `LibsUiGridLayoutService`, ở đó mỗi
|
|
135
|
+
* thao tác sửa bố cục vừa vẽ lại vừa bắn `outChange` đúng MỘT lần. Các hàm ở đây cố tình KHÔNG tự
|
|
136
|
+
* vẽ lại: để chúng tự gọi thì mỗi thao tác vẽ hai lượt và bắn hai sự kiện (người dùng chốt
|
|
137
|
+
* 28/08/2026: "đặt đúng các điểm mà làm cho layout có thể thay đổi thôi").
|
|
138
|
+
*/
|
|
139
|
+
class LibsUiGridLayoutTreeService {
|
|
140
|
+
findById(node, id) {
|
|
141
|
+
if (node.id === id) {
|
|
142
|
+
return node;
|
|
143
|
+
}
|
|
144
|
+
for (const child of node.children ?? []) {
|
|
145
|
+
const found = this.findById(child, id);
|
|
146
|
+
if (found) {
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
findParent(node, id) {
|
|
153
|
+
for (const child of node.children ?? []) {
|
|
154
|
+
if (child.id === id) {
|
|
155
|
+
return node;
|
|
156
|
+
}
|
|
157
|
+
const found = this.findParent(child, id);
|
|
158
|
+
if (found) {
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Thêm khối vào cuối một container. Thư viện tự sinh `id` và tự đặt `x` · `y` xuống hàng trống.
|
|
166
|
+
*
|
|
167
|
+
* @returns khối vừa tạo (kèm `id` vừa sinh), hoặc `undefined` khi không tìm thấy container.
|
|
168
|
+
*/
|
|
169
|
+
addChild(root, parentId, item) {
|
|
170
|
+
const parent = this.findById(root, parentId);
|
|
171
|
+
if (!parent) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
parent.children = parent.children ?? [];
|
|
175
|
+
// 🔴 Thư viện tự sinh `id`. Đây là mã máy dùng để tra cứu, không phải thứ người dùng nghĩ ra —
|
|
176
|
+
// bắt họ tự đặt là mời họ tạo trùng, mà trùng id thì `findById` trả nhầm khối và cả cây hỏng
|
|
177
|
+
// im lặng (người dùng chốt 28/08/2026).
|
|
178
|
+
const node = { ...item, id: item.id ?? uuid(), x: 0, y: this.nextFreeRow(parent.children) };
|
|
179
|
+
parent.children.push(node);
|
|
180
|
+
// Trả về khối vừa tạo để nơi dùng biết `id` thư viện vừa sinh.
|
|
181
|
+
return node;
|
|
182
|
+
}
|
|
183
|
+
/** Xoá một khối khỏi cây. Không cho xoá gốc. */
|
|
184
|
+
removeById(root, id) {
|
|
185
|
+
if (root.id === id) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const parent = this.findParent(root, id);
|
|
189
|
+
if (!parent?.children) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
parent.children = parent.children.filter((item) => item.id !== id);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Chuyển một khối đơn thành CONTAINER: nội dung cũ trở thành khối con đầu tiên.
|
|
197
|
+
*
|
|
198
|
+
* Không mất dữ liệu — `data` của khối được mang theo xuống khối con.
|
|
199
|
+
*
|
|
200
|
+
* @returns `false` khi không tìm thấy khối, hoặc khối đã là container rồi.
|
|
201
|
+
*/
|
|
202
|
+
toContainer(root, id, colCount) {
|
|
203
|
+
const node = this.findById(root, id);
|
|
204
|
+
if (!node || (node.children?.length ?? 0) > 0) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
node.children = [{ ...node, id: uuid(), x: 0, y: 0, cols: colCount, rows: 4, children: [] }];
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Chuyển một CONTAINER về khối đơn: các khối con được NÂNG lên cấp trên, không bị xoá.
|
|
212
|
+
*
|
|
213
|
+
* @returns `false` khi không tìm thấy khối, hoặc khối vốn không phải container.
|
|
214
|
+
*/
|
|
215
|
+
toSingle(root, id) {
|
|
216
|
+
const node = this.findById(root, id);
|
|
217
|
+
if (!node || (node.children?.length ?? 0) === 0) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const parent = this.findParent(root, id);
|
|
221
|
+
for (const child of node.children ?? []) {
|
|
222
|
+
parent?.children?.push({ ...child, cols: node.cols, rows: 2 });
|
|
223
|
+
}
|
|
224
|
+
node.children = [];
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Làm phẳng mọi container lồng vượt trần.
|
|
229
|
+
*
|
|
230
|
+
* 🔴 Cần thiết vì bố cục ĐÃ LƯU không đi qua luật chặn nào của giao diện. Một bản lưu cũ — sinh
|
|
231
|
+
* trước khi có trần, hoặc do thao tác lỗi — vẫn nạp thẳng vào và hiện container lồng container.
|
|
232
|
+
* Container vượt trần bị hạ thành khối thường, con của nó nâng lên cấp trên: KHÔNG mất dữ liệu.
|
|
233
|
+
*
|
|
234
|
+
* @returns số container đã làm phẳng
|
|
235
|
+
*/
|
|
236
|
+
flattenOverDepthContainers(node, maxDepth) {
|
|
237
|
+
return this.walkFlatten(node, 0, maxDepth);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Dồn khối khít lên trên theo thứ tự đọc, lấp hết chỗ trống.
|
|
241
|
+
*
|
|
242
|
+
* 🔴 KHÔNG sửa `node.rows` của container. Chiều cao container tính theo đơn vị hàng của lưới CHA,
|
|
243
|
+
* còn `currentRow` đếm theo đơn vị hàng của lưới CON — trộn hai thang đo là container phình lên
|
|
244
|
+
* gấp mấy lần.
|
|
245
|
+
*/
|
|
246
|
+
compact(node, colCount) {
|
|
247
|
+
this.walkCompact(node, colCount);
|
|
248
|
+
}
|
|
249
|
+
walkCompact(node, colCount) {
|
|
250
|
+
const children = node.children;
|
|
251
|
+
if (!children?.length) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const inReadingOrder = [...children].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
255
|
+
let currentRow = 0;
|
|
256
|
+
let usedCols = 0;
|
|
257
|
+
let tallestInRow = 0;
|
|
258
|
+
for (const item of inReadingOrder) {
|
|
259
|
+
const width = Math.min(item.cols, colCount);
|
|
260
|
+
if (usedCols > 0 && usedCols + width > colCount) {
|
|
261
|
+
currentRow += tallestInRow;
|
|
262
|
+
usedCols = 0;
|
|
263
|
+
tallestInRow = 0;
|
|
264
|
+
}
|
|
265
|
+
item.x = usedCols;
|
|
266
|
+
item.y = currentRow;
|
|
267
|
+
item.cols = width;
|
|
268
|
+
usedCols += width;
|
|
269
|
+
tallestInRow = Math.max(tallestInRow, item.rows);
|
|
270
|
+
if ((item.children?.length ?? 0) > 0) {
|
|
271
|
+
this.walkCompact(item, colCount);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
walkFlatten(node, containerDepth, maxDepth) {
|
|
276
|
+
const children = node.children;
|
|
277
|
+
if (!children?.length) {
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
let fixedCount = 0;
|
|
281
|
+
const result = [];
|
|
282
|
+
for (const child of children) {
|
|
283
|
+
const isContainer = (child.children?.length ?? 0) > 0;
|
|
284
|
+
if (isContainer && containerDepth >= maxDepth) {
|
|
285
|
+
fixedCount += 1;
|
|
286
|
+
for (const item of child.children ?? []) {
|
|
287
|
+
fixedCount += this.walkFlatten(item, containerDepth, maxDepth);
|
|
288
|
+
result.push(item);
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
fixedCount += this.walkFlatten(child, isContainer ? containerDepth + 1 : containerDepth, maxDepth);
|
|
293
|
+
result.push(child);
|
|
294
|
+
}
|
|
295
|
+
node.children = result;
|
|
296
|
+
return fixedCount;
|
|
297
|
+
}
|
|
298
|
+
nextFreeRow(children) {
|
|
299
|
+
return children.reduce((max, child) => Math.max(max, child.y + child.rows), 0);
|
|
300
|
+
}
|
|
301
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutTreeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
302
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutTreeService });
|
|
303
|
+
}
|
|
304
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutTreeService, decorators: [{
|
|
305
|
+
type: Injectable
|
|
306
|
+
}] });
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Dựng một mặt phẳng kéo thả và tự gắn component của nơi dùng lên từng khối.
|
|
310
|
+
*
|
|
311
|
+
* Nơi dùng chỉ cần đưa vào: loại component muốn dựng
|
|
312
|
+
* (`nodeComponentType`) và cây dữ liệu (`data`). Toàn bộ việc vẽ lưới, kéo thả, đổi kích thước,
|
|
313
|
+
* lồng container và dọn dẹp do thư viện lo.
|
|
314
|
+
*
|
|
315
|
+
* 🔴 Đây là service theo TỪNG mặt phẳng, KHÔNG `providedIn: 'root'`. Khai trong `providers` của
|
|
316
|
+
* component dùng nó; hai mặt phẳng trên cùng một màn phải là hai thực thể riêng, dùng chung thì
|
|
317
|
+
* `componentRefs` của mặt phẳng này bị mặt phẳng kia dọn mất.
|
|
318
|
+
*/
|
|
319
|
+
class LibsUiGridLayoutService {
|
|
320
|
+
// 🔴 TỰ TẠO, không `inject`. Nơi dùng chỉ phải viết `providers: [LibsUiGridLayoutService]` —
|
|
321
|
+
// không phải biết bên trong thư viện còn service nào khác (người dùng chốt 28/08/2026).
|
|
322
|
+
// `TreeService` không giữ trạng thái dùng chung nên mỗi mặt phẳng một bản là đúng.
|
|
323
|
+
treeService = new LibsUiGridLayoutTreeService();
|
|
324
|
+
/** Lượt tải component đang chờ, theo id khối — để huỷ khi khối bị gỡ giữa chừng. */
|
|
325
|
+
pendingLoads = new Map();
|
|
326
|
+
constructor() {
|
|
327
|
+
// 🔴 Service khai trong `providers` của component nên Angular huỷ nó cùng component — thư viện
|
|
328
|
+
// tự dọn ở đây, nơi dùng KHÔNG phải gọi `destroy()` trong `ngOnDestroy` (người dùng chốt
|
|
329
|
+
// 28/08/2026). Bắt nhớ một bước dọn dẹp là kiểu API dễ quên, và quên thì rò rỉ bộ nhớ im lặng.
|
|
330
|
+
inject(DestroyRef).onDestroy(() => this.destroy());
|
|
331
|
+
}
|
|
332
|
+
config;
|
|
333
|
+
componentRefs = new Map();
|
|
334
|
+
rootNode = signal(undefined);
|
|
335
|
+
editMode = signal(false);
|
|
336
|
+
/** tăng mỗi lần cây đổi — component con đọc để biết phải tính lại */
|
|
337
|
+
version = signal(0);
|
|
338
|
+
/**
|
|
339
|
+
* Signal chỉ đọc để nơi dùng theo dõi.
|
|
340
|
+
*
|
|
341
|
+
* 🔴 Là THUỘC TÍNH, không phải getter. Getter trả signal thì chỗ đọc phải viết `Version()()` —
|
|
342
|
+
* quên cặp ngoặc thứ hai là computed KHÔNG phụ thuộc vào nó, cây đổi mà lưới đứng im, và
|
|
343
|
+
* TypeScript không hề báo lỗi (đo 28/08/2026: thêm khối không hiện, sắp gọn không nhúc nhích).
|
|
344
|
+
*/
|
|
345
|
+
Root = this.rootNode.asReadonly();
|
|
346
|
+
IsEditMode = this.editMode.asReadonly();
|
|
347
|
+
/** tăng mỗi lần cây hoặc cấu hình đổi */
|
|
348
|
+
Version = this.version.asReadonly();
|
|
349
|
+
/**
|
|
350
|
+
* trần số cấp container lồng nhau — canvas đọc để biết có cho chuyển thành container không */
|
|
351
|
+
maxContainerDepth() {
|
|
352
|
+
return this.config?.maxContainerDepth ?? 1;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* lề trong của container, px — canvas đọc để đặt padding lên vỏ container */
|
|
356
|
+
containerPadding() {
|
|
357
|
+
return this.config?.containerPadding ?? 12;
|
|
358
|
+
}
|
|
359
|
+
/** Khởi tạo mặt phẳng. Gọi lại sẽ dọn sạch component cũ rồi dựng lại từ đầu. */
|
|
360
|
+
init(config) {
|
|
361
|
+
this.destroy();
|
|
362
|
+
this.config = config;
|
|
363
|
+
this.assignOverlapLayers(config.data);
|
|
364
|
+
// 🔴 Nhân bản như `refresh()`: `init()` lại (đổi chế độ, bật/tắt chồng khối) trên CÙNG object cây
|
|
365
|
+
// thì `@for` không dựng lại `gridster-item`, nên `layerIndex` vừa gán không tới `$item` và khối
|
|
366
|
+
// vẫn không đè nhau được (đo 28/08/2026: `item.layerIndex` = 1|2|3|4|7 mà `$item.layerIndex`
|
|
367
|
+
// toàn `undefined`).
|
|
368
|
+
this.rootNode.set(this.shallowCloneTree(config.data));
|
|
369
|
+
this.editMode.set(config.isEditMode ?? false);
|
|
370
|
+
this.version.update((v) => v + 1);
|
|
371
|
+
}
|
|
372
|
+
setEditMode(value) {
|
|
373
|
+
this.editMode.set(value);
|
|
374
|
+
// 🔴 PHẢI bơm `version`: `gridsterOptions()` đọc `editMode` để bật/tắt kéo–thả, mà computed
|
|
375
|
+
// `gridOptions` bên canvas chỉ chạy lại khi `Version` đổi. Thiếu dòng này thì đổi chế độ xong
|
|
376
|
+
// gridster vẫn giữ options cũ — chuyển sang XEM mà khối vẫn kéo được.
|
|
377
|
+
this.version.update((v) => v + 1);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Bật/tắt cho khối chồng lên nhau — KHÔNG dựng lại mặt phẳng.
|
|
381
|
+
*
|
|
382
|
+
* Trước đây nơi dùng phải gọi `init()` lại để đổi cờ này, mà `init()` huỷ sạch rồi dựng lại mọi
|
|
383
|
+
* component nội dung — lãng phí, và người dùng thấy màn chớp (người dùng chốt 28/08/2026).
|
|
384
|
+
*/
|
|
385
|
+
setAllowOverlap(value) {
|
|
386
|
+
if (!this.config) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.config = { ...this.config, allowOverlap: value };
|
|
390
|
+
const root = this.rootNode();
|
|
391
|
+
if (!root) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// 🔴 Gán lớp vào CÂY ĐANG VẼ (`rootNode`), KHÔNG phải `config.data`.
|
|
395
|
+
// `init()` đã clone `config.data` sang `rootNode` nên hai object KHÁC nhau; gán vào `config.data`
|
|
396
|
+
// thì `refresh()` clone lại từ `rootNode` — lớp vừa gán không tới `gridster-item` nào, `z-index`
|
|
397
|
+
// của mọi khối đứng ở `1` và khối nào vẽ sau thì nằm trên: chồng khối "lúc được lúc không"
|
|
398
|
+
// (đo 04/09/2026: `item.layerIndex` undefined, `getLayerIndex()` = 0 cho cả 8 khối).
|
|
399
|
+
this.assignOverlapLayers(root);
|
|
400
|
+
// Thay tham chiếu cây để `layerIndex` vừa gán tới được `$item` của gridster; component nội dung
|
|
401
|
+
// KHÔNG bị dựng lại vì `attachNode` nhận ra id cũ và chỉ `setInput`.
|
|
402
|
+
this.refresh();
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
*
|
|
406
|
+
* Cấu hình gridster cho một cấp lưới.
|
|
407
|
+
*
|
|
408
|
+
* @param depth 0 = lưới cấp trang, ≥ 1 = lưới lồng trong container
|
|
409
|
+
*/
|
|
410
|
+
buildGridsterOptions(depth, itemChangeCallback) {
|
|
411
|
+
const base = depth === 0 ? gridLayoutRootConfig() : gridLayoutNestedConfig();
|
|
412
|
+
const config = this.config;
|
|
413
|
+
if (!config) {
|
|
414
|
+
return base;
|
|
415
|
+
}
|
|
416
|
+
const isFreeMode = config.mode === "free" /* E_gridLayout_mode_string.free */;
|
|
417
|
+
const canEdit = this.editMode();
|
|
418
|
+
const canOverlap = isFreeMode || (config.allowOverlap ?? false);
|
|
419
|
+
return {
|
|
420
|
+
...base,
|
|
421
|
+
...(config.cols ? { minCols: config.cols, maxCols: config.cols, maxItemCols: config.cols } : {}),
|
|
422
|
+
...(config.rowHeight && depth === 0 ? { fixedRowHeight: config.rowHeight } : {}),
|
|
423
|
+
// Khe và chiều cao hàng khác nhau theo cấp: lưới trang thoáng, lưới lồng khít.
|
|
424
|
+
...(depth === 0 && config.margin !== undefined ? { margin: config.margin } : {}),
|
|
425
|
+
...(depth > 0 && config.nestedMargin !== undefined ? { margin: config.nestedMargin } : {}),
|
|
426
|
+
...(depth > 0 && config.nestedRowHeight !== undefined ? { fixedRowHeight: config.nestedRowHeight } : {}),
|
|
427
|
+
...(depth === 0 && config.outerMargin !== undefined ? { outerMargin: config.outerMargin } : {}),
|
|
428
|
+
...(depth === 0 && config.outerMarginBottom !== undefined ? { outerMarginBottom: config.outerMarginBottom } : {}),
|
|
429
|
+
...(config.minItemRows !== undefined ? { minItemRows: config.minItemRows } : {}),
|
|
430
|
+
...(config.defaultItemCols !== undefined ? { defaultItemCols: config.defaultItemCols } : {}),
|
|
431
|
+
...(config.defaultItemRows !== undefined ? { defaultItemRows: config.defaultItemRows } : {}),
|
|
432
|
+
// 🔴 Chế độ XEM phải TẮT hẳn kéo và đổi kích thước.
|
|
433
|
+
// `base` luôn để `enabled: true`; không ghi đè theo `editMode` thì người xem vẫn kéo được khối
|
|
434
|
+
// và đổi bố cục của người khác (người dùng báo 28/08/2026: "đang xem mà vẫn kéo thả thay đổi
|
|
435
|
+
// vị trí và thêm khối được").
|
|
436
|
+
draggable: { ...base.draggable, enabled: canEdit },
|
|
437
|
+
resizable: { ...base.resizable, enabled: canEdit },
|
|
438
|
+
// Chế độ tự do LUÔN cho chồng; chế độ lưới thì theo cờ của nơi dùng.
|
|
439
|
+
allowMultiLayer: canOverlap,
|
|
440
|
+
// 🔴 Bật `allowMultiLayer` MỘT MÌNH không đủ. Mã lib so `layerIndex` của hai khối: KHÁC nhau
|
|
441
|
+
// thì cho đè, GIỐNG nhau vẫn báo va chạm. Khối không khai `layerIndex` đều rơi về
|
|
442
|
+
// `defaultLayerIndex` — tức cùng lớp, và vẫn bị đẩy nhau như lưới thường (người dùng báo
|
|
443
|
+
// 28/08/2026: "bật chồng khối nhưng khi kéo thì các khối vẫn di chuyển theo").
|
|
444
|
+
// Cho chồng thì phải TẮT đẩy khối, nếu không khối bị dạt đi trước khi kịp đè lên nhau.
|
|
445
|
+
...(canOverlap ? { pushItems: false, pushResizeItems: false, swap: false, swapWhileDragging: false } : {}),
|
|
446
|
+
itemChangeCallback: (item) => {
|
|
447
|
+
// 🔴 Khối vừa thao tác phải lên LỚP TRÊN CÙNG.
|
|
448
|
+
// `layerIndex` gán một lần theo thứ tự cây lúc khởi tạo, nên khối kéo lên chỉ nổi TRONG LÚC
|
|
449
|
+
// kéo (nhờ `z-index: 30` của lớp `gridster-item-moving`); thả tay ra là về lớp cũ và bị khối
|
|
450
|
+
// đứng sau trong cây đè ngược lên (người dùng báo 28/08/2026, đo được: kéo z=30, thả về
|
|
451
|
+
// z=10 trong khi hai khối kế bên đang z=11 và z=12).
|
|
452
|
+
if (canOverlap) {
|
|
453
|
+
this.raiseToTopLayer(item);
|
|
454
|
+
}
|
|
455
|
+
this.version.update((v) => v + 1);
|
|
456
|
+
itemChangeCallback?.();
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
*
|
|
462
|
+
* Gắn component của nơi dùng lên một khối.
|
|
463
|
+
*
|
|
464
|
+
* @param host chỗ gắn RIÊNG của ô đó — mỗi khối một `ViewContainerRef`, dùng chung một cái thì
|
|
465
|
+
* mọi component bị dồn hết vào một ô.
|
|
466
|
+
*/
|
|
467
|
+
attachNode(node, depth, host) {
|
|
468
|
+
const config = this.config;
|
|
469
|
+
if (!config) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const existing = this.componentRefs.get(node.id);
|
|
473
|
+
if (existing) {
|
|
474
|
+
this.applyInputs(existing, node, depth);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// Khối tự khai thì hỏi nó; không thì hỏi hàm phân giải chung. Cả hai đều nhận `node` để đọc
|
|
478
|
+
// được `data.type` mà quyết định trả component nào.
|
|
479
|
+
const stream = (node.getComponentOutlet ?? config.getComponentOutlet)?.(node);
|
|
480
|
+
if (!stream) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// 🔴 Huỷ lượt tải CŨ của đúng khối này trước khi mở lượt mới.
|
|
484
|
+
// Khối bị gỡ rồi gắn lại (đổi bố cục, sắp gọn) trong lúc `import()` chưa xong thì lượt cũ vẫn
|
|
485
|
+
// chạy tiếp và dựng component vào một `ViewContainerRef` đã chết.
|
|
486
|
+
this.pendingLoads.get(node.id)?.unsubscribe();
|
|
487
|
+
const sub = stream.pipe(take(1)).subscribe((componentType) => {
|
|
488
|
+
this.pendingLoads.delete(node.id);
|
|
489
|
+
// Khối có thể đã bị xoá trong lúc chờ tải — dựng vào đây là rò rỉ.
|
|
490
|
+
if (!this.treeService.findById(this.rootNode() ?? node, node.id)) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const ref = host.createComponent(componentType);
|
|
494
|
+
this.applyInputs(ref, node, depth);
|
|
495
|
+
this.componentRefs.set(node.id, ref);
|
|
496
|
+
});
|
|
497
|
+
this.pendingLoads.set(node.id, sub);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Gỡ component của một khối và huỷ nó. */
|
|
501
|
+
detachNode(nodeId) {
|
|
502
|
+
// Khối bị gỡ khi component của nó còn đang tải — phải huỷ lượt tải, không thì nó dựng vào một
|
|
503
|
+
// `ViewContainerRef` đã chết.
|
|
504
|
+
this.pendingLoads.get(nodeId)?.unsubscribe();
|
|
505
|
+
this.pendingLoads.delete(nodeId);
|
|
506
|
+
const ref = this.componentRefs.get(nodeId);
|
|
507
|
+
if (!ref) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
ref.destroy();
|
|
511
|
+
this.componentRefs.delete(nodeId);
|
|
512
|
+
}
|
|
513
|
+
destroy() {
|
|
514
|
+
for (const sub of this.pendingLoads.values()) {
|
|
515
|
+
sub.unsubscribe();
|
|
516
|
+
}
|
|
517
|
+
for (const ref of this.componentRefs.values()) {
|
|
518
|
+
ref.destroy();
|
|
519
|
+
}
|
|
520
|
+
this.pendingLoads.clear();
|
|
521
|
+
this.componentRefs.clear();
|
|
522
|
+
this.config = undefined;
|
|
523
|
+
}
|
|
524
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
525
|
+
// Nhóm hàm cho nơi dùng CHỦ ĐỘNG gọi — không phải chờ người dùng bấm nút trên khối.
|
|
526
|
+
// Sửa cây thì dùng thẳng `TreeService` rồi gọi `refresh()` — xem demo trong core-ui.
|
|
527
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
528
|
+
/**
|
|
529
|
+
* Báo cho service biết cây vừa đổi từ bên ngoài (thêm/xoá khối, sắp gọn, nạp bố cục khác).
|
|
530
|
+
*
|
|
531
|
+
* 🔴 PHẢI thay THAM CHIẾU của node gốc, không chỉ bơm `version`.
|
|
532
|
+
* `@for … track item.id` giữ nguyên object khi id không đổi, nên `<gridster-item [item]="…">` vẫn
|
|
533
|
+
* nhận đúng object cũ và gridster không hề biết `x/y/cols/rows/layerIndex` vừa bị sửa. Bơm mỗi
|
|
534
|
+
* `version` thì computed chạy lại nhưng trả về CÙNG mảng cũ — lưới đứng im (đo 28/08/2026: bấm
|
|
535
|
+
* "Sắp gọn" cây đã dồn khít mà màn hình không nhúc nhích; bật chồng khối mà `$item.layerIndex`
|
|
536
|
+
* vẫn trống).
|
|
537
|
+
*
|
|
538
|
+
* Nhân bản NÔNG cả node gốc và danh sách con là đủ: mảng mới → `@for` dựng lại item, object con
|
|
539
|
+
* mới → gridster đọc lại toàn bộ toạ độ.
|
|
540
|
+
*/
|
|
541
|
+
refresh(event) {
|
|
542
|
+
const root = event?.root ?? this.rootNode();
|
|
543
|
+
if (!root) {
|
|
544
|
+
this.version.update((v) => v + 1);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (this.config) {
|
|
548
|
+
this.config = { ...this.config, data: root };
|
|
549
|
+
}
|
|
550
|
+
this.rootNode.set(this.shallowCloneTree(root));
|
|
551
|
+
this.version.update((v) => v + 1);
|
|
552
|
+
}
|
|
553
|
+
/** Nhân bản nông theo cả chiều sâu: mỗi node và mảng `children` thành tham chiếu mới. */
|
|
554
|
+
/**
|
|
555
|
+
* Nhân bản SÂU cả cây — dùng cho `exportLayout()`.
|
|
556
|
+
*
|
|
557
|
+
* 🔴 BỎ `getComponentOutlet` khỏi bản sao: nó là một HÀM, không tuần tự hoá được nên gửi xuống
|
|
558
|
+
* backend là hỏng (`JSON.stringify` nuốt mất, `structuredClone` ném lỗi). Nạp lại thì hàm phân
|
|
559
|
+
* giải chung của cấu hình lo — component suy ra từ `data.type`, không cần lưu.
|
|
560
|
+
*/
|
|
561
|
+
deepCloneTree(node) {
|
|
562
|
+
const copy = { ...node };
|
|
563
|
+
// Cắt hàm phân giải — xem chú thích trên.
|
|
564
|
+
delete copy.getComponentOutlet;
|
|
565
|
+
const { children, data, ...rest } = copy;
|
|
566
|
+
return {
|
|
567
|
+
...rest,
|
|
568
|
+
...(data === undefined ? {} : { data: cloneDeep(data) }),
|
|
569
|
+
...(children ? { children: children.map((child) => this.deepCloneTree(child)) } : {}),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
shallowCloneTree(node) {
|
|
573
|
+
if (!node.children?.length) {
|
|
574
|
+
return { ...node };
|
|
575
|
+
}
|
|
576
|
+
return { ...node, children: node.children.map((child) => this.shallowCloneTree(child)) };
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Gán `layerIndex` khác nhau cho từng khối khi bật chồng khối.
|
|
580
|
+
*
|
|
581
|
+
* 🔴 `allowMultiLayer: true` MỘT MÌNH không cho khối đè nhau. Mã lib so `layerIndex` của hai khối:
|
|
582
|
+
* KHÁC nhau thì cho đè, GIỐNG nhau vẫn báo va chạm. Khối không khai `layerIndex` đều rơi về
|
|
583
|
+
* `defaultLayerIndex` — tức CÙNG lớp, nên kéo vào nhau là bị chặn y như lưới thường (người dùng
|
|
584
|
+
* báo 28/08/2026: "bật chồng khối nhưng khi kéo thì các khối vẫn di chuyển theo").
|
|
585
|
+
*
|
|
586
|
+
* Nơi dùng tự khai `layerIndex` thì giữ nguyên; ai không khai thì thư viện xếp mỗi khối một lớp
|
|
587
|
+
* theo thứ tự, đủ để mọi cặp đều khác lớp.
|
|
588
|
+
*/
|
|
589
|
+
assignOverlapLayers(root) {
|
|
590
|
+
const config = this.config;
|
|
591
|
+
const canOverlap = config?.mode === "free" /* E_gridLayout_mode_string.free */ || (config?.allowOverlap ?? false);
|
|
592
|
+
if (!canOverlap) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
let layer = 0;
|
|
596
|
+
const walk = (node) => {
|
|
597
|
+
for (const child of node.children ?? []) {
|
|
598
|
+
if (child.layerIndex === undefined) {
|
|
599
|
+
layer += 1;
|
|
600
|
+
child.layerIndex = layer;
|
|
601
|
+
}
|
|
602
|
+
walk(child);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
walk(root);
|
|
606
|
+
}
|
|
607
|
+
/** Đưa một khối lên trên tất cả: lớp của nó = lớp lớn nhất đang có + 1. */
|
|
608
|
+
raiseToTopLayer(item) {
|
|
609
|
+
const root = this.rootNode();
|
|
610
|
+
if (!root) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
let topLayer = 0;
|
|
614
|
+
const walk = (node) => {
|
|
615
|
+
for (const child of node.children ?? []) {
|
|
616
|
+
topLayer = Math.max(topLayer, child.layerIndex ?? 0);
|
|
617
|
+
walk(child);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
walk(root);
|
|
621
|
+
// `item` gridster trả về CHÍNH là object node của cây (truyền thẳng qua `[item]`), nên gán vào
|
|
622
|
+
// đây là cây cũng đổi theo.
|
|
623
|
+
const node = item;
|
|
624
|
+
if (node.layerIndex === topLayer) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
node.layerIndex = topLayer + 1;
|
|
628
|
+
}
|
|
629
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
630
|
+
// SỬA CÂY — thư viện đã giữ cây nên bạn KHÔNG phải truyền nó vào.
|
|
631
|
+
// Mọi hàm dưới đây tự vẽ lại; không cần gọi `refresh()`.
|
|
632
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────
|
|
633
|
+
/**
|
|
634
|
+
* Thêm một khối.
|
|
635
|
+
*
|
|
636
|
+
* @param item khối mới — KHÔNG cần `id` (thư viện sinh) và `x`/`y` (tự đặt xuống hàng trống).
|
|
637
|
+
* @param parentId container muốn thêm vào; bỏ trống thì thêm vào mặt phẳng gốc.
|
|
638
|
+
* @returns khối vừa tạo (kèm `id` vừa sinh), hoặc `undefined` khi không tìm thấy container.
|
|
639
|
+
*/
|
|
640
|
+
addBlock(item, parentId) {
|
|
641
|
+
const root = this.rootNode();
|
|
642
|
+
const added = root ? this.treeService.addChild(root, parentId ?? root.id, item) : undefined;
|
|
643
|
+
if (added) {
|
|
644
|
+
this.refresh();
|
|
645
|
+
}
|
|
646
|
+
return added;
|
|
647
|
+
}
|
|
648
|
+
/** Xoá một khối. Xoá container là xoá cả ruột. Trả `false` khi không xoá được (vd khối gốc). */
|
|
649
|
+
removeBlock(id) {
|
|
650
|
+
const root = this.rootNode();
|
|
651
|
+
const done = root ? this.treeService.removeById(root, id) : false;
|
|
652
|
+
if (done) {
|
|
653
|
+
this.refresh();
|
|
654
|
+
}
|
|
655
|
+
return done;
|
|
656
|
+
}
|
|
657
|
+
/** Biến khối đơn thành CONTAINER — nội dung cũ thành khối con đầu tiên, không mất dữ liệu. */
|
|
658
|
+
toContainer(id) {
|
|
659
|
+
const root = this.rootNode();
|
|
660
|
+
const done = root ? this.treeService.toContainer(root, id, this.config?.cols ?? 12) : false;
|
|
661
|
+
if (done) {
|
|
662
|
+
this.refresh();
|
|
663
|
+
}
|
|
664
|
+
return done;
|
|
665
|
+
}
|
|
666
|
+
/** Đưa CONTAINER về khối đơn — khối con được nâng lên cấp trên, không bị xoá. */
|
|
667
|
+
toSingle(id) {
|
|
668
|
+
const root = this.rootNode();
|
|
669
|
+
const done = root ? this.treeService.toSingle(root, id) : false;
|
|
670
|
+
if (done) {
|
|
671
|
+
this.refresh();
|
|
672
|
+
}
|
|
673
|
+
return done;
|
|
674
|
+
}
|
|
675
|
+
/** Dồn khối khít lên trên, lấp hết chỗ trống. Đệ quy vào container. */
|
|
676
|
+
compact() {
|
|
677
|
+
const root = this.rootNode();
|
|
678
|
+
if (!root) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
this.treeService.compact(root, this.config?.cols ?? 12);
|
|
682
|
+
this.refresh();
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Bố cục HIỆN TẠI, đã cắt mọi tham chiếu — gửi thẳng xuống backend được.
|
|
686
|
+
*
|
|
687
|
+
* Dùng khi bấm nút Lưu. Khác `Root()` ở chỗ đây là bản SAO: nơi dùng có sửa gì trên kết quả cũng
|
|
688
|
+
* không đụng tới cây mà thư viện đang vẽ.
|
|
689
|
+
*
|
|
690
|
+
* @returns `undefined` khi mặt phẳng chưa `init()`.
|
|
691
|
+
*/
|
|
692
|
+
exportLayout() {
|
|
693
|
+
const root = this.rootNode();
|
|
694
|
+
return root ? this.deepCloneTree(root) : undefined;
|
|
695
|
+
}
|
|
696
|
+
/** Tìm một khối theo id. */
|
|
697
|
+
findBlock(id) {
|
|
698
|
+
const root = this.rootNode();
|
|
699
|
+
return root ? this.treeService.findById(root, id) : undefined;
|
|
700
|
+
}
|
|
701
|
+
/** Tìm khối cha trực tiếp của một khối. */
|
|
702
|
+
findParentBlock(id) {
|
|
703
|
+
const root = this.rootNode();
|
|
704
|
+
return root ? this.treeService.findParent(root, id) : undefined;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Cổng DUY NHẤT cho canvas của thư viện. Nơi dùng KHÔNG gọi tới đây.
|
|
708
|
+
*
|
|
709
|
+
* Mọi thứ canvas cần đều là `private` để không lọt ra API công khai; gom vào một getter theo đúng
|
|
710
|
+
* khuôn `FunctionsControl` của bộ quy ước, thay vì để rải rác hàm public mà người ngoài tưởng
|
|
711
|
+
* mình phải gọi (người dùng chốt 28/08/2026).
|
|
712
|
+
*/
|
|
713
|
+
get FunctionsControl() {
|
|
714
|
+
return {
|
|
715
|
+
maxContainerDepth: () => this.maxContainerDepth(),
|
|
716
|
+
containerPadding: () => this.containerPadding(),
|
|
717
|
+
gridsterOptions: (depth, cb) => this.buildGridsterOptions(depth, cb),
|
|
718
|
+
attachNode: (node, depth, host) => this.attachNode(node, depth, host),
|
|
719
|
+
detachNode: (nodeId) => this.detachNode(nodeId),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
applyInputs(ref, node, depth) {
|
|
723
|
+
ref.setInput('data', node.data);
|
|
724
|
+
ref.setInput('node', node);
|
|
725
|
+
ref.setInput('isEditMode', this.editMode());
|
|
726
|
+
ref.setInput('depth', depth);
|
|
727
|
+
}
|
|
728
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
729
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutService });
|
|
730
|
+
}
|
|
731
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutService, decorators: [{
|
|
732
|
+
type: Injectable
|
|
733
|
+
}], ctorParameters: () => [] });
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Mặt phẳng kéo thả. Đệ quy: khối có `children` thì ruột nó lại là một mặt phẳng nữa.
|
|
737
|
+
*
|
|
738
|
+
* Nơi dùng KHÔNG dựng component này trực tiếp cho từng khối — chỉ đặt MỘT thẻ ở gốc và truyền
|
|
739
|
+
* `nodeComponentType` qua `LibsUiGridLayoutService.init()`. Thư viện tự gắn component cho mọi khối.
|
|
740
|
+
*/
|
|
741
|
+
class LibsUiGridLayoutCanvasComponent {
|
|
742
|
+
/**
|
|
743
|
+
* @internal Chỉ mặt phẳng LỒNG dùng — thư viện tự truyền khi vẽ ruột container.
|
|
744
|
+
*
|
|
745
|
+
* Nơi dùng KHÔNG truyền: mặt phẳng gốc đọc cây từ service (đã đưa vào qua `init()`), truyền thêm
|
|
746
|
+
* ở đây là khai hai lần cùng một thứ (người dùng chốt 28/08/2026).
|
|
747
|
+
*/
|
|
748
|
+
node = input();
|
|
749
|
+
/** @internal Chỉ mặt phẳng LỒNG dùng — thư viện tự truyền. */
|
|
750
|
+
depth = input(0);
|
|
751
|
+
/**
|
|
752
|
+
* Bố cục vừa đổi do NGƯỜI DÙNG kéo hoặc đổi kích thước khối.
|
|
753
|
+
*
|
|
754
|
+
* Nơi dùng tự gọi `addBlock` · `removeBlock` · `compact` thì KHÔNG bắn — họ vừa gọi thì đã biết,
|
|
755
|
+
* bắn ngược lại là thừa (người dùng chốt 28/08/2026).
|
|
756
|
+
*/
|
|
757
|
+
outChange = output();
|
|
758
|
+
/**
|
|
759
|
+
* Người dùng bấm nút XOÁ trên một khối. Thư viện CHƯA xoá — gọi `event.confirm()` để xoá thật.
|
|
760
|
+
* Cần hỏi trước thì hỏi xong mới gọi; không cho phép thì đừng gọi.
|
|
761
|
+
*/
|
|
762
|
+
outRemoveNode = output();
|
|
763
|
+
/**
|
|
764
|
+
* Người dùng bấm nút THÊM trên một container. Thư viện KHÔNG tự thêm — nó không biết bạn có
|
|
765
|
+
* những loại khối nào. Mở menu chọn loại rồi gọi `event.confirm(item)` với khối bạn muốn thêm.
|
|
766
|
+
*/
|
|
767
|
+
outAddBlockInside = output();
|
|
768
|
+
/** Người dùng bấm nút GỘP/TÁCH. Thư viện chưa làm gì — gọi `event.confirm()` để thực hiện. */
|
|
769
|
+
outToggleType = output();
|
|
770
|
+
gridsterRef = viewChild(GridsterComponent);
|
|
771
|
+
/** mỗi ô một chỗ gắn riêng — cùng thứ tự với `children()` */
|
|
772
|
+
itemHosts = viewChildren('itemHost', { read: ViewContainerRef });
|
|
773
|
+
/** Sáu chấm của tay cầm kéo — chỉ để `@for` dựng đủ 6 thẻ, giá trị không dùng tới. */
|
|
774
|
+
dragBarDots = [0, 1, 2, 3, 4, 5];
|
|
775
|
+
service = inject(LibsUiGridLayoutService);
|
|
776
|
+
/**
|
|
777
|
+
* Node THẬT của mặt phẳng này.
|
|
778
|
+
*
|
|
779
|
+
* 🔴 Mặt phẳng GỐC (depth 0) đọc cây từ SERVICE, không dùng input.
|
|
780
|
+
* Nơi dùng thường sửa cây TẠI CHỖ (`treeService.compact(tree)`) rồi gọi `refresh()`, nên signal
|
|
781
|
+
* `root()` của họ giữ nguyên tham chiếu và input `node` không hề đổi — `@for` không dựng lại
|
|
782
|
+
* `gridster-item`, lưới đứng im dù dữ liệu đã khác (đo 28/08/2026: bấm "Sắp gọn" không tác dụng).
|
|
783
|
+
* `refresh()` của service thay tham chiếu cây, nên đọc từ đó mới thấy thay đổi.
|
|
784
|
+
*
|
|
785
|
+
* Mặt phẳng LỒNG vẫn dùng input: nó nhận node con từ mặt phẳng cha, vốn đã là object mới.
|
|
786
|
+
*/
|
|
787
|
+
currentNode = computed(() => {
|
|
788
|
+
this.service.Version();
|
|
789
|
+
if (this.depth() > 0) {
|
|
790
|
+
return this.node() ?? emptyRootNode();
|
|
791
|
+
}
|
|
792
|
+
return this.service.Root() ?? emptyRootNode();
|
|
793
|
+
});
|
|
794
|
+
children = computed(() => this.currentNode().children ?? []);
|
|
795
|
+
isNestedCanvas = computed(() => this.depth() > 0);
|
|
796
|
+
/** trần lồng container — khối đã chạm trần thì không cho chuyển thành container */
|
|
797
|
+
maxContainerDepth = computed(() => this.service.FunctionsControl.maxContainerDepth());
|
|
798
|
+
/**
|
|
799
|
+
* Lề trong của container, px.
|
|
800
|
+
*
|
|
801
|
+
* 🔴 Đặt trên VỎ container, không đặt vào lưới con. Lưới con giữ nguyên `margin: 2` như cũ; lề
|
|
802
|
+
* này chỉ co ruột lại khỏi biên vỏ, nhờ đó tay cầm đổi kích thước của khối con luôn nằm trong tầm
|
|
803
|
+
* chuột (người dùng chốt 28/08/2026).
|
|
804
|
+
*/
|
|
805
|
+
containerPadding = computed(() => `${this.service.FunctionsControl.containerPadding()}px`);
|
|
806
|
+
/** Dải kéo chỉ hiện ở chế độ sửa — chế độ xem không kéo được nên hiện dải là gây nhầm. */
|
|
807
|
+
isEditMode = computed(() => this.service.IsEditMode());
|
|
808
|
+
gridOptions = computed(() => {
|
|
809
|
+
// Đọc `Version` để `init()` lại (đổi chế độ, bật/tắt chồng khối) sinh ra options MỚI —
|
|
810
|
+
// gridster chỉ áp dụng cấu hình khi tham chiếu `options` đổi.
|
|
811
|
+
this.service.Version();
|
|
812
|
+
return this.service.FunctionsControl.gridsterOptions(this.depth(), () => this.outChange.emit({ root: this.currentNode() }));
|
|
813
|
+
});
|
|
814
|
+
constructor() {
|
|
815
|
+
// Gắn component của nơi dùng vào từng ô, và gắn LẠI mỗi khi cây đổi.
|
|
816
|
+
// Dùng effect vì `itemHosts()` chỉ có giá trị sau khi Angular dựng xong @for.
|
|
817
|
+
effect(() => {
|
|
818
|
+
const hosts = this.itemHosts();
|
|
819
|
+
const items = this.children();
|
|
820
|
+
items.forEach((item, i) => {
|
|
821
|
+
const host = hosts[i];
|
|
822
|
+
if (!host) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
// Khối có con là CONTAINER: ruột nó do mặt phẳng lồng vẽ, không gắn component nội dung.
|
|
826
|
+
if ((item.children?.length ?? 0) > 0) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.service.FunctionsControl.attachNode(item, this.depth() + 1, host);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
ngOnDestroy() {
|
|
834
|
+
for (const item of this.children()) {
|
|
835
|
+
this.service.FunctionsControl.detachNode(item.id);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/** Vẽ lại lưới — gọi khi khung ngoài đổi kích thước hoặc cây vừa đổi diện rộng. */
|
|
839
|
+
get FunctionControl() {
|
|
840
|
+
return {
|
|
841
|
+
veLaiLuoi: () => this.gridsterRef()?.optionsChanged(),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
handlerTrackNode(_index, item) {
|
|
845
|
+
return item.id;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* 🔴 Chặn `mousedown` nổi lên lưới CHA khi thao tác trong lưới con.
|
|
849
|
+
* Không chặn thì cả hai lưới cùng nhận một cú bấm: kéo khối con là nhấc luôn container.
|
|
850
|
+
*/
|
|
851
|
+
/**
|
|
852
|
+
* Chặn `mousedown` của khối trong lưới LỒNG nổi lên lưới cha.
|
|
853
|
+
*
|
|
854
|
+
* Chỉ chặn ở lưới lồng: lưới cấp trang không có cha nào để chặn, và chặn ở đó sẽ nuốt luôn thao
|
|
855
|
+
* tác của chính nó.
|
|
856
|
+
*/
|
|
857
|
+
/**
|
|
858
|
+
* 🔴 Ba handler dưới đây PHẢI `stopPropagation`.
|
|
859
|
+
* Nút nằm trong vùng mang `dragHandleClass`, không chặn thì cú bấm bị gridster hiểu là bắt đầu kéo
|
|
860
|
+
* và `delayStart: 160` nuốt luôn `click` — người dùng thấy như nút chết.
|
|
861
|
+
*/
|
|
862
|
+
handlerRemove(event, node) {
|
|
863
|
+
event.stopPropagation();
|
|
864
|
+
event.preventDefault();
|
|
865
|
+
this.outRemoveNode.emit({ nodeId: node.id, node, confirm: () => this.service.removeBlock(node.id) });
|
|
866
|
+
}
|
|
867
|
+
handlerAddInside(event, node) {
|
|
868
|
+
event.stopPropagation();
|
|
869
|
+
event.preventDefault();
|
|
870
|
+
this.outAddBlockInside.emit({
|
|
871
|
+
nodeId: node.id,
|
|
872
|
+
node,
|
|
873
|
+
confirm: (item) => this.service.addBlock(item, node.id),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
handlerToggleType(event, node) {
|
|
877
|
+
event.stopPropagation();
|
|
878
|
+
event.preventDefault();
|
|
879
|
+
this.outToggleType.emit({
|
|
880
|
+
nodeId: node.id,
|
|
881
|
+
node,
|
|
882
|
+
// Gộp hay tách suy ra từ chính khối: đang là container thì tách, không thì gộp.
|
|
883
|
+
confirm: () => (this.isContainer(node) ? this.service.toSingle(node.id) : this.service.toContainer(node.id)),
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
/** khối này có phải container không (có khối con) */
|
|
887
|
+
isContainer(item) {
|
|
888
|
+
return (item.children?.length ?? 0) > 0;
|
|
889
|
+
}
|
|
890
|
+
/** container đã chạm trần lồng thì không cho thêm khối vào trong nữa */
|
|
891
|
+
canAddInside(item) {
|
|
892
|
+
return this.isContainer(item) && this.depth() < this.maxContainerDepth();
|
|
893
|
+
}
|
|
894
|
+
/** khối đơn ở cấp chưa chạm trần mới được chuyển thành container */
|
|
895
|
+
canToggleType(item) {
|
|
896
|
+
if (this.isContainer(item)) {
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
return this.depth() < this.maxContainerDepth();
|
|
900
|
+
}
|
|
901
|
+
handlerBlockParentDrag(event) {
|
|
902
|
+
if (!this.isNestedCanvas()) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
event.stopPropagation();
|
|
906
|
+
}
|
|
907
|
+
handlerMouseDownTrongLuoi(event) {
|
|
908
|
+
if (this.depth() === 0) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
event.stopPropagation();
|
|
912
|
+
}
|
|
913
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
914
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LibsUiGridLayoutCanvasComponent, isStandalone: true, selector: "libs_ui-services-grid_layout-canvas", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outChange: "outChange", outRemoveNode: "outRemoveNode", outAddBlockInside: "outAddBlockInside", outToggleType: "outToggleType" }, viewQueries: [{ propertyName: "gridsterRef", first: true, predicate: GridsterComponent, descendants: true, isSignal: true }, { propertyName: "itemHosts", predicate: ["itemHost"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- \uD83D\uDD34 `relative` l\u00E0 B\u1EAET BU\u1ED8C, C\u1EA4M b\u1ECF.\n gridster \u0111\u1ED5i v\u1ECB tr\u00ED chu\u1ED9t th\u00E0nh \u00F4 l\u01B0\u1EDBi b\u1EB1ng `e.clientY + (el.scrollTop - el.offsetTop)` \u2014 c\u00F4ng\n th\u1EE9c \u0111\u00F3 ch\u1EC9 \u0111\u00FAng khi `offsetParent` c\u1EE7a <gridster> CH\u00CDNH L\u00C0 kh\u1ED1i cu\u1ED9n n\u00E0y. Thi\u1EBFu `relative` th\u00EC\n `offsetParent` nh\u1EA3y l\u00EAn kh\u1ED1i bao ngo\u00E0i: `offsetTop` tr\u1EA3 16 trong khi l\u1EC7ch th\u1EADt l\u00E0 65 \u2192 gridster\n t\u00EDnh \u00F4 sai 49px, \u00F4 ch\u1EDD \u0111\u1EB7t hi\u1EC7n l\u1EC7ch kh\u1ECFi con tr\u1ECF n\u00EAn k\u00E9o th\u1EA3 kh\u00F4ng \u0111\u1ED5i \u0111\u01B0\u1EE3c v\u1ECB tr\u00ED, v\u00E0 k\u00E9o m\u00E9p\n c\u0169ng t\u00EDnh sai \u0111i\u1EC3m d\u1EEBng. -->\n<div\n class=\"libs-ui-grid-layout-scroll-area relative h-full min-h-0 w-full min-w-0\"\n [class.overflow-y-auto]=\"!isNestedCanvas()\"\n (mousedown)=\"handlerMouseDownTrongLuoi($event)\">\n <gridster class=\"h-full w-full\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id) {\n <!-- \uD83D\uDD34 Ch\u1EB7n `mousedown` t\u1EA1i \u0110\u00C2Y cho l\u01B0\u1EDBi l\u1ED3ng \u2014 C\u1EA4M b\u1ECF v\u00E0 C\u1EA4M chuy\u1EC3n sang ch\u1ED7 kh\u00E1c.\n `ignoreContentClass` m\u1ED9t m\u00ECnh KH\u00D4NG \u0111\u1EE7: `delayStart: 160` l\u00E0m M\u1ED6I l\u01B0\u1EDBi l\u00EAn l\u1ECBch\n `dragStart` b\u1EB1ng `setTimeout` ngay t\u1EA1i `mousedown`, c\u00F2n `stopPropagation` m\u00E0 lib g\u1ECDi b\u00EAn\n trong `dragStart` ch\u1EA1y sau 160ms n\u00EAn qu\u00E1 mu\u1ED9n \u2014 l\u01B0\u1EDBi CHA \u0111\u00E3 k\u1ECBp nh\u1EADn (\u0111o 28/08/2026: k\u00E9o\n d\u1EA3i kh\u1ED1i con, chu\u1ED7i class duy\u1EC7t l\u00EAn \u0110\u00DANG m\u00E0 container v\u1EABn nh\u1EA3y 0,0 \u2192 0,1).\n G\u1EAFn \u1EDF `gridster-item` l\u00E0 \u0111\u00FAng bi\u00EAn: l\u01B0\u1EDBi con nghe tr\u00EAn ch\u00EDnh ph\u1EA7n t\u1EED n\u00E0y n\u00EAn \u0111\u00E3 nh\u1EADn xong,\n l\u01B0\u1EDBi cha \u1EDF ngo\u00E0i b\u1ECB ch\u1EB7n. -->\n <gridster-item\n [item]=\"item\"\n (mousedown)=\"handlerBlockParentDrag($event)\">\n <!-- \uD83D\uDD34 V\u1ECF n\u00E0y mang `dragHandleClass` \u2014 C\u1EA4M b\u1ECF.\n `ignoreContent: true` b\u1EAFt gridster CH\u1EC8 cho k\u00E9o khi ch\u1ED7 b\u1EA5m n\u1EB1m d\u01B0\u1EDBi m\u1ED9t ph\u1EA7n t\u1EED c\u00F3\n `dragHandleClass`. Kh\u00F4ng c\u00F3 v\u1ECF n\u00E0y th\u00EC component c\u1EE7a n\u01A1i d\u00F9ng (th\u01B0 vi\u1EC7n kh\u00F4ng ki\u1EC3m so\u00E1t\n \u0111\u01B0\u1EE3c class c\u1EE7a n\u00F3) s\u1EBD kh\u00F4ng c\u00F3 tay c\u1EA7m n\u00E0o, v\u00E0 KH\u00D4NG kh\u1ED1i n\u00E0o k\u00E9o \u0111\u01B0\u1EE3c \u2014 \u0111o 28/08/2026:\n k\u00E9o 320px, to\u1EA1 \u0111\u1ED9 kh\u1ED1i gi\u1EEF nguy\u00EAn x=3,y=0.\n T\u00EAn class theo c\u1EA5p: l\u01B0\u1EDBi trang v\u00E0 l\u01B0\u1EDBi l\u1ED3ng d\u00F9ng hai t\u00EAn KH\u00C1C nhau, n\u1EBFu kh\u00F4ng l\u01B0\u1EDBi cha\n c\u0169ng nh\u1EADn ra tay c\u1EA7m c\u1EE7a kh\u1ED1i con v\u00E0 nh\u1EA5c lu\u00F4n container. -->\n <!-- \uD83D\uDD34 V\u1ECF KH\u00D4NG mang `dragHandleClass` \u2014 C\u1EA4M th\u00EAm l\u1EA1i.\n K\u00E9o ch\u1EC9 \u0111\u01B0\u1EE3c ph\u00E9p b\u1EAFt \u0111\u1EA7u t\u1EEB D\u1EA2I XANH (ng\u01B0\u1EDDi d\u00F9ng ch\u1ED1t 28/08/2026: \"ph\u1EA3i hover v\u00E0o v\u00F9ng\n m\u00E0u xanh th\u00EC m\u1EDBi k\u00E9o th\u1EA3 \u0111\u01B0\u1EE3c cho \u0111\u1ED3ng nh\u1EA5t\"). Cho c\u1EA3 v\u1ECF l\u00E0m tay c\u1EA7m th\u00EC b\u1EA5m \u0111\u00E2u c\u0169ng\n k\u00E9o, v\u00E0 kh\u1ED1i con n\u1EB1m trong container s\u1EBD nh\u1EA5c lu\u00F4n container v\u00EC l\u01B0\u1EDBi cha duy\u1EC7t l\u00EAn g\u1EB7p\n tay c\u1EA7m c\u1EE7a v\u1ECF container tr\u01B0\u1EDBc.\n V\u1ECF kh\u1ED1i con v\u1EABn gi\u1EEF class CH\u1EB6N \u0111\u1EC3 l\u01B0\u1EDBi cha d\u1EEBng \u0111\u00FAng \u1EDF bi\u00EAn. -->\n <div\n class=\"libs-ui-grid-layout-block-shell group relative h-full w-full min-w-0\"\n [class.libs-ui-grid-layout-block-parent-drag]=\"isNestedCanvas()\">\n <!-- D\u1EA3i k\u00E9o: hi\u1EC7n khi r\u00EA chu\u1ED9t v\u00E0o kh\u1ED1i, ba ch\u1EA5m l\u00E0 d\u1EA5u hi\u1EC7u k\u00E9o quen thu\u1ED9c.\n Ng\u01B0\u1EDDi d\u00F9ng c\u1EA7n m\u1ED9t ch\u1ED7 b\u00E1m R\u00D5 R\u00C0NG thay v\u00EC \u0111o\u00E1n xem b\u1EA5m \u0111\u00E2u th\u00EC k\u00E9o \u0111\u01B0\u1EE3c\n (ch\u1ED1t 28/08/2026, theo \u0111\u00FAng d\u1EA3i \u0111ang d\u00F9ng \u1EDF m\u00E0n Customer 360 c\u1EE7a mobio-web). -->\n @if (isEditMode()) {\n <!-- \uD83D\uDD34 D\u1EA3i PH\u1EA2I mang tay c\u1EA7m \u0110\u00DANG C\u1EA4P c\u1EE7a n\u00F3. D\u1EA3i nh\u1EADn chu\u1ED9t (`pointer-events: auto`),\n n\u00EAn n\u1EBFu kh\u00F4ng mang tay c\u1EA7m th\u00EC `checkDragHandleClass` duy\u1EC7t l\u00EAn g\u1EB7p tay c\u1EA7m c\u1EE7a\n container v\u00E0 nh\u1EA5c container thay v\u00EC kh\u1ED1i con (\u0111o 28/08/2026: k\u00E9o d\u1EA3i kh\u1ED1i con,\n container nh\u1EA3y 0,0 \u2192 0,1 c\u00F2n kh\u1ED1i con \u0111\u1EE9ng im).\n \uD83D\uDD34 D\u1EA3i CH\u1EC8 mang tay c\u1EA7m, C\u1EA4M mang th\u00EAm class ch\u1EB7n: `checkDragHandleClass` x\u00E9t hai\n class \u0111\u00F3 tr\u00EAn C\u00D9NG m\u1ED9t node n\u00EAn \u0111\u1EC3 chung l\u00E0 k\u00E9o h\u1ECFng (\u0111o 28/08/2026: b\u1EA5m d\u1EA3i kh\u1ED1i\n con k\u00E9o 140px, kh\u1ED1i \u0111\u1EE9ng im). Vi\u1EC7c ch\u1EB7n l\u01B0\u1EDBi cha do V\u1ECE KH\u1ED0I lo. -->\n <div\n class=\"libs-ui-grid-layout-drag-bar\"\n [class.libs-ui-grid-layout-drag-bar-child]=\"isNestedCanvas()\"\n [class.libs-ui-grid-layout-drag-handle]=\"!isNestedCanvas()\"\n [class.libs-ui-grid-layout-drag-handle-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-drag-bar-dots\">\n @for (dot of dragBarDots; track dot) {\n <span class=\"libs-ui-grid-layout-drag-bar-dot\"></span>\n }\n </span>\n </div>\n }\n\n <!-- Khung ch\u1EE9a component c\u1EE7a n\u01A1i d\u00F9ng.\n \uD83D\uDD34 PH\u1EA2I c\u00F3 khung th\u1EADt (kh\u00F4ng ph\u1EA3i `ng-container` tr\u1ED1ng): n\u00F3 lo h\u1ED9 n\u01A1i d\u00F9ng ph\u1EA7n l\u1EA5p \u0111\u1EA7y\n \u00F4 v\u00E0 vi\u1EC1n b\u00E1o ch\u1EBF \u0111\u1ED9 s\u1EEDa. Kh\u00F4ng c\u00F3 khung th\u00EC m\u1ED7i component n\u1ED9i dung l\u1EA1i ph\u1EA3i t\u1EF1 vi\u1EBFt\n `:host { width/height: 100% }` + vi\u1EC1n \u0111\u1EE9t \u2014 \u0111\u00F3 l\u00E0 vi\u1EC7c c\u1EE7a th\u01B0 vi\u1EC7n, kh\u00F4ng ph\u1EA3i c\u1EE7a\n ng\u01B0\u1EDDi d\u00F9ng (ng\u01B0\u1EDDi d\u00F9ng ch\u1ED1t 28/08/2026).\n M\u1ED7i kh\u1ED1i m\u1ED9t ViewContainerRef ri\u00EAng n\u00EAn component n\u1EB1m \u0111\u00FAng \u00F4 c\u1EE7a n\u00F3. -->\n <div\n class=\"libs-ui-grid-layout-content-frame\"\n [class.libs-ui-grid-layout-content-frame-edit]=\"isEditMode() && !isContainer(item)\"\n [class.libs-ui-grid-layout-content-frame-empty]=\"isContainer(item)\">\n <ng-container #itemHost />\n </div>\n\n <!-- D\u1EA5u hi\u1EC7u CONTAINER: nh\u00E3n g\u00F3c tr\u00EAn-ph\u1EA3i + vi\u1EC1n \u0111\u1EE9t bao ru\u1ED9t. Kh\u00F4ng c\u00F3 d\u1EA5u hi\u1EC7u th\u00EC\n ng\u01B0\u1EDDi d\u00F9ng kh\u00F4ng bi\u1EBFt kh\u1ED1i n\u00E0o ch\u1EE9a \u0111\u01B0\u1EE3c kh\u1ED1i kh\u00E1c (ch\u1ED1t 28/08/2026, theo \u0111\u00FAng c\u00E1ch\n m\u00E0n Customer 360 c\u1EE7a mobio-web \u0111ang l\u00E0m). -->\n @if (isEditMode() && isContainer(item)) {\n <div class=\"libs-ui-grid-layout-container-label\">Kh\u1ED1i gh\u00E9p</div>\n <div class=\"libs-ui-grid-layout-container-border\"></div>\n }\n\n <!-- Thanh c\u00F4ng c\u1EE5: hi\u1EC7n khi r\u00EA chu\u1ED9t v\u00E0o kh\u1ED1i.\n \uD83D\uDD34 D\u00F9ng `libs_ui-components-buttons-button` \u2014 C\u1EA4M t\u1EF1 d\u1EF1ng th\u1EBB `<button>`.\n Component n\u00E0y \u0111\u00E3 c\u00F3 s\u1EB5n ki\u1EC3u n\u00FAt, c\u1EE1 n\u00FAt v\u00E0 bong b\u00F3ng ch\u00FA th\u00EDch theo \u0111\u00FAng b\u1ED9 giao di\u1EC7n\n c\u1EE7a s\u1EA3n ph\u1EA9m; t\u1EF1 d\u1EF1ng l\u00E0 m\u1ED7i n\u01A1i m\u1ED9t ki\u1EC3u v\u00E0 m\u1EA5t lu\u00F4n tooltip. -->\n @if (isEditMode()) {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n @if (canAddInside(item)) {\n <libs_ui-components-buttons-button\n [type]=\"'button-third'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-add'\"\n [popover]=\"{ config: { content: 'Th\u00EAm kh\u1ED1i v\u00E0o trong', zIndex: 1300 } }\"\n (outClick)=\"handlerAddInside($event, item)\" />\n }\n @if (canToggleType(item)) {\n <libs_ui-components-buttons-button\n [type]=\"'button-third'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"isContainer(item) ? 'libs-ui-icon-split-cell' : 'libs-ui-icon-merge-cell'\"\n [popover]=\"{ config: { content: isContainer(item) ? '\u0110\u01B0a v\u1EC1 kh\u1ED1i \u0111\u01A1n' : 'Chuy\u1EC3n th\u00E0nh kh\u1ED1i gh\u00E9p', zIndex: 1300 } }\"\n (outClick)=\"handlerToggleType($event, item)\" />\n }\n <libs_ui-components-buttons-button\n [type]=\"'button-third-hover-danger'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-remove'\"\n [popover]=\"{ config: { content: isContainer(item) ? 'Xo\u00E1 c\u1EA3 kh\u1ED1i gh\u00E9p' : 'Xo\u00E1 kh\u1ED1i', zIndex: 1300 } }\"\n (outClick)=\"handlerRemove($event, item)\" />\n </div>\n }\n\n @if (item.children && item.children.length > 0) {\n <!-- Kh\u1ED1i c\u00F3 con \u2192 ru\u1ED9t n\u00F3 l\u1EA1i l\u00E0 m\u1ED9t m\u1EB7t ph\u1EB3ng n\u1EEFa. \u0110\u1EC7 quy \u1EDF \u0111\u00E2y, n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i lo.\n L\u1EC1 \u0111\u1EB7t \u1EDF \u0110\u00C2Y (v\u1ECF container), kh\u00F4ng \u0111\u1EB7t v\u00E0o l\u01B0\u1EDBi con \u2014 l\u01B0\u1EDBi con gi\u1EEF nguy\u00EAn khe 2px. -->\n <libs_ui-services-grid_layout-canvas\n class=\"block h-full w-full\"\n [style.padding]=\"containerPadding()\"\n [node]=\"item\"\n [depth]=\"depth() + 1\"\n (outChange)=\"outChange.emit($event)\"\n (outRemoveNode)=\"outRemoveNode.emit($event)\"\n (outAddBlockInside)=\"outAddBlockInside.emit($event)\"\n (outToggleType)=\"outToggleType.emit($event)\" />\n }\n </div>\n </gridster-item>\n }\n </gridster>\n</div>\n", styles: [":host{display:block;box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0}.libs-ui-grid-layout-block-shell{container-type:inline-size;display:block;box-sizing:border-box;height:100%}.libs-ui-grid-layout-drag-bar{position:absolute;top:2px;left:50%;z-index:15;display:flex;align-items:center;justify-content:center;width:64px;height:16px;background:transparent;transform:translate(-50%);opacity:1;transition:opacity .12s ease;pointer-events:auto;cursor:move}.libs-ui-grid-layout-drag-bar-dots{display:grid;grid-template-columns:repeat(3,3px);gap:3px;color:#9ca2ad}.libs-ui-grid-layout-drag-bar-dot{width:3px;height:3px;background:currentColor;border-radius:50%}.libs-ui-grid-layout-drag-bar:hover .libs-ui-grid-layout-drag-bar-dots{color:#3d6ef5}.libs-ui-grid-layout-drag-bar:not(.libs-ui-grid-layout-drag-bar-child){opacity:1}.libs-ui-grid-layout-drag-bar-child{top:auto;bottom:0;background:#d9f2e4;border-radius:4px 4px 0 0}.libs-ui-grid-layout-drag-bar-child .libs-ui-grid-layout-drag-bar-dots{color:#9ca2ad}.libs-ui-grid-layout-drag-bar-child:hover .libs-ui-grid-layout-drag-bar-dots{color:#00a757}.libs-ui-grid-layout-drag-bar-child{opacity:0}.libs-ui-grid-layout-block-shell:hover>.libs-ui-grid-layout-drag-bar-child{opacity:1}.libs-ui-grid-layout-block-shell:has(>.libs-ui-grid-layout-drag-bar:not(.libs-ui-grid-layout-drag-bar-child):hover){outline:2px solid #3d6ef5;outline-offset:-2px;border-radius:8px}.libs-ui-grid-layout-block-shell:has(>.libs-ui-grid-layout-drag-bar-child:hover){outline:2px solid #00a757;outline-offset:-2px;border-radius:8px}.libs-ui-grid-layout-container-label{position:absolute;left:6px;top:2px;display:flex;height:20px;align-items:center;z-index:3;padding:0 6px;color:#3d6ef5;font-weight:500;font-size:10px;line-height:14px;background:#dbe4ff;border-radius:4px;pointer-events:none}.libs-ui-grid-layout-content-frame{box-sizing:border-box;width:100%;height:100%;overflow:hidden;border-radius:6px}.libs-ui-grid-layout-content-frame ::ng-deep>*{display:block;box-sizing:border-box;width:100%;height:100%}.libs-ui-grid-layout-content-frame-empty{height:0}.libs-ui-grid-layout-content-frame-edit{border:1px dashed #c3cede}.libs-ui-grid-layout-container-border{position:absolute;inset:0;border:1px dashed #9db4f0;border-radius:8px;pointer-events:none}.libs-ui-grid-layout-toolbar{position:absolute;top:2px;right:8px;z-index:24;display:flex;gap:2px;align-items:center;height:20px;background:#fff;border-radius:4px;box-shadow:0 2px 8px #0716311f;opacity:0;transition:opacity .12s ease}.libs-ui-grid-layout-toolbar-child{top:50%;right:8px;left:auto;transform:translateY(-50%)}.libs-ui-grid-layout-block-shell:hover>.libs-ui-grid-layout-toolbar{opacity:1}:host ::ng-deep gridster{background:transparent}:host ::ng-deep .gridster-item-resizable-handler.handle-n,:host ::ng-deep .gridster-item-resizable-handler.handle-s{height:16px}:host ::ng-deep .gridster-item-resizable-handler.handle-e,:host ::ng-deep .gridster-item-resizable-handler.handle-w{width:16px}:host ::ng-deep .gridster-item-resizable-handler.handle-n{top:0}:host ::ng-deep .gridster-item-resizable-handler.handle-s{bottom:0}:host ::ng-deep .gridster-item-resizable-handler.handle-e{right:0}:host ::ng-deep .gridster-item-resizable-handler.handle-w{left:0}:host ::ng-deep gridster-item.gridster-item-moving,:host ::ng-deep gridster-item.gridster-item-resizing{z-index:30!important}:host ::ng-deep gridster-item.gridster-item-moving>.libs-ui-grid-layout-block-shell,:host ::ng-deep gridster-item.gridster-item-resizing>.libs-ui-grid-layout-block-shell{outline:2px solid #3d6ef5;outline-offset:-2px;border-radius:8px}:host ::ng-deep gridster gridster gridster-item.gridster-item-moving>.libs-ui-grid-layout-block-shell,:host ::ng-deep gridster gridster gridster-item.gridster-item-resizing>.libs-ui-grid-layout-block-shell{outline-color:#00a757}@container (max-width: 80px){.libs-ui-grid-layout-drag-bar{top:26px}}\n"], dependencies: [{ kind: "component", type: LibsUiGridLayoutCanvasComponent, selector: "libs_ui-services-grid_layout-canvas", inputs: ["node", "depth"], outputs: ["outChange", "outRemoveNode", "outAddBlockInside", "outToggleType"] }, { kind: "ngmodule", type: GridsterModule }, { kind: "component", type: i1.GridsterComponent, selector: "gridster", inputs: ["options"] }, { kind: "component", type: i1.GridsterItemComponent, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "component", type: LibsUiComponentsButtonsButtonComponent, selector: "libs_ui-components-buttons-button", inputs: ["flagMouse", "type", "buttonCustom", "sizeButton", "label", "disable", "isPending", "imageLeft", "classInclude", "classIconLeft", "classIconRight", "classLabel", "iconOnlyType", "popover", "ignoreStopPropagationEvent", "zIndex", "widthLabelPopover", "styleIconLeft", "styleButton", "ignoreFocusWhenInputTab", "ignoreSetClickWhenShowPopover", "ignorePointerEvent", "isActive", "isHandlerEnterDocumentClickButton"], outputs: ["outClick", "outPopoverEvent", "outFunctionsControl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
915
|
+
}
|
|
916
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutCanvasComponent, decorators: [{
|
|
917
|
+
type: Component,
|
|
918
|
+
args: [{ selector: 'libs_ui-services-grid_layout-canvas', standalone: true, imports: [GridsterModule, LibsUiComponentsButtonsButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- \uD83D\uDD34 `relative` l\u00E0 B\u1EAET BU\u1ED8C, C\u1EA4M b\u1ECF.\n gridster \u0111\u1ED5i v\u1ECB tr\u00ED chu\u1ED9t th\u00E0nh \u00F4 l\u01B0\u1EDBi b\u1EB1ng `e.clientY + (el.scrollTop - el.offsetTop)` \u2014 c\u00F4ng\n th\u1EE9c \u0111\u00F3 ch\u1EC9 \u0111\u00FAng khi `offsetParent` c\u1EE7a <gridster> CH\u00CDNH L\u00C0 kh\u1ED1i cu\u1ED9n n\u00E0y. Thi\u1EBFu `relative` th\u00EC\n `offsetParent` nh\u1EA3y l\u00EAn kh\u1ED1i bao ngo\u00E0i: `offsetTop` tr\u1EA3 16 trong khi l\u1EC7ch th\u1EADt l\u00E0 65 \u2192 gridster\n t\u00EDnh \u00F4 sai 49px, \u00F4 ch\u1EDD \u0111\u1EB7t hi\u1EC7n l\u1EC7ch kh\u1ECFi con tr\u1ECF n\u00EAn k\u00E9o th\u1EA3 kh\u00F4ng \u0111\u1ED5i \u0111\u01B0\u1EE3c v\u1ECB tr\u00ED, v\u00E0 k\u00E9o m\u00E9p\n c\u0169ng t\u00EDnh sai \u0111i\u1EC3m d\u1EEBng. -->\n<div\n class=\"libs-ui-grid-layout-scroll-area relative h-full min-h-0 w-full min-w-0\"\n [class.overflow-y-auto]=\"!isNestedCanvas()\"\n (mousedown)=\"handlerMouseDownTrongLuoi($event)\">\n <gridster class=\"h-full w-full\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id) {\n <!-- \uD83D\uDD34 Ch\u1EB7n `mousedown` t\u1EA1i \u0110\u00C2Y cho l\u01B0\u1EDBi l\u1ED3ng \u2014 C\u1EA4M b\u1ECF v\u00E0 C\u1EA4M chuy\u1EC3n sang ch\u1ED7 kh\u00E1c.\n `ignoreContentClass` m\u1ED9t m\u00ECnh KH\u00D4NG \u0111\u1EE7: `delayStart: 160` l\u00E0m M\u1ED6I l\u01B0\u1EDBi l\u00EAn l\u1ECBch\n `dragStart` b\u1EB1ng `setTimeout` ngay t\u1EA1i `mousedown`, c\u00F2n `stopPropagation` m\u00E0 lib g\u1ECDi b\u00EAn\n trong `dragStart` ch\u1EA1y sau 160ms n\u00EAn qu\u00E1 mu\u1ED9n \u2014 l\u01B0\u1EDBi CHA \u0111\u00E3 k\u1ECBp nh\u1EADn (\u0111o 28/08/2026: k\u00E9o\n d\u1EA3i kh\u1ED1i con, chu\u1ED7i class duy\u1EC7t l\u00EAn \u0110\u00DANG m\u00E0 container v\u1EABn nh\u1EA3y 0,0 \u2192 0,1).\n G\u1EAFn \u1EDF `gridster-item` l\u00E0 \u0111\u00FAng bi\u00EAn: l\u01B0\u1EDBi con nghe tr\u00EAn ch\u00EDnh ph\u1EA7n t\u1EED n\u00E0y n\u00EAn \u0111\u00E3 nh\u1EADn xong,\n l\u01B0\u1EDBi cha \u1EDF ngo\u00E0i b\u1ECB ch\u1EB7n. -->\n <gridster-item\n [item]=\"item\"\n (mousedown)=\"handlerBlockParentDrag($event)\">\n <!-- \uD83D\uDD34 V\u1ECF n\u00E0y mang `dragHandleClass` \u2014 C\u1EA4M b\u1ECF.\n `ignoreContent: true` b\u1EAFt gridster CH\u1EC8 cho k\u00E9o khi ch\u1ED7 b\u1EA5m n\u1EB1m d\u01B0\u1EDBi m\u1ED9t ph\u1EA7n t\u1EED c\u00F3\n `dragHandleClass`. Kh\u00F4ng c\u00F3 v\u1ECF n\u00E0y th\u00EC component c\u1EE7a n\u01A1i d\u00F9ng (th\u01B0 vi\u1EC7n kh\u00F4ng ki\u1EC3m so\u00E1t\n \u0111\u01B0\u1EE3c class c\u1EE7a n\u00F3) s\u1EBD kh\u00F4ng c\u00F3 tay c\u1EA7m n\u00E0o, v\u00E0 KH\u00D4NG kh\u1ED1i n\u00E0o k\u00E9o \u0111\u01B0\u1EE3c \u2014 \u0111o 28/08/2026:\n k\u00E9o 320px, to\u1EA1 \u0111\u1ED9 kh\u1ED1i gi\u1EEF nguy\u00EAn x=3,y=0.\n T\u00EAn class theo c\u1EA5p: l\u01B0\u1EDBi trang v\u00E0 l\u01B0\u1EDBi l\u1ED3ng d\u00F9ng hai t\u00EAn KH\u00C1C nhau, n\u1EBFu kh\u00F4ng l\u01B0\u1EDBi cha\n c\u0169ng nh\u1EADn ra tay c\u1EA7m c\u1EE7a kh\u1ED1i con v\u00E0 nh\u1EA5c lu\u00F4n container. -->\n <!-- \uD83D\uDD34 V\u1ECF KH\u00D4NG mang `dragHandleClass` \u2014 C\u1EA4M th\u00EAm l\u1EA1i.\n K\u00E9o ch\u1EC9 \u0111\u01B0\u1EE3c ph\u00E9p b\u1EAFt \u0111\u1EA7u t\u1EEB D\u1EA2I XANH (ng\u01B0\u1EDDi d\u00F9ng ch\u1ED1t 28/08/2026: \"ph\u1EA3i hover v\u00E0o v\u00F9ng\n m\u00E0u xanh th\u00EC m\u1EDBi k\u00E9o th\u1EA3 \u0111\u01B0\u1EE3c cho \u0111\u1ED3ng nh\u1EA5t\"). Cho c\u1EA3 v\u1ECF l\u00E0m tay c\u1EA7m th\u00EC b\u1EA5m \u0111\u00E2u c\u0169ng\n k\u00E9o, v\u00E0 kh\u1ED1i con n\u1EB1m trong container s\u1EBD nh\u1EA5c lu\u00F4n container v\u00EC l\u01B0\u1EDBi cha duy\u1EC7t l\u00EAn g\u1EB7p\n tay c\u1EA7m c\u1EE7a v\u1ECF container tr\u01B0\u1EDBc.\n V\u1ECF kh\u1ED1i con v\u1EABn gi\u1EEF class CH\u1EB6N \u0111\u1EC3 l\u01B0\u1EDBi cha d\u1EEBng \u0111\u00FAng \u1EDF bi\u00EAn. -->\n <div\n class=\"libs-ui-grid-layout-block-shell group relative h-full w-full min-w-0\"\n [class.libs-ui-grid-layout-block-parent-drag]=\"isNestedCanvas()\">\n <!-- D\u1EA3i k\u00E9o: hi\u1EC7n khi r\u00EA chu\u1ED9t v\u00E0o kh\u1ED1i, ba ch\u1EA5m l\u00E0 d\u1EA5u hi\u1EC7u k\u00E9o quen thu\u1ED9c.\n Ng\u01B0\u1EDDi d\u00F9ng c\u1EA7n m\u1ED9t ch\u1ED7 b\u00E1m R\u00D5 R\u00C0NG thay v\u00EC \u0111o\u00E1n xem b\u1EA5m \u0111\u00E2u th\u00EC k\u00E9o \u0111\u01B0\u1EE3c\n (ch\u1ED1t 28/08/2026, theo \u0111\u00FAng d\u1EA3i \u0111ang d\u00F9ng \u1EDF m\u00E0n Customer 360 c\u1EE7a mobio-web). -->\n @if (isEditMode()) {\n <!-- \uD83D\uDD34 D\u1EA3i PH\u1EA2I mang tay c\u1EA7m \u0110\u00DANG C\u1EA4P c\u1EE7a n\u00F3. D\u1EA3i nh\u1EADn chu\u1ED9t (`pointer-events: auto`),\n n\u00EAn n\u1EBFu kh\u00F4ng mang tay c\u1EA7m th\u00EC `checkDragHandleClass` duy\u1EC7t l\u00EAn g\u1EB7p tay c\u1EA7m c\u1EE7a\n container v\u00E0 nh\u1EA5c container thay v\u00EC kh\u1ED1i con (\u0111o 28/08/2026: k\u00E9o d\u1EA3i kh\u1ED1i con,\n container nh\u1EA3y 0,0 \u2192 0,1 c\u00F2n kh\u1ED1i con \u0111\u1EE9ng im).\n \uD83D\uDD34 D\u1EA3i CH\u1EC8 mang tay c\u1EA7m, C\u1EA4M mang th\u00EAm class ch\u1EB7n: `checkDragHandleClass` x\u00E9t hai\n class \u0111\u00F3 tr\u00EAn C\u00D9NG m\u1ED9t node n\u00EAn \u0111\u1EC3 chung l\u00E0 k\u00E9o h\u1ECFng (\u0111o 28/08/2026: b\u1EA5m d\u1EA3i kh\u1ED1i\n con k\u00E9o 140px, kh\u1ED1i \u0111\u1EE9ng im). Vi\u1EC7c ch\u1EB7n l\u01B0\u1EDBi cha do V\u1ECE KH\u1ED0I lo. -->\n <div\n class=\"libs-ui-grid-layout-drag-bar\"\n [class.libs-ui-grid-layout-drag-bar-child]=\"isNestedCanvas()\"\n [class.libs-ui-grid-layout-drag-handle]=\"!isNestedCanvas()\"\n [class.libs-ui-grid-layout-drag-handle-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-drag-bar-dots\">\n @for (dot of dragBarDots; track dot) {\n <span class=\"libs-ui-grid-layout-drag-bar-dot\"></span>\n }\n </span>\n </div>\n }\n\n <!-- Khung ch\u1EE9a component c\u1EE7a n\u01A1i d\u00F9ng.\n \uD83D\uDD34 PH\u1EA2I c\u00F3 khung th\u1EADt (kh\u00F4ng ph\u1EA3i `ng-container` tr\u1ED1ng): n\u00F3 lo h\u1ED9 n\u01A1i d\u00F9ng ph\u1EA7n l\u1EA5p \u0111\u1EA7y\n \u00F4 v\u00E0 vi\u1EC1n b\u00E1o ch\u1EBF \u0111\u1ED9 s\u1EEDa. Kh\u00F4ng c\u00F3 khung th\u00EC m\u1ED7i component n\u1ED9i dung l\u1EA1i ph\u1EA3i t\u1EF1 vi\u1EBFt\n `:host { width/height: 100% }` + vi\u1EC1n \u0111\u1EE9t \u2014 \u0111\u00F3 l\u00E0 vi\u1EC7c c\u1EE7a th\u01B0 vi\u1EC7n, kh\u00F4ng ph\u1EA3i c\u1EE7a\n ng\u01B0\u1EDDi d\u00F9ng (ng\u01B0\u1EDDi d\u00F9ng ch\u1ED1t 28/08/2026).\n M\u1ED7i kh\u1ED1i m\u1ED9t ViewContainerRef ri\u00EAng n\u00EAn component n\u1EB1m \u0111\u00FAng \u00F4 c\u1EE7a n\u00F3. -->\n <div\n class=\"libs-ui-grid-layout-content-frame\"\n [class.libs-ui-grid-layout-content-frame-edit]=\"isEditMode() && !isContainer(item)\"\n [class.libs-ui-grid-layout-content-frame-empty]=\"isContainer(item)\">\n <ng-container #itemHost />\n </div>\n\n <!-- D\u1EA5u hi\u1EC7u CONTAINER: nh\u00E3n g\u00F3c tr\u00EAn-ph\u1EA3i + vi\u1EC1n \u0111\u1EE9t bao ru\u1ED9t. Kh\u00F4ng c\u00F3 d\u1EA5u hi\u1EC7u th\u00EC\n ng\u01B0\u1EDDi d\u00F9ng kh\u00F4ng bi\u1EBFt kh\u1ED1i n\u00E0o ch\u1EE9a \u0111\u01B0\u1EE3c kh\u1ED1i kh\u00E1c (ch\u1ED1t 28/08/2026, theo \u0111\u00FAng c\u00E1ch\n m\u00E0n Customer 360 c\u1EE7a mobio-web \u0111ang l\u00E0m). -->\n @if (isEditMode() && isContainer(item)) {\n <div class=\"libs-ui-grid-layout-container-label\">Kh\u1ED1i gh\u00E9p</div>\n <div class=\"libs-ui-grid-layout-container-border\"></div>\n }\n\n <!-- Thanh c\u00F4ng c\u1EE5: hi\u1EC7n khi r\u00EA chu\u1ED9t v\u00E0o kh\u1ED1i.\n \uD83D\uDD34 D\u00F9ng `libs_ui-components-buttons-button` \u2014 C\u1EA4M t\u1EF1 d\u1EF1ng th\u1EBB `<button>`.\n Component n\u00E0y \u0111\u00E3 c\u00F3 s\u1EB5n ki\u1EC3u n\u00FAt, c\u1EE1 n\u00FAt v\u00E0 bong b\u00F3ng ch\u00FA th\u00EDch theo \u0111\u00FAng b\u1ED9 giao di\u1EC7n\n c\u1EE7a s\u1EA3n ph\u1EA9m; t\u1EF1 d\u1EF1ng l\u00E0 m\u1ED7i n\u01A1i m\u1ED9t ki\u1EC3u v\u00E0 m\u1EA5t lu\u00F4n tooltip. -->\n @if (isEditMode()) {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n @if (canAddInside(item)) {\n <libs_ui-components-buttons-button\n [type]=\"'button-third'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-add'\"\n [popover]=\"{ config: { content: 'Th\u00EAm kh\u1ED1i v\u00E0o trong', zIndex: 1300 } }\"\n (outClick)=\"handlerAddInside($event, item)\" />\n }\n @if (canToggleType(item)) {\n <libs_ui-components-buttons-button\n [type]=\"'button-third'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"isContainer(item) ? 'libs-ui-icon-split-cell' : 'libs-ui-icon-merge-cell'\"\n [popover]=\"{ config: { content: isContainer(item) ? '\u0110\u01B0a v\u1EC1 kh\u1ED1i \u0111\u01A1n' : 'Chuy\u1EC3n th\u00E0nh kh\u1ED1i gh\u00E9p', zIndex: 1300 } }\"\n (outClick)=\"handlerToggleType($event, item)\" />\n }\n <libs_ui-components-buttons-button\n [type]=\"'button-third-hover-danger'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-remove'\"\n [popover]=\"{ config: { content: isContainer(item) ? 'Xo\u00E1 c\u1EA3 kh\u1ED1i gh\u00E9p' : 'Xo\u00E1 kh\u1ED1i', zIndex: 1300 } }\"\n (outClick)=\"handlerRemove($event, item)\" />\n </div>\n }\n\n @if (item.children && item.children.length > 0) {\n <!-- Kh\u1ED1i c\u00F3 con \u2192 ru\u1ED9t n\u00F3 l\u1EA1i l\u00E0 m\u1ED9t m\u1EB7t ph\u1EB3ng n\u1EEFa. \u0110\u1EC7 quy \u1EDF \u0111\u00E2y, n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i lo.\n L\u1EC1 \u0111\u1EB7t \u1EDF \u0110\u00C2Y (v\u1ECF container), kh\u00F4ng \u0111\u1EB7t v\u00E0o l\u01B0\u1EDBi con \u2014 l\u01B0\u1EDBi con gi\u1EEF nguy\u00EAn khe 2px. -->\n <libs_ui-services-grid_layout-canvas\n class=\"block h-full w-full\"\n [style.padding]=\"containerPadding()\"\n [node]=\"item\"\n [depth]=\"depth() + 1\"\n (outChange)=\"outChange.emit($event)\"\n (outRemoveNode)=\"outRemoveNode.emit($event)\"\n (outAddBlockInside)=\"outAddBlockInside.emit($event)\"\n (outToggleType)=\"outToggleType.emit($event)\" />\n }\n </div>\n </gridster-item>\n }\n </gridster>\n</div>\n", styles: [":host{display:block;box-sizing:border-box;width:100%;height:100%;min-width:0;min-height:0}.libs-ui-grid-layout-block-shell{container-type:inline-size;display:block;box-sizing:border-box;height:100%}.libs-ui-grid-layout-drag-bar{position:absolute;top:2px;left:50%;z-index:15;display:flex;align-items:center;justify-content:center;width:64px;height:16px;background:transparent;transform:translate(-50%);opacity:1;transition:opacity .12s ease;pointer-events:auto;cursor:move}.libs-ui-grid-layout-drag-bar-dots{display:grid;grid-template-columns:repeat(3,3px);gap:3px;color:#9ca2ad}.libs-ui-grid-layout-drag-bar-dot{width:3px;height:3px;background:currentColor;border-radius:50%}.libs-ui-grid-layout-drag-bar:hover .libs-ui-grid-layout-drag-bar-dots{color:#3d6ef5}.libs-ui-grid-layout-drag-bar:not(.libs-ui-grid-layout-drag-bar-child){opacity:1}.libs-ui-grid-layout-drag-bar-child{top:auto;bottom:0;background:#d9f2e4;border-radius:4px 4px 0 0}.libs-ui-grid-layout-drag-bar-child .libs-ui-grid-layout-drag-bar-dots{color:#9ca2ad}.libs-ui-grid-layout-drag-bar-child:hover .libs-ui-grid-layout-drag-bar-dots{color:#00a757}.libs-ui-grid-layout-drag-bar-child{opacity:0}.libs-ui-grid-layout-block-shell:hover>.libs-ui-grid-layout-drag-bar-child{opacity:1}.libs-ui-grid-layout-block-shell:has(>.libs-ui-grid-layout-drag-bar:not(.libs-ui-grid-layout-drag-bar-child):hover){outline:2px solid #3d6ef5;outline-offset:-2px;border-radius:8px}.libs-ui-grid-layout-block-shell:has(>.libs-ui-grid-layout-drag-bar-child:hover){outline:2px solid #00a757;outline-offset:-2px;border-radius:8px}.libs-ui-grid-layout-container-label{position:absolute;left:6px;top:2px;display:flex;height:20px;align-items:center;z-index:3;padding:0 6px;color:#3d6ef5;font-weight:500;font-size:10px;line-height:14px;background:#dbe4ff;border-radius:4px;pointer-events:none}.libs-ui-grid-layout-content-frame{box-sizing:border-box;width:100%;height:100%;overflow:hidden;border-radius:6px}.libs-ui-grid-layout-content-frame ::ng-deep>*{display:block;box-sizing:border-box;width:100%;height:100%}.libs-ui-grid-layout-content-frame-empty{height:0}.libs-ui-grid-layout-content-frame-edit{border:1px dashed #c3cede}.libs-ui-grid-layout-container-border{position:absolute;inset:0;border:1px dashed #9db4f0;border-radius:8px;pointer-events:none}.libs-ui-grid-layout-toolbar{position:absolute;top:2px;right:8px;z-index:24;display:flex;gap:2px;align-items:center;height:20px;background:#fff;border-radius:4px;box-shadow:0 2px 8px #0716311f;opacity:0;transition:opacity .12s ease}.libs-ui-grid-layout-toolbar-child{top:50%;right:8px;left:auto;transform:translateY(-50%)}.libs-ui-grid-layout-block-shell:hover>.libs-ui-grid-layout-toolbar{opacity:1}:host ::ng-deep gridster{background:transparent}:host ::ng-deep .gridster-item-resizable-handler.handle-n,:host ::ng-deep .gridster-item-resizable-handler.handle-s{height:16px}:host ::ng-deep .gridster-item-resizable-handler.handle-e,:host ::ng-deep .gridster-item-resizable-handler.handle-w{width:16px}:host ::ng-deep .gridster-item-resizable-handler.handle-n{top:0}:host ::ng-deep .gridster-item-resizable-handler.handle-s{bottom:0}:host ::ng-deep .gridster-item-resizable-handler.handle-e{right:0}:host ::ng-deep .gridster-item-resizable-handler.handle-w{left:0}:host ::ng-deep gridster-item.gridster-item-moving,:host ::ng-deep gridster-item.gridster-item-resizing{z-index:30!important}:host ::ng-deep gridster-item.gridster-item-moving>.libs-ui-grid-layout-block-shell,:host ::ng-deep gridster-item.gridster-item-resizing>.libs-ui-grid-layout-block-shell{outline:2px solid #3d6ef5;outline-offset:-2px;border-radius:8px}:host ::ng-deep gridster gridster gridster-item.gridster-item-moving>.libs-ui-grid-layout-block-shell,:host ::ng-deep gridster gridster gridster-item.gridster-item-resizing>.libs-ui-grid-layout-block-shell{outline-color:#00a757}@container (max-width: 80px){.libs-ui-grid-layout-drag-bar{top:26px}}\n"] }]
|
|
919
|
+
}], ctorParameters: () => [] });
|
|
920
|
+
|
|
921
|
+
// Bề mặt CÔNG KHAI — chỉ những gì nơi dùng thật sự cần.
|
|
922
|
+
//
|
|
923
|
+
// `LibsUiGridLayoutTreeService` CỐ Ý không xuất: mọi thao tác trên cây đã có mặt trên
|
|
924
|
+
// `LibsUiGridLayoutService` với chữ ký gọn hơn (không phải truyền `root` — thư viện đang giữ nó).
|
|
925
|
+
// Cấu hình gridster cũng không xuất: nó đã được vá theo hàng chục lỗi đo trên sản phẩm thật, để nơi
|
|
926
|
+
// dùng sửa lại là mở đường cho chính những lỗi đó quay về.
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Generated bundle index. Do not edit.
|
|
930
|
+
*/
|
|
931
|
+
|
|
932
|
+
export { LibsUiGridLayoutCanvasComponent, LibsUiGridLayoutService };
|
|
933
|
+
//# sourceMappingURL=libs-ui-services-grid-layout.mjs.map
|