@libs-ui/services-grid-layout 0.2.357-23 → 0.2.357-25

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.
@@ -262,7 +262,10 @@ const findHoveredGroup = (elements, canvasContainer, root) => {
262
262
  /**
263
263
  * Calculates snapped spatial coordinates and pixel dimensions for drop ghost preview box.
264
264
  */
265
- const calculateDropGhost = (event, canvasContainer, root, payload, totalCols = 24) => {
265
+ const calculateDropGhost = (event, canvasContainer, root, payload, totalCols = 24,
266
+ // 🔴 Chiều cao một hàng lưới, tính bằng pixel. Trước đây đóng cứng 68 nên khung xem trước vẽ sai cỡ
267
+ // ở mặt phẳng có hàng thấp, nơi dùng phải ẩn khung của thư viện rồi tự vẽ lại.
268
+ rowStep = 68) => {
266
269
  if (typeof document === 'undefined' || !canvasContainer) {
267
270
  return undefined;
268
271
  }
@@ -358,7 +361,6 @@ const calculateDropGhost = (event, canvasContainer, root, payload, totalCols = 2
358
361
  };
359
362
  }
360
363
  const margin = 12;
361
- const rowStep = 68;
362
364
  const gridsterWidth = unscaledWidth || gridsterEl.clientWidth || 1200;
363
365
  const colStep = Math.max(20, (gridsterWidth - margin) / totalCols);
364
366
  const gridsterStageLeft = (gridsterRect.left - stageRect.left) / zoom;
@@ -505,7 +507,20 @@ class LibsUiGridLayoutTreeService {
505
507
  // 🔴 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 —
506
508
  // 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
507
509
  // im lặng (người dùng chốt 28/08/2026).
508
- const node = { ...item, id: item.id ?? uuid(), x: 0, y: this.nextFreeRow(parent.children) };
510
+ // 🔴 Mặc định VẪN tự xếp xuống hàng trống kế tiếp giữ nguyên hành vi cũ.
511
+ //
512
+ // Chỉ khi nơi dùng bật `keepPosition` mới giữ `x`/`y` truyền vào. CẤM suy ra ý định từ chính giá
513
+ // trị toạ độ (kiểu `item.y ?? ...`): `y: 0` là giá trị hợp lệ và cũng là thứ mọi hàm dựng node
514
+ // điền sẵn, nên không tài nào phân biệt "người ta muốn đặt ở hàng 0" với "người ta không quan
515
+ // tâm" — đoán sai là khối mới ĐÈ lên khối đang có.
516
+ const node = {
517
+ ...item,
518
+ id: item.id ?? uuid(),
519
+ x: item.keepPosition ? (item.x ?? 0) : 0,
520
+ y: item.keepPosition ? (item.y ?? 0) : this.nextFreeRow(parent.children),
521
+ };
522
+ // Cờ điều khiển, không phải dữ liệu của khối — không để nó nằm lại trong cây.
523
+ delete node.keepPosition;
509
524
  parent.children.push(node);
510
525
  // Trả về khối vừa tạo để nơi dùng biết `id` thư viện vừa sinh.
511
526
  return node;
@@ -901,15 +916,19 @@ class LibsUiGridLayoutService {
901
916
  *
902
917
  * @param host chỗ gắn RIÊNG của ô đó — mỗi khối một `ViewContainerRef`, dùng chung một cái thì
903
918
  * mọi component bị dồn hết vào một ô.
919
+ * @param childCanvas khuôn mặt phẳng LỒNG của khối, chỉ có với khối container tự khai
920
+ * `getComponentOutlet`. Component vỏ nhận nó qua input `childCanvas` và tự đặt vào giữa tiêu đề
921
+ * với phần thân của mình. Không truyền xuống thì vỏ chiếm trọn chiều cao khối và đẩy toàn bộ thẻ
922
+ * con ra ngoài vùng nhìn thấy.
904
923
  */
905
- attachNode(node, depth, host) {
924
+ attachNode(node, depth, host, childCanvas) {
906
925
  const config = this.config;
907
926
  if (!config) {
908
927
  return;
909
928
  }
910
929
  const existing = this.componentRefs.get(node.id);
911
930
  if (existing) {
912
- this.applyInputs(existing, node, depth);
931
+ this.applyInputs(existing, node, depth, childCanvas);
913
932
  return;
914
933
  }
915
934
  // 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
@@ -929,7 +948,7 @@ class LibsUiGridLayoutService {
929
948
  return;
930
949
  }
931
950
  const ref = host.createComponent(componentType);
932
- this.applyInputs(ref, node, depth);
951
+ this.applyInputs(ref, node, depth, childCanvas);
933
952
  this.componentRefs.set(node.id, ref);
934
953
  });
935
954
  this.pendingLoads.set(node.id, sub);
@@ -1132,6 +1151,22 @@ class LibsUiGridLayoutService {
1132
1151
  get totalColumns() {
1133
1152
  return this.config?.cols ?? gridLayoutRootConfig().maxCols ?? 12;
1134
1153
  }
1154
+ /**
1155
+ * Chiều cao một hàng lưới (pixel) dùng để tính khung xem trước lúc kéo thả.
1156
+ *
1157
+ * 🔴 Phải đọc từ `config.rowHeight` của nơi dùng. Trước đây `calculateDropGhost` đóng cứng 68, nên
1158
+ * mặt phẳng khai hàng thấp thì khung xem trước cao gấp nhiều lần khối thật — nơi dùng phải ẩn khung
1159
+ * của thư viện đi rồi tự vẽ lại.
1160
+ */
1161
+ get rowStepPx() {
1162
+ return this.config?.rowHeight ?? gridLayoutRootConfig().fixedRowHeight ?? 68;
1163
+ }
1164
+ /**
1165
+ * Khe NHÌN THẤY giữa hai khối cấp gốc (px). `0` = không áp, giữ đúng hành vi cũ.
1166
+ */
1167
+ get visualGapPx() {
1168
+ return this.config?.visualGap ?? 0;
1169
+ }
1135
1170
  /**
1136
1171
  * Đổi số cột / số hàng của một khối rồi vẽ lại.
1137
1172
  *
@@ -1309,15 +1344,52 @@ class LibsUiGridLayoutService {
1309
1344
  resetZoom: (key) => this.resetZoom(key),
1310
1345
  fitToScreen: (cW, sW, pad, key) => this.fitToScreen(cW, sW, pad, key),
1311
1346
  gridsterOptions: (depth, cb) => this.buildGridsterOptions(depth, cb),
1312
- attachNode: (node, depth, host) => this.attachNode(node, depth, host),
1347
+ attachNode: (node, depth, host, childCanvas) => this.attachNode(node, depth, host, childCanvas),
1313
1348
  detachNode: (nodeId) => this.detachNode(nodeId),
1349
+ getNodeElement: (nodeId, scope) => this.getNodeElement(nodeId, scope),
1350
+ getNodeCanvasElement: (nodeId, scope) => this.getNodeCanvasElement(nodeId, scope),
1314
1351
  };
1315
1352
  }
1316
- applyInputs(ref, node, depth) {
1353
+ /**
1354
+ * Trả về vỏ DOM của một khối theo `id`.
1355
+ *
1356
+ * 🔴 Nơi dùng cần chạm vào DOM của khối (đo kích thước, gắn lớp phủ riêng) BẮT BUỘC đi qua đây.
1357
+ * Trước khi có hàm này, nơi dùng phải tự viết `querySelector('.libs-ui-grid-layout-block-shell[...]')`
1358
+ * — tức bám vào tên class NỘI BỘ của thư viện: đổi tên class là màn bên kia vỡ IM LẶNG, biên dịch
1359
+ * vẫn xanh vì chuỗi selector không ai kiểm.
1360
+ *
1361
+ * `scope` giới hạn phạm vi tìm (mặc định cả tài liệu) — cần khi nhiều mặt phẳng cùng nằm trên màn.
1362
+ */
1363
+ getNodeElement(nodeId, scope) {
1364
+ if (typeof document === 'undefined') {
1365
+ return undefined;
1366
+ }
1367
+ const root = scope ?? document;
1368
+ return root.querySelector(`.libs-ui-grid-layout-block-shell[data-node-id="${nodeId}"]`) ?? undefined;
1369
+ }
1370
+ /**
1371
+ * Trả về mặt phẳng LỒNG nằm trong một khối container (khối có con), nếu có.
1372
+ *
1373
+ * Tách riêng khỏi `getNodeElement` vì nơi dùng thường cần chèn phần tử của mình vào TRƯỚC mặt phẳng
1374
+ * lồng này (ví dụ hàng tiêu đề của khối ghép), nên phải cầm đúng nó chứ không phải cả vỏ.
1375
+ */
1376
+ getNodeCanvasElement(nodeId, scope) {
1377
+ const shell = this.getNodeElement(nodeId, scope);
1378
+ if (!shell) {
1379
+ return undefined;
1380
+ }
1381
+ return shell.querySelector(':scope > libs_ui-services-grid_layout-canvas') ?? undefined;
1382
+ }
1383
+ applyInputs(ref, node, depth, childCanvas) {
1317
1384
  ref.setInput('data', node.data);
1318
1385
  ref.setInput('node', node);
1319
1386
  ref.setInput('isEditMode', this.editMode());
1320
1387
  ref.setInput('depth', depth);
1388
+ // 🔴 CHỈ đặt khi có khuôn: `setInput` với một input component không khai là lỗi lúc chạy, mà
1389
+ // component nội dung của khối LÁ thì không cần biết đến mặt phẳng lồng.
1390
+ if (childCanvas) {
1391
+ ref.setInput('childCanvas', childCanvas);
1392
+ }
1321
1393
  }
1322
1394
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1323
1395
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutService });
@@ -1417,6 +1489,15 @@ class LibsUiGridLayoutCanvasComponent {
1417
1489
  zoomDockRef = viewChild('zoomDock');
1418
1490
  /** mỗi ô một chỗ gắn riêng — cùng thứ tự với `children()` */
1419
1491
  itemHosts = viewChildren('itemHost', { read: ViewContainerRef });
1492
+ /**
1493
+ * Khuôn mặt phẳng LỒNG của từng khối container, để đưa vào component vỏ khi khối đó tự khai
1494
+ * `getComponentOutlet`.
1495
+ *
1496
+ * 🔴 Danh sách này KHÔNG cùng thứ tự với `children()`: `ng-template #childCanvasTpl` chỉ tồn tại
1497
+ * cho khối CÓ CON. Ghép theo index là gán nhầm khuôn của khối này cho khối khác — phải lọc lại
1498
+ * đúng cùng điều kiện mà template dùng (xem `childCanvasTemplateOf`).
1499
+ */
1500
+ childCanvasTemplates = viewChildren('childCanvasTpl');
1420
1501
  /** Khung xem trước vị trí thả thẻ từ bên ngoài (snapped ghost preview) */
1421
1502
  dropGhost = signal(undefined);
1422
1503
  activeHoveredEl;
@@ -1478,6 +1559,11 @@ class LibsUiGridLayoutCanvasComponent {
1478
1559
  toggleType: () => this.emitToggleType(item),
1479
1560
  })));
1480
1561
  isNestedCanvas = computed(() => this.depth() > 0);
1562
+ /**
1563
+ * Khe nhìn thấy giữa các khối CẤP GỐC. Mặt phẳng lồng trả 0 — khối con là các dòng của cùng một
1564
+ * thẻ, tách chúng ra làm chúng trông như thẻ rời.
1565
+ */
1566
+ visualGap = computed(() => (this.depth() === 0 ? this.service.visualGapPx : 0));
1481
1567
  /** trần lồng container — khối đã chạm trần thì không cho chuyển thành container */
1482
1568
  maxContainerDepth = computed(() => this.service.FunctionsControl.maxContainerDepth());
1483
1569
  /**
@@ -1502,16 +1588,26 @@ class LibsUiGridLayoutCanvasComponent {
1502
1588
  effect(() => {
1503
1589
  const hosts = this.itemHosts();
1504
1590
  const items = this.children();
1591
+ const canvasTemplates = this.childCanvasTemplates();
1505
1592
  items.forEach((item, i) => {
1506
1593
  const host = hosts[i];
1507
1594
  if (!host) {
1508
1595
  return;
1509
1596
  }
1510
- // 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.
1511
- if ((item.children?.length ?? 0) > 0) {
1597
+ // Khối có con là CONTAINER: RUỘT nó do mặt phẳng lồng vẽ.
1598
+ //
1599
+ // 🔴 Nhưng container VẪN được gắn component nếu nơi dùng chủ động khai `getComponentOutlet`
1600
+ // cho chính khối đó — đó là VỎ của khối ghép: hàng tiêu đề, nút thu gọn, nền. Vỏ KHÔNG thay
1601
+ // thế mặt phẳng lồng; nó NHẬN mặt phẳng đó vào trong qua input `childCanvas` và tự đặt vào
1602
+ // đúng chỗ giữa tiêu đề và phần thân của mình.
1603
+ //
1604
+ // Container KHÔNG khai outlet riêng thì vẫn bỏ qua y như cũ — `config.getComponentOutlet`
1605
+ // dùng chung là để vẽ NỘI DUNG khối lá, đem áp cho container sẽ đè mất mặt phẳng lồng.
1606
+ const isContainerNode = (item.children?.length ?? 0) > 0;
1607
+ if (isContainerNode && !item.getComponentOutlet) {
1512
1608
  return;
1513
1609
  }
1514
- this.service.FunctionsControl.attachNode(item, this.depth() + 1, host);
1610
+ this.service.FunctionsControl.attachNode(item, this.depth() + 1, host, isContainerNode ? this.childCanvasTemplateOf(item, canvasTemplates) : undefined);
1515
1611
  });
1516
1612
  });
1517
1613
  // Đồng bộ nếu model zoom bên ngoài thay đổi
@@ -1710,6 +1806,20 @@ class LibsUiGridLayoutCanvasComponent {
1710
1806
  isContainer(item) {
1711
1807
  return (item.children?.length ?? 0) > 0;
1712
1808
  }
1809
+ /**
1810
+ * Lấy đúng khuôn mặt phẳng lồng của MỘT khối.
1811
+ *
1812
+ * 🔴 `viewChildren('childCanvasTpl')` chỉ chứa khuôn của khối CÓ CON, còn `children()` chứa mọi
1813
+ * khối — hai danh sách lệch nhau. Phải lọc `children()` theo cùng điều kiện mà template dùng rồi
1814
+ * mới tra theo vị trí trong danh sách đã lọc; ghép thẳng theo index của `children()` là gán khuôn
1815
+ * của khối này cho khối khác, và thẻ con hiện sai chỗ.
1816
+ */
1817
+ childCanvasTemplateOf(item, templates) {
1818
+ const index = this.children()
1819
+ .filter((node) => (node.children?.length ?? 0) > 0)
1820
+ .findIndex((node) => node.id === item.id);
1821
+ return index >= 0 ? templates[index] : undefined;
1822
+ }
1713
1823
  /** container đã chạm trần lồng thì không cho thêm khối vào trong nữa */
1714
1824
  canAddInside(item) {
1715
1825
  return this.isContainer(item) && this.depth() < this.maxContainerDepth();
@@ -1735,8 +1845,11 @@ class LibsUiGridLayoutCanvasComponent {
1735
1845
  }
1736
1846
  handlerSelectNode(node, event) {
1737
1847
  if (event) {
1848
+ // 🔴 CHỈ bỏ qua khi đích thực sự là nút bấm. Trước đây bỏ qua cả vùng
1849
+ // `.libs-ui-grid-layout-toolbar`: thanh này phủ gần kín khối nhỏ nên bấm vào khối không bao giờ
1850
+ // chọn được, nơi dùng phải tắt `pointer-events` của cả thanh để cứu.
1738
1851
  const target = event.target;
1739
- if (target.closest('.libs-ui-grid-layout-toolbar') || target.closest('button')) {
1852
+ if (target.closest('button') || target.closest('[data-grid-layout-action]')) {
1740
1853
  return;
1741
1854
  }
1742
1855
  }
@@ -1773,7 +1886,7 @@ class LibsUiGridLayoutCanvasComponent {
1773
1886
  }
1774
1887
  this.rootGridsterEl = rootGridster;
1775
1888
  }
1776
- const ghost = calculateDropGhost(event, container, root, payload, this.service.totalColumns);
1889
+ const ghost = calculateDropGhost(event, container, root, payload, this.service.totalColumns, this.service.rowStepPx);
1777
1890
  if (ghost?.isInsideGroup && ghost.hoveredGridster) {
1778
1891
  if (this.activeHoveredEl !== ghost.hoveredGridster) {
1779
1892
  this.cleanupHoveredGroup();
@@ -1874,11 +1987,11 @@ class LibsUiGridLayoutCanvasComponent {
1874
1987
  }
1875
1988
  }
1876
1989
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1877
- 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 }, hiddenToolbar: { classPropertyName: "hiddenToolbar", publicName: "hiddenToolbar", isSignal: true, isRequired: false, transformFunction: null }, templateToolbar: { classPropertyName: "templateToolbar", publicName: "templateToolbar", isSignal: true, isRequired: false, transformFunction: null }, enableZoom: { classPropertyName: "enableZoom", publicName: "enableZoom", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, zoomMin: { classPropertyName: "zoomMin", publicName: "zoomMin", isSignal: true, isRequired: false, transformFunction: null }, zoomMax: { classPropertyName: "zoomMax", publicName: "zoomMax", isSignal: true, isRequired: false, transformFunction: null }, zoomStep: { classPropertyName: "zoomStep", publicName: "zoomStep", isSignal: true, isRequired: false, transformFunction: null }, stageWidth: { classPropertyName: "stageWidth", publicName: "stageWidth", isSignal: true, isRequired: false, transformFunction: null }, stageMinHeight: { classPropertyName: "stageMinHeight", publicName: "stageMinHeight", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null }, showZoomDock: { classPropertyName: "showZoomDock", publicName: "showZoomDock", isSignal: true, isRequired: false, transformFunction: null }, autoFitOnLoad: { classPropertyName: "autoFitOnLoad", publicName: "autoFitOnLoad", isSignal: true, isRequired: false, transformFunction: null }, enableExternalDrop: { classPropertyName: "enableExternalDrop", publicName: "enableExternalDrop", isSignal: true, isRequired: false, transformFunction: null }, showGridLines: { classPropertyName: "showGridLines", publicName: "showGridLines", isSignal: true, isRequired: false, transformFunction: null }, selectedNodeId: { classPropertyName: "selectedNodeId", publicName: "selectedNodeId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outChange: "outChange", outRemoveNode: "outRemoveNode", outAddBlockInside: "outAddBlockInside", outToggleType: "outToggleType", zoom: "zoomChange", outZoomChange: "outZoomChange", selectedNodeId: "selectedNodeIdChange", outDropNode: "outDropNode", outSelectNode: "outSelectNode", outConfigNode: "outConfigNode" }, host: { listeners: { "document:keydown": "handlerDocumentKeyDown($event)", "document:click": "handlerDocumentClick($event)" } }, viewQueries: [{ propertyName: "gridsterRef", first: true, predicate: GridsterComponent, descendants: true, isSignal: true }, { propertyName: "scrollAreaRef", first: true, predicate: ["scrollArea"], descendants: true, isSignal: true }, { propertyName: "zoomDockRef", first: true, predicate: ["zoomDock"], descendants: true, isSignal: true }, { propertyName: "itemHosts", predicate: ["itemHost"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "@if (isZoomActive()) {\n <!-- Khu v\u1EF1c cu\u1ED9n ch\u1EE9a canvas stage thu ph\u00F3ng -->\n <div\n #scrollArea\n class=\"libs-ui-grid-layout-scroll-area relative h-full min-h-0 w-full min-w-0 overflow-auto p-6 flex justify-center items-start\"\n (mousedown)=\"handlerMouseDownTrongLuoi($event)\"\n (wheel)=\"handlerWheel($event)\"\n (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <div\n #canvasStage\n id=\"bi-canvas-stage\"\n class=\"libs-ui-grid-layout-stage relative flex flex-col transition-[zoom] duration-150 origin-top shrink-0 bg-white/80 border border-slate-200/80 rounded-2xl shadow-sm p-4\"\n [style.zoom]=\"currentZoom()\"\n [style.width.px]=\"stageWidth()\"\n [style.min-width.px]=\"stageWidth()\"\n [style.min-height.px]=\"stageMinHeight()\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\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 </div>\n\n <!-- Thanh dock \u0111i\u1EC1u khi\u1EC3n thu ph\u00F3ng n\u1ED5i \u1EDF g\u00F3c d\u01B0\u1EDBi tr\u00E1i -->\n @if (showZoomDock()) {\n <div\n #zoomDock\n id=\"canvas-zoom-dock\"\n role=\"toolbar\"\n tabindex=\"0\"\n class=\"absolute bottom-4 left-6 z-30 flex items-center bg-white rounded-full shadow-2xl border-2 border-slate-400 p-1 space-x-1 select-none\"\n (mousedown)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\">\n <!-- Thu nh\u1ECF (-) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() <= zoomMin()\"\n [title]=\"'i18n_zoom_out' | translate\"\n (click)=\"handlerZoomOut()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- T\u1EF7 l\u1EC7 % v\u00E0 menu preset -->\n <div class=\"relative\">\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-slate-700 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_options' | translate\"\n (click)=\"handlerToggleMenu()\">\n <span>{{ zoomPercent() }}%</span>\n <svg class=\"w-[12px] h-[12px] text-slate-400 transition-transform duration-150\" [class.rotate-180]=\"isMenuOpen()\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </button>\n\n <!-- Menu dropdown c\u00E1c m\u1ED1c thu ph\u00F3ng -->\n @if (isMenuOpen()) {\n <div class=\"absolute bottom-full mb-2 left-0 w-[256px] bg-white rounded-xl shadow-xl border border-slate-200 py-1.5 z-40 text-xs select-none\">\n <div class=\"px-3 py-1 text-[11px] font-semibold text-slate-400 uppercase tracking-wider\">\n {{ 'i18n_zoom_canvas_width' | translate: { width: stageWidth() } }}\n </div>\n <button\n type=\"button\"\n class=\"w-full px-3 py-2 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n (click)=\"handlerFitScreen()\">\n <span class=\"flex items-center gap-2 text-slate-700 font-medium\">\n <svg class=\"w-3.5 h-3.5 text-slate-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n {{ 'i18n_zoom_fit_screen' | translate }}\n </span>\n <span class=\"text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium\">{{ 'i18n_auto' | translate }}</span>\n </button>\n <div class=\"h-px bg-slate-100 my-1\"></div>\n @for (preset of zoomPresets; track preset.value) {\n <button\n type=\"button\"\n class=\"w-full px-3 py-1.5 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n [class.text-blue-600]=\"isCurrentPreset(preset.value)\"\n [class.font-semibold]=\"isCurrentPreset(preset.value)\"\n [class.bg-blue-50]=\"isCurrentPreset(preset.value)\"\n (click)=\"handlerSelectPreset(preset.value)\">\n <span class=\"text-slate-700\" [class.text-blue-600]=\"isCurrentPreset(preset.value)\">\n {{ preset.label | translate }}\n </span>\n @if (isCurrentPreset(preset.value)) {\n <svg class=\"w-3.5 h-3.5 text-blue-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n }\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Ph\u00F3ng to (+) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() >= zoomMax()\"\n [title]=\"'i18n_zoom_in' | translate\"\n (click)=\"handlerZoomIn()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- Ph\u00E2n c\u00E1ch -->\n <div class=\"h-[16px] w-px bg-slate-200\"></div>\n\n <!-- N\u00FAt V\u1EEBa m\u00E0n h\u00ECnh nhanh -->\n <button\n type=\"button\"\n class=\"h-[28px] shrink-0 px-2.5 flex items-center gap-1 rounded-full text-xs text-slate-700 hover:text-slate-900 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_fit_screen_tooltip' | translate\"\n (click)=\"handlerFitScreen()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n <span class=\"whitespace-nowrap font-medium\">{{ 'i18n_zoom_fit_screen' | translate }}</span>\n </button>\n\n <!-- N\u00FAt \u0110\u1EB7t l\u1EA1i 100% (ch\u1EC9 hi\u1EC7n khi kh\u00E1c 100%) -->\n @if (zoomPercent() !== 100) {\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-blue-600 hover:bg-blue-50 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_reset_tooltip' | translate: { width: stageWidth(), height: stageMinHeight() }\"\n (click)=\"handlerResetZoom()\">\n <svg class=\"w-[12px] h-[12px]\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"1 4 1 10 7 10\"/><path d=\"M3.51 15a9 9 0 1 0 2.13-9.36L1 10\"/></svg>\n <span>100%</span>\n </button>\n }\n </div>\n }\n} @else {\n <!-- \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 #scrollArea\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 (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\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}\n", styles: [":host{display:block;position:relative;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:6px;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}:host ::ng-deep gridster-item.libs-ui-grid-layout-selected>.libs-ui-grid-layout-block-shell{outline:2px solid #2563eb!important;outline-offset:-2px!important;border-radius:8px!important;box-shadow:0 0 0 4px #2563eb26!important}:host ::ng-deep gridster.display-grid .gridster-column,:host ::ng-deep gridster gridster.display-grid .gridster-column{border-right:1px solid rgba(226,232,240,.7)!important;border-left:none!important;pointer-events:none!important;height:100%!important;min-height:100%!important}:host ::ng-deep gridster.display-grid .gridster-column:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-column:first-child{border-left:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster.display-grid .gridster-row,:host ::ng-deep gridster gridster.display-grid .gridster-row{border-bottom:1px solid rgba(226,232,240,.7)!important;border-top:none!important;pointer-events:none!important}:host ::ng-deep gridster.display-grid .gridster-row:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-row:first-child{border-top:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster gridster.display-grid{background-color:#f8fafc80!important}:host ::ng-deep gridster-preview{background:#2563eb1f!important;border:1.5px dashed #2563eb!important;border-radius:6px!important;z-index:25!important}.libs-ui-grid-layout-dim-badge{display:inline-flex;align-items:center;height:20px;padding:0 6px;background-color:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;color:#64748b;font-size:10px;font-weight:500;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;pointer-events:none;white-space:nowrap}@container (max-width: 80px){.libs-ui-grid-layout-drag-bar{top:26px}}.libs-ui-grid-layout-stage{box-sizing:border-box;transform-origin:top center;transition:zoom .15s ease-out}#canvas-zoom-dock{box-shadow:0 10px 25px -5px #0000001a,0 8px 10px -6px #0000001a}\n"], dependencies: [{ kind: "component", type: LibsUiGridLayoutCanvasComponent, selector: "libs_ui-services-grid_layout-canvas", inputs: ["node", "depth", "hiddenToolbar", "templateToolbar", "enableZoom", "zoom", "zoomMin", "zoomMax", "zoomStep", "stageWidth", "stageMinHeight", "storageKey", "showZoomDock", "autoFitOnLoad", "enableExternalDrop", "showGridLines", "selectedNodeId"], outputs: ["outChange", "outRemoveNode", "outAddBlockInside", "outToggleType", "zoomChange", "outZoomChange", "selectedNodeIdChange", "outDropNode", "outSelectNode", "outConfigNode"] }, { 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"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1990
+ 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 }, hiddenToolbar: { classPropertyName: "hiddenToolbar", publicName: "hiddenToolbar", isSignal: true, isRequired: false, transformFunction: null }, templateToolbar: { classPropertyName: "templateToolbar", publicName: "templateToolbar", isSignal: true, isRequired: false, transformFunction: null }, enableZoom: { classPropertyName: "enableZoom", publicName: "enableZoom", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, zoomMin: { classPropertyName: "zoomMin", publicName: "zoomMin", isSignal: true, isRequired: false, transformFunction: null }, zoomMax: { classPropertyName: "zoomMax", publicName: "zoomMax", isSignal: true, isRequired: false, transformFunction: null }, zoomStep: { classPropertyName: "zoomStep", publicName: "zoomStep", isSignal: true, isRequired: false, transformFunction: null }, stageWidth: { classPropertyName: "stageWidth", publicName: "stageWidth", isSignal: true, isRequired: false, transformFunction: null }, stageMinHeight: { classPropertyName: "stageMinHeight", publicName: "stageMinHeight", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null }, showZoomDock: { classPropertyName: "showZoomDock", publicName: "showZoomDock", isSignal: true, isRequired: false, transformFunction: null }, autoFitOnLoad: { classPropertyName: "autoFitOnLoad", publicName: "autoFitOnLoad", isSignal: true, isRequired: false, transformFunction: null }, enableExternalDrop: { classPropertyName: "enableExternalDrop", publicName: "enableExternalDrop", isSignal: true, isRequired: false, transformFunction: null }, showGridLines: { classPropertyName: "showGridLines", publicName: "showGridLines", isSignal: true, isRequired: false, transformFunction: null }, selectedNodeId: { classPropertyName: "selectedNodeId", publicName: "selectedNodeId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outChange: "outChange", outRemoveNode: "outRemoveNode", outAddBlockInside: "outAddBlockInside", outToggleType: "outToggleType", zoom: "zoomChange", outZoomChange: "outZoomChange", selectedNodeId: "selectedNodeIdChange", outDropNode: "outDropNode", outSelectNode: "outSelectNode", outConfigNode: "outConfigNode" }, host: { listeners: { "document:keydown": "handlerDocumentKeyDown($event)", "document:click": "handlerDocumentClick($event)" } }, viewQueries: [{ propertyName: "gridsterRef", first: true, predicate: GridsterComponent, descendants: true, isSignal: true }, { propertyName: "scrollAreaRef", first: true, predicate: ["scrollArea"], descendants: true, isSignal: true }, { propertyName: "zoomDockRef", first: true, predicate: ["zoomDock"], descendants: true, isSignal: true }, { propertyName: "itemHosts", predicate: ["itemHost"], descendants: true, read: ViewContainerRef, isSignal: true }, { propertyName: "childCanvasTemplates", predicate: ["childCanvasTpl"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isZoomActive()) {\n <!-- Khu v\u1EF1c cu\u1ED9n ch\u1EE9a canvas stage thu ph\u00F3ng -->\n <div\n #scrollArea\n class=\"libs-ui-grid-layout-scroll-area relative h-full min-h-0 w-full min-w-0 overflow-auto p-6 flex justify-start items-start\"\n (mousedown)=\"handlerMouseDownTrongLuoi($event)\"\n (wheel)=\"handlerWheel($event)\"\n (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <div\n #canvasStage\n id=\"bi-canvas-stage\"\n class=\"libs-ui-grid-layout-stage relative flex flex-col transition-[zoom] duration-150 origin-top shrink-0 mx-auto bg-white/80 border border-slate-200/80 rounded-2xl shadow-sm p-4\"\n [style.zoom]=\"currentZoom()\"\n [style.width.px]=\"stageWidth()\"\n [style.min-width.px]=\"stageWidth()\"\n [style.min-height.px]=\"stageMinHeight()\"\n [style.--libs-ui-grid-layout-visual-gap.px]=\"visualGap()\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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 [attr.data-node-id]=\"item.id\"\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 <!-- \uD83D\uDD34 Container C\u00D3 `getComponentOutlet`: khung n\u00E0y l\u00E0 V\u1ECE, v\u00E0 canvas con \u0111\u01B0\u1EE3c \u0111\u01B0a V\u00C0O TRONG\n component v\u1ECF qua input `childCanvas` (m\u1ED9t `TemplateRef`) \u2014 xem `ng-template #childCanvasTpl`\n b\u00EAn d\u01B0\u1EDBi. V\u1ECF t\u1EF1 quy\u1EBFt \u0111\u1EB7t canvas \u1EDF \u0111\u00E2u gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a n\u00F3.\n Tr\u01B0\u1EDBc \u0111\u00E2y canvas con l\u00E0 ANH EM c\u1EE7a khung n\u00E0y, n\u00EAn v\u1ECF chi\u1EBFm tr\u1ECDn chi\u1EC1u cao r\u1ED3i \u0111\u1EA9y to\u00E0n b\u1ED9\n th\u1EBB con ra ngo\u00E0i v\u00F9ng nh\u00ECn th\u1EA5y (\u0111o 14/09/2026: m\u1ECDi kh\u1ED1i gh\u00E9p ch\u1EC9 c\u00F2n nh\u00E3n v\u00E0 ru\u1ED9t tr\u1EAFng). -->\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) && !item.getComponentOutlet\">\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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\n \uD83D\uDD34 Khu\u00F4n \u0111\u1EB7t trong `ng-template` \u0111\u1EC3 \u0111i \u0111\u01B0\u1EE3c HAI \u0111\u01B0\u1EDDng:\n - Container KH\u00D4NG c\u00F3 v\u1ECF ri\u00EAng \u2192 v\u1EBD th\u1EB3ng t\u1EA1i \u0111\u00E2y, y nh\u01B0 tr\u01B0\u1EDBc.\n - Container C\u00D3 `getComponentOutlet` \u2192 th\u01B0 vi\u1EC7n truy\u1EC1n khu\u00F4n n\u00E0y xu\u1ED1ng component v\u1ECF qua\n input `childCanvas`, v\u1ECF t\u1EF1 \u0111\u1EB7t n\u00F3 v\u00E0o gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a m\u00ECnh. V\u1EBD th\u00EAm \u1EDF\n \u0111\u00E2y n\u1EEFa l\u00E0 hai m\u1EB7t ph\u1EB3ng l\u1ED3ng ch\u1ED3ng nhau. -->\n <ng-template #childCanvasTpl>\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\n (outChange)=\"outChange.emit($event)\"\n (outRemoveNode)=\"outRemoveNode.emit($event)\"\n (outAddBlockInside)=\"outAddBlockInside.emit($event)\"\n (outToggleType)=\"outToggleType.emit($event)\" />\n </ng-template>\n\n @if (!item.getComponentOutlet) {\n <ng-container [ngTemplateOutlet]=\"childCanvasTpl\" />\n }\n }\n </div>\n </gridster-item>\n }\n </gridster>\n </div>\n </div>\n\n <!-- Thanh dock \u0111i\u1EC1u khi\u1EC3n thu ph\u00F3ng n\u1ED5i \u1EDF g\u00F3c d\u01B0\u1EDBi tr\u00E1i -->\n @if (showZoomDock()) {\n <div\n #zoomDock\n id=\"canvas-zoom-dock\"\n role=\"toolbar\"\n tabindex=\"0\"\n class=\"absolute bottom-4 left-6 z-30 flex items-center bg-white rounded-full shadow-2xl border-2 border-slate-400 p-1 space-x-1 select-none\"\n (mousedown)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\">\n <!-- Thu nh\u1ECF (-) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() <= zoomMin()\"\n [title]=\"'i18n_zoom_out' | translate\"\n (click)=\"handlerZoomOut()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- T\u1EF7 l\u1EC7 % v\u00E0 menu preset -->\n <div class=\"relative\">\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-slate-700 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_options' | translate\"\n (click)=\"handlerToggleMenu()\">\n <span>{{ zoomPercent() }}%</span>\n <svg class=\"w-[12px] h-[12px] text-slate-400 transition-transform duration-150\" [class.rotate-180]=\"isMenuOpen()\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </button>\n\n <!-- Menu dropdown c\u00E1c m\u1ED1c thu ph\u00F3ng -->\n @if (isMenuOpen()) {\n <div class=\"absolute bottom-full mb-2 left-0 w-[256px] bg-white rounded-xl shadow-xl border border-slate-200 py-1.5 z-40 text-xs select-none\">\n <div class=\"px-3 py-1 text-[11px] font-semibold text-slate-400 uppercase tracking-wider\">\n {{ 'i18n_zoom_canvas_width' | translate: { width: stageWidth() } }}\n </div>\n <button\n type=\"button\"\n class=\"w-full px-3 py-2 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n (click)=\"handlerFitScreen()\">\n <span class=\"flex items-center gap-2 text-slate-700 font-medium\">\n <svg class=\"w-3.5 h-3.5 text-slate-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n {{ 'i18n_zoom_fit_screen' | translate }}\n </span>\n <span class=\"text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium\">{{ 'i18n_auto' | translate }}</span>\n </button>\n <div class=\"h-px bg-slate-100 my-1\"></div>\n @for (preset of zoomPresets; track preset.value) {\n <button\n type=\"button\"\n class=\"w-full px-3 py-1.5 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n [class.text-blue-600]=\"isCurrentPreset(preset.value)\"\n [class.font-semibold]=\"isCurrentPreset(preset.value)\"\n [class.bg-blue-50]=\"isCurrentPreset(preset.value)\"\n (click)=\"handlerSelectPreset(preset.value)\">\n <span class=\"text-slate-700\" [class.text-blue-600]=\"isCurrentPreset(preset.value)\">\n {{ preset.label | translate }}\n </span>\n @if (isCurrentPreset(preset.value)) {\n <svg class=\"w-3.5 h-3.5 text-blue-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n }\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Ph\u00F3ng to (+) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() >= zoomMax()\"\n [title]=\"'i18n_zoom_in' | translate\"\n (click)=\"handlerZoomIn()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- Ph\u00E2n c\u00E1ch -->\n <div class=\"h-[16px] w-px bg-slate-200\"></div>\n\n <!-- N\u00FAt V\u1EEBa m\u00E0n h\u00ECnh nhanh -->\n <button\n type=\"button\"\n class=\"h-[28px] shrink-0 px-2.5 flex items-center gap-1 rounded-full text-xs text-slate-700 hover:text-slate-900 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_fit_screen_tooltip' | translate\"\n (click)=\"handlerFitScreen()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n <span class=\"whitespace-nowrap font-medium\">{{ 'i18n_zoom_fit_screen' | translate }}</span>\n </button>\n\n <!-- N\u00FAt \u0110\u1EB7t l\u1EA1i 100% (ch\u1EC9 hi\u1EC7n khi kh\u00E1c 100%) -->\n @if (zoomPercent() !== 100) {\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-blue-600 hover:bg-blue-50 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_reset_tooltip' | translate: { width: stageWidth(), height: stageMinHeight() }\"\n (click)=\"handlerResetZoom()\">\n <svg class=\"w-[12px] h-[12px]\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"1 4 1 10 7 10\"/><path d=\"M3.51 15a9 9 0 1 0 2.13-9.36L1 10\"/></svg>\n <span>100%</span>\n </button>\n }\n </div>\n }\n} @else {\n <!-- \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 #scrollArea\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 (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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 [attr.data-node-id]=\"item.id\"\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 <!-- \uD83D\uDD34 Container C\u00D3 `getComponentOutlet`: khung n\u00E0y l\u00E0 V\u1ECE, v\u00E0 canvas con \u0111\u01B0\u1EE3c \u0111\u01B0a V\u00C0O TRONG\n component v\u1ECF qua input `childCanvas` (m\u1ED9t `TemplateRef`) \u2014 xem `ng-template #childCanvasTpl`\n b\u00EAn d\u01B0\u1EDBi. V\u1ECF t\u1EF1 quy\u1EBFt \u0111\u1EB7t canvas \u1EDF \u0111\u00E2u gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a n\u00F3.\n Tr\u01B0\u1EDBc \u0111\u00E2y canvas con l\u00E0 ANH EM c\u1EE7a khung n\u00E0y, n\u00EAn v\u1ECF chi\u1EBFm tr\u1ECDn chi\u1EC1u cao r\u1ED3i \u0111\u1EA9y to\u00E0n b\u1ED9\n th\u1EBB con ra ngo\u00E0i v\u00F9ng nh\u00ECn th\u1EA5y (\u0111o 14/09/2026: m\u1ECDi kh\u1ED1i gh\u00E9p ch\u1EC9 c\u00F2n nh\u00E3n v\u00E0 ru\u1ED9t tr\u1EAFng). -->\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) && !item.getComponentOutlet\">\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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\n \uD83D\uDD34 Khu\u00F4n \u0111\u1EB7t trong `ng-template` \u0111\u1EC3 \u0111i \u0111\u01B0\u1EE3c HAI \u0111\u01B0\u1EDDng:\n - Container KH\u00D4NG c\u00F3 v\u1ECF ri\u00EAng \u2192 v\u1EBD th\u1EB3ng t\u1EA1i \u0111\u00E2y, y nh\u01B0 tr\u01B0\u1EDBc.\n - Container C\u00D3 `getComponentOutlet` \u2192 th\u01B0 vi\u1EC7n truy\u1EC1n khu\u00F4n n\u00E0y xu\u1ED1ng component v\u1ECF qua\n input `childCanvas`, v\u1ECF t\u1EF1 \u0111\u1EB7t n\u00F3 v\u00E0o gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a m\u00ECnh. V\u1EBD th\u00EAm \u1EDF\n \u0111\u00E2y n\u1EEFa l\u00E0 hai m\u1EB7t ph\u1EB3ng l\u1ED3ng ch\u1ED3ng nhau. -->\n <ng-template #childCanvasTpl>\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\n (outChange)=\"outChange.emit($event)\"\n (outRemoveNode)=\"outRemoveNode.emit($event)\"\n (outAddBlockInside)=\"outAddBlockInside.emit($event)\"\n (outToggleType)=\"outToggleType.emit($event)\" />\n </ng-template>\n\n @if (!item.getComponentOutlet) {\n <ng-container [ngTemplateOutlet]=\"childCanvasTpl\" />\n }\n }\n </div>\n </gridster-item>\n }\n </gridster>\n </div>\n}\n", styles: [":host{display:block;position:relative;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:6px;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}:host ::ng-deep gridster-item.libs-ui-grid-layout-selected>.libs-ui-grid-layout-block-shell{outline:2px solid #2563eb!important;outline-offset:-2px!important;border-radius:8px!important;box-shadow:0 0 0 4px #2563eb26!important}:host ::ng-deep gridster.display-grid .gridster-column,:host ::ng-deep gridster gridster.display-grid .gridster-column{border-right:1px solid rgba(226,232,240,.7)!important;border-left:none!important;pointer-events:none!important;height:100%!important;min-height:100%!important}:host ::ng-deep gridster.display-grid .gridster-column:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-column:first-child{border-left:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster.display-grid .gridster-row,:host ::ng-deep gridster gridster.display-grid .gridster-row{border-bottom:1px solid rgba(226,232,240,.7)!important;border-top:none!important;pointer-events:none!important}:host ::ng-deep gridster.display-grid .gridster-row:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-row:first-child{border-top:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster gridster.display-grid{background-color:#f8fafc80!important}:host ::ng-deep gridster-preview{background:#2563eb1f!important;border:1.5px dashed #2563eb!important;border-radius:6px!important;z-index:25!important}.libs-ui-grid-layout-dim-badge{display:inline-flex;align-items:center;height:20px;padding:0 6px;background-color:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;color:#64748b;font-size:10px;font-weight:500;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;pointer-events:none;white-space:nowrap}@container (max-width: 80px){.libs-ui-grid-layout-drag-bar{top:26px}}.libs-ui-grid-layout-stage{box-sizing:border-box;transform-origin:top center;transition:zoom .15s ease-out}#canvas-zoom-dock{box-shadow:0 10px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.libs-ui-grid-layout-stage>gridster>gridster-item>.libs-ui-grid-layout-block-shell{inset:calc(var(--libs-ui-grid-layout-visual-gap, 0px) / 2);width:calc(100% - var(--libs-ui-grid-layout-visual-gap, 0px));height:calc(100% - var(--libs-ui-grid-layout-visual-gap, 0px));box-sizing:border-box}\n"], dependencies: [{ kind: "component", type: LibsUiGridLayoutCanvasComponent, selector: "libs_ui-services-grid_layout-canvas", inputs: ["node", "depth", "hiddenToolbar", "templateToolbar", "enableZoom", "zoom", "zoomMin", "zoomMax", "zoomStep", "stageWidth", "stageMinHeight", "storageKey", "showZoomDock", "autoFitOnLoad", "enableExternalDrop", "showGridLines", "selectedNodeId"], outputs: ["outChange", "outRemoveNode", "outAddBlockInside", "outToggleType", "zoomChange", "outZoomChange", "selectedNodeIdChange", "outDropNode", "outSelectNode", "outConfigNode"] }, { 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"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1878
1991
  }
1879
1992
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiGridLayoutCanvasComponent, decorators: [{
1880
1993
  type: Component,
1881
- args: [{ selector: 'libs_ui-services-grid_layout-canvas', standalone: true, imports: [GridsterModule, LibsUiComponentsButtonsButtonComponent, NgTemplateOutlet, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isZoomActive()) {\n <!-- Khu v\u1EF1c cu\u1ED9n ch\u1EE9a canvas stage thu ph\u00F3ng -->\n <div\n #scrollArea\n class=\"libs-ui-grid-layout-scroll-area relative h-full min-h-0 w-full min-w-0 overflow-auto p-6 flex justify-center items-start\"\n (mousedown)=\"handlerMouseDownTrongLuoi($event)\"\n (wheel)=\"handlerWheel($event)\"\n (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <div\n #canvasStage\n id=\"bi-canvas-stage\"\n class=\"libs-ui-grid-layout-stage relative flex flex-col transition-[zoom] duration-150 origin-top shrink-0 bg-white/80 border border-slate-200/80 rounded-2xl shadow-sm p-4\"\n [style.zoom]=\"currentZoom()\"\n [style.width.px]=\"stageWidth()\"\n [style.min-width.px]=\"stageWidth()\"\n [style.min-height.px]=\"stageMinHeight()\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\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 </div>\n\n <!-- Thanh dock \u0111i\u1EC1u khi\u1EC3n thu ph\u00F3ng n\u1ED5i \u1EDF g\u00F3c d\u01B0\u1EDBi tr\u00E1i -->\n @if (showZoomDock()) {\n <div\n #zoomDock\n id=\"canvas-zoom-dock\"\n role=\"toolbar\"\n tabindex=\"0\"\n class=\"absolute bottom-4 left-6 z-30 flex items-center bg-white rounded-full shadow-2xl border-2 border-slate-400 p-1 space-x-1 select-none\"\n (mousedown)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\">\n <!-- Thu nh\u1ECF (-) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() <= zoomMin()\"\n [title]=\"'i18n_zoom_out' | translate\"\n (click)=\"handlerZoomOut()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- T\u1EF7 l\u1EC7 % v\u00E0 menu preset -->\n <div class=\"relative\">\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-slate-700 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_options' | translate\"\n (click)=\"handlerToggleMenu()\">\n <span>{{ zoomPercent() }}%</span>\n <svg class=\"w-[12px] h-[12px] text-slate-400 transition-transform duration-150\" [class.rotate-180]=\"isMenuOpen()\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </button>\n\n <!-- Menu dropdown c\u00E1c m\u1ED1c thu ph\u00F3ng -->\n @if (isMenuOpen()) {\n <div class=\"absolute bottom-full mb-2 left-0 w-[256px] bg-white rounded-xl shadow-xl border border-slate-200 py-1.5 z-40 text-xs select-none\">\n <div class=\"px-3 py-1 text-[11px] font-semibold text-slate-400 uppercase tracking-wider\">\n {{ 'i18n_zoom_canvas_width' | translate: { width: stageWidth() } }}\n </div>\n <button\n type=\"button\"\n class=\"w-full px-3 py-2 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n (click)=\"handlerFitScreen()\">\n <span class=\"flex items-center gap-2 text-slate-700 font-medium\">\n <svg class=\"w-3.5 h-3.5 text-slate-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n {{ 'i18n_zoom_fit_screen' | translate }}\n </span>\n <span class=\"text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium\">{{ 'i18n_auto' | translate }}</span>\n </button>\n <div class=\"h-px bg-slate-100 my-1\"></div>\n @for (preset of zoomPresets; track preset.value) {\n <button\n type=\"button\"\n class=\"w-full px-3 py-1.5 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n [class.text-blue-600]=\"isCurrentPreset(preset.value)\"\n [class.font-semibold]=\"isCurrentPreset(preset.value)\"\n [class.bg-blue-50]=\"isCurrentPreset(preset.value)\"\n (click)=\"handlerSelectPreset(preset.value)\">\n <span class=\"text-slate-700\" [class.text-blue-600]=\"isCurrentPreset(preset.value)\">\n {{ preset.label | translate }}\n </span>\n @if (isCurrentPreset(preset.value)) {\n <svg class=\"w-3.5 h-3.5 text-blue-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n }\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Ph\u00F3ng to (+) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() >= zoomMax()\"\n [title]=\"'i18n_zoom_in' | translate\"\n (click)=\"handlerZoomIn()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- Ph\u00E2n c\u00E1ch -->\n <div class=\"h-[16px] w-px bg-slate-200\"></div>\n\n <!-- N\u00FAt V\u1EEBa m\u00E0n h\u00ECnh nhanh -->\n <button\n type=\"button\"\n class=\"h-[28px] shrink-0 px-2.5 flex items-center gap-1 rounded-full text-xs text-slate-700 hover:text-slate-900 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_fit_screen_tooltip' | translate\"\n (click)=\"handlerFitScreen()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n <span class=\"whitespace-nowrap font-medium\">{{ 'i18n_zoom_fit_screen' | translate }}</span>\n </button>\n\n <!-- N\u00FAt \u0110\u1EB7t l\u1EA1i 100% (ch\u1EC9 hi\u1EC7n khi kh\u00E1c 100%) -->\n @if (zoomPercent() !== 100) {\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-blue-600 hover:bg-blue-50 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_reset_tooltip' | translate: { width: stageWidth(), height: stageMinHeight() }\"\n (click)=\"handlerResetZoom()\">\n <svg class=\"w-[12px] h-[12px]\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"1 4 1 10 7 10\"/><path d=\"M3.51 15a9 9 0 1 0 2.13-9.36L1 10\"/></svg>\n <span>100%</span>\n </button>\n }\n </div>\n }\n} @else {\n <!-- \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 #scrollArea\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 (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\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}\n", styles: [":host{display:block;position:relative;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:6px;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}:host ::ng-deep gridster-item.libs-ui-grid-layout-selected>.libs-ui-grid-layout-block-shell{outline:2px solid #2563eb!important;outline-offset:-2px!important;border-radius:8px!important;box-shadow:0 0 0 4px #2563eb26!important}:host ::ng-deep gridster.display-grid .gridster-column,:host ::ng-deep gridster gridster.display-grid .gridster-column{border-right:1px solid rgba(226,232,240,.7)!important;border-left:none!important;pointer-events:none!important;height:100%!important;min-height:100%!important}:host ::ng-deep gridster.display-grid .gridster-column:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-column:first-child{border-left:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster.display-grid .gridster-row,:host ::ng-deep gridster gridster.display-grid .gridster-row{border-bottom:1px solid rgba(226,232,240,.7)!important;border-top:none!important;pointer-events:none!important}:host ::ng-deep gridster.display-grid .gridster-row:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-row:first-child{border-top:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster gridster.display-grid{background-color:#f8fafc80!important}:host ::ng-deep gridster-preview{background:#2563eb1f!important;border:1.5px dashed #2563eb!important;border-radius:6px!important;z-index:25!important}.libs-ui-grid-layout-dim-badge{display:inline-flex;align-items:center;height:20px;padding:0 6px;background-color:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;color:#64748b;font-size:10px;font-weight:500;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;pointer-events:none;white-space:nowrap}@container (max-width: 80px){.libs-ui-grid-layout-drag-bar{top:26px}}.libs-ui-grid-layout-stage{box-sizing:border-box;transform-origin:top center;transition:zoom .15s ease-out}#canvas-zoom-dock{box-shadow:0 10px 25px -5px #0000001a,0 8px 10px -6px #0000001a}\n"] }]
1994
+ args: [{ selector: 'libs_ui-services-grid_layout-canvas', standalone: true, imports: [GridsterModule, LibsUiComponentsButtonsButtonComponent, NgTemplateOutlet, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isZoomActive()) {\n <!-- Khu v\u1EF1c cu\u1ED9n ch\u1EE9a canvas stage thu ph\u00F3ng -->\n <div\n #scrollArea\n class=\"libs-ui-grid-layout-scroll-area relative h-full min-h-0 w-full min-w-0 overflow-auto p-6 flex justify-start items-start\"\n (mousedown)=\"handlerMouseDownTrongLuoi($event)\"\n (wheel)=\"handlerWheel($event)\"\n (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <div\n #canvasStage\n id=\"bi-canvas-stage\"\n class=\"libs-ui-grid-layout-stage relative flex flex-col transition-[zoom] duration-150 origin-top shrink-0 mx-auto bg-white/80 border border-slate-200/80 rounded-2xl shadow-sm p-4\"\n [style.zoom]=\"currentZoom()\"\n [style.width.px]=\"stageWidth()\"\n [style.min-width.px]=\"stageWidth()\"\n [style.min-height.px]=\"stageMinHeight()\"\n [style.--libs-ui-grid-layout-visual-gap.px]=\"visualGap()\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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 [attr.data-node-id]=\"item.id\"\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 <!-- \uD83D\uDD34 Container C\u00D3 `getComponentOutlet`: khung n\u00E0y l\u00E0 V\u1ECE, v\u00E0 canvas con \u0111\u01B0\u1EE3c \u0111\u01B0a V\u00C0O TRONG\n component v\u1ECF qua input `childCanvas` (m\u1ED9t `TemplateRef`) \u2014 xem `ng-template #childCanvasTpl`\n b\u00EAn d\u01B0\u1EDBi. V\u1ECF t\u1EF1 quy\u1EBFt \u0111\u1EB7t canvas \u1EDF \u0111\u00E2u gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a n\u00F3.\n Tr\u01B0\u1EDBc \u0111\u00E2y canvas con l\u00E0 ANH EM c\u1EE7a khung n\u00E0y, n\u00EAn v\u1ECF chi\u1EBFm tr\u1ECDn chi\u1EC1u cao r\u1ED3i \u0111\u1EA9y to\u00E0n b\u1ED9\n th\u1EBB con ra ngo\u00E0i v\u00F9ng nh\u00ECn th\u1EA5y (\u0111o 14/09/2026: m\u1ECDi kh\u1ED1i gh\u00E9p ch\u1EC9 c\u00F2n nh\u00E3n v\u00E0 ru\u1ED9t tr\u1EAFng). -->\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) && !item.getComponentOutlet\">\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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\n \uD83D\uDD34 Khu\u00F4n \u0111\u1EB7t trong `ng-template` \u0111\u1EC3 \u0111i \u0111\u01B0\u1EE3c HAI \u0111\u01B0\u1EDDng:\n - Container KH\u00D4NG c\u00F3 v\u1ECF ri\u00EAng \u2192 v\u1EBD th\u1EB3ng t\u1EA1i \u0111\u00E2y, y nh\u01B0 tr\u01B0\u1EDBc.\n - Container C\u00D3 `getComponentOutlet` \u2192 th\u01B0 vi\u1EC7n truy\u1EC1n khu\u00F4n n\u00E0y xu\u1ED1ng component v\u1ECF qua\n input `childCanvas`, v\u1ECF t\u1EF1 \u0111\u1EB7t n\u00F3 v\u00E0o gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a m\u00ECnh. V\u1EBD th\u00EAm \u1EDF\n \u0111\u00E2y n\u1EEFa l\u00E0 hai m\u1EB7t ph\u1EB3ng l\u1ED3ng ch\u1ED3ng nhau. -->\n <ng-template #childCanvasTpl>\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\n (outChange)=\"outChange.emit($event)\"\n (outRemoveNode)=\"outRemoveNode.emit($event)\"\n (outAddBlockInside)=\"outAddBlockInside.emit($event)\"\n (outToggleType)=\"outToggleType.emit($event)\" />\n </ng-template>\n\n @if (!item.getComponentOutlet) {\n <ng-container [ngTemplateOutlet]=\"childCanvasTpl\" />\n }\n }\n </div>\n </gridster-item>\n }\n </gridster>\n </div>\n </div>\n\n <!-- Thanh dock \u0111i\u1EC1u khi\u1EC3n thu ph\u00F3ng n\u1ED5i \u1EDF g\u00F3c d\u01B0\u1EDBi tr\u00E1i -->\n @if (showZoomDock()) {\n <div\n #zoomDock\n id=\"canvas-zoom-dock\"\n role=\"toolbar\"\n tabindex=\"0\"\n class=\"absolute bottom-4 left-6 z-30 flex items-center bg-white rounded-full shadow-2xl border-2 border-slate-400 p-1 space-x-1 select-none\"\n (mousedown)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\">\n <!-- Thu nh\u1ECF (-) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() <= zoomMin()\"\n [title]=\"'i18n_zoom_out' | translate\"\n (click)=\"handlerZoomOut()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- T\u1EF7 l\u1EC7 % v\u00E0 menu preset -->\n <div class=\"relative\">\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-slate-700 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_options' | translate\"\n (click)=\"handlerToggleMenu()\">\n <span>{{ zoomPercent() }}%</span>\n <svg class=\"w-[12px] h-[12px] text-slate-400 transition-transform duration-150\" [class.rotate-180]=\"isMenuOpen()\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </button>\n\n <!-- Menu dropdown c\u00E1c m\u1ED1c thu ph\u00F3ng -->\n @if (isMenuOpen()) {\n <div class=\"absolute bottom-full mb-2 left-0 w-[256px] bg-white rounded-xl shadow-xl border border-slate-200 py-1.5 z-40 text-xs select-none\">\n <div class=\"px-3 py-1 text-[11px] font-semibold text-slate-400 uppercase tracking-wider\">\n {{ 'i18n_zoom_canvas_width' | translate: { width: stageWidth() } }}\n </div>\n <button\n type=\"button\"\n class=\"w-full px-3 py-2 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n (click)=\"handlerFitScreen()\">\n <span class=\"flex items-center gap-2 text-slate-700 font-medium\">\n <svg class=\"w-3.5 h-3.5 text-slate-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n {{ 'i18n_zoom_fit_screen' | translate }}\n </span>\n <span class=\"text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium\">{{ 'i18n_auto' | translate }}</span>\n </button>\n <div class=\"h-px bg-slate-100 my-1\"></div>\n @for (preset of zoomPresets; track preset.value) {\n <button\n type=\"button\"\n class=\"w-full px-3 py-1.5 text-left flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer\"\n [class.text-blue-600]=\"isCurrentPreset(preset.value)\"\n [class.font-semibold]=\"isCurrentPreset(preset.value)\"\n [class.bg-blue-50]=\"isCurrentPreset(preset.value)\"\n (click)=\"handlerSelectPreset(preset.value)\">\n <span class=\"text-slate-700\" [class.text-blue-600]=\"isCurrentPreset(preset.value)\">\n {{ preset.label | translate }}\n </span>\n @if (isCurrentPreset(preset.value)) {\n <svg class=\"w-3.5 h-3.5 text-blue-600\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>\n }\n </button>\n }\n </div>\n }\n </div>\n\n <!-- Ph\u00F3ng to (+) -->\n <button\n type=\"button\"\n class=\"w-[28px] h-[28px] flex items-center justify-center rounded-full text-slate-700 hover:text-slate-900 hover:bg-slate-200 disabled:opacity-30 disabled:hover:bg-transparent transition-colors cursor-pointer\"\n [disabled]=\"currentZoom() >= zoomMax()\"\n [title]=\"'i18n_zoom_in' | translate\"\n (click)=\"handlerZoomIn()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\"><line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"/><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"/></svg>\n </button>\n\n <!-- Ph\u00E2n c\u00E1ch -->\n <div class=\"h-[16px] w-px bg-slate-200\"></div>\n\n <!-- N\u00FAt V\u1EEBa m\u00E0n h\u00ECnh nhanh -->\n <button\n type=\"button\"\n class=\"h-[28px] shrink-0 px-2.5 flex items-center gap-1 rounded-full text-xs text-slate-700 hover:text-slate-900 hover:bg-slate-200 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_fit_screen_tooltip' | translate\"\n (click)=\"handlerFitScreen()\">\n <svg class=\"w-3.5 h-3.5\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"/><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"/></svg>\n <span class=\"whitespace-nowrap font-medium\">{{ 'i18n_zoom_fit_screen' | translate }}</span>\n </button>\n\n <!-- N\u00FAt \u0110\u1EB7t l\u1EA1i 100% (ch\u1EC9 hi\u1EC7n khi kh\u00E1c 100%) -->\n @if (zoomPercent() !== 100) {\n <button\n type=\"button\"\n class=\"h-[28px] px-2 flex items-center gap-1 rounded-full text-xs font-semibold text-blue-600 hover:bg-blue-50 transition-colors cursor-pointer\"\n [title]=\"'i18n_zoom_reset_tooltip' | translate: { width: stageWidth(), height: stageMinHeight() }\"\n (click)=\"handlerResetZoom()\">\n <svg class=\"w-[12px] h-[12px]\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"1 4 1 10 7 10\"/><path d=\"M3.51 15a9 9 0 1 0 2.13-9.36L1 10\"/></svg>\n <span>100%</span>\n </button>\n }\n </div>\n }\n} @else {\n <!-- \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 #scrollArea\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 (dragover)=\"handlerDragOver($event)\"\n (dragleave)=\"handlerDragLeave($event)\"\n (drop)=\"handlerDrop($event)\">\n <!-- Snapped Drop Ghost Preview -->\n @if (dropGhost(); as ghost) {\n <div\n class=\"pointer-events-none absolute z-30 flex flex-col items-center justify-center overflow-hidden rounded-lg border-2 border-dashed border-blue-500 bg-blue-500/15 p-1 shadow-sm transition-all duration-75 ease-out backdrop-blur-[0.5px] select-none\"\n [style.left.px]=\"ghost.pixelLeft\"\n [style.top.px]=\"ghost.pixelTop\"\n [style.width.px]=\"ghost.pixelWidth\"\n [style.height.px]=\"ghost.pixelHeight\">\n <div class=\"inline-flex max-w-[95%] items-center gap-1 truncate rounded-md bg-blue-600 px-2 py-0.5 text-[11px] font-semibold text-white shadow-xs\">\n @if (ghost.cardIconClass) {\n <i [class]=\"ghost.cardIconClass + ' text-xs'\"></i>\n } @else if (ghost.cardIconText) {\n <span class=\"text-[10px] font-bold\">{{ ghost.cardIconText }}</span>\n } @else {\n <span class=\"text-xs font-bold\">{{ ghost.isInsideGroup ? '\u21B3' : '+' }}</span>\n }\n <span class=\"truncate\">{{ ghost.cardTitle }}</span>\n <span class=\"text-[9px] font-mono opacity-75\">({{ ghost.cols }}c)</span>\n </div>\n\n @if (ghost.pixelHeight >= 80) {\n <div class=\"mt-1 flex max-w-[95%] items-center gap-1.5 truncate rounded border border-blue-100 bg-white/95 px-2 py-0.5 text-[10px] font-medium text-blue-700 shadow-xs\">\n @if (ghost.isInsideGroup) {\n <span class=\"truncate font-bold text-blue-900\">\u21B3 {{ ghost.groupTitle }}</span>\n <span class=\"text-blue-300\">\u2022</span>\n }\n <span>C\u1ED9t {{ ghost.col + 1 }}-{{ ghost.col + ghost.cols }}</span>\n </div>\n }\n </div>\n }\n <gridster class=\"h-full w-full\" [class.display-grid]=\"showGridLines()\" [options]=\"gridOptions()\">\n @for (item of children(); track item.id; let itemIndex = $index) {\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 [class.libs-ui-grid-layout-selected]=\"selectedNodeId() === item.id\"\n (click)=\"handlerSelectNode(item, $event)\"\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 [attr.data-node-id]=\"item.id\"\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 <!-- \uD83D\uDD34 Container C\u00D3 `getComponentOutlet`: khung n\u00E0y l\u00E0 V\u1ECE, v\u00E0 canvas con \u0111\u01B0\u1EE3c \u0111\u01B0a V\u00C0O TRONG\n component v\u1ECF qua input `childCanvas` (m\u1ED9t `TemplateRef`) \u2014 xem `ng-template #childCanvasTpl`\n b\u00EAn d\u01B0\u1EDBi. V\u1ECF t\u1EF1 quy\u1EBFt \u0111\u1EB7t canvas \u1EDF \u0111\u00E2u gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a n\u00F3.\n Tr\u01B0\u1EDBc \u0111\u00E2y canvas con l\u00E0 ANH EM c\u1EE7a khung n\u00E0y, n\u00EAn v\u1ECF chi\u1EBFm tr\u1ECDn chi\u1EC1u cao r\u1ED3i \u0111\u1EA9y to\u00E0n b\u1ED9\n th\u1EBB con ra ngo\u00E0i v\u00F9ng nh\u00ECn th\u1EA5y (\u0111o 14/09/2026: m\u1ECDi kh\u1ED1i gh\u00E9p ch\u1EC9 c\u00F2n nh\u00E3n v\u00E0 ru\u1ED9t tr\u1EAFng). -->\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) && !item.getComponentOutlet\">\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\">{{ 'i18n_container_block' | translate }}</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() && !hiddenToolbar()) {\n @if (templateToolbar()) {\n <!-- N\u01A1i d\u00F9ng t\u1EF1 v\u1EBD thanh c\u00F4ng c\u1EE5: nh\u1EADn kh\u1ED1i + c\u00E1c h\u00E0m ph\u00E1t event qua context.\n\n \uD83D\uDD34 Kh\u1ED1i b\u1ECDc t\u1EF1 ch\u1EB7n `mousedown`/`click` n\u1ED5i l\u00EAn gridster. Thanh c\u00F4ng c\u1EE5 n\u1EB1m trong\n v\u00F9ng mang `dragHandleClass`; kh\u00F4ng ch\u1EB7n th\u00EC c\u00FA b\u1EA5m b\u1ECB hi\u1EC3u l\u00E0 b\u1EAFt \u0111\u1EA7u k\u00E9o v\u00E0\n `delayStart: 160` nu\u1ED1t lu\u00F4n `click` \u2014 n\u00FAt trong template ngo\u00E0i b\u1EA5m kh\u00F4ng \u0103n\n (\u0111o 09/09/2026: b\u1EA5m b\u1EB1ng `.click()` c\u1EE7a DOM th\u00EC m\u1EDF, b\u1EA5m chu\u1ED9t th\u1EADt th\u00EC kh\u00F4ng).\n Ch\u1EB7n \u1EDF \u0110\u00C2Y \u0111\u1EC3 n\u01A1i d\u00F9ng kh\u00F4ng ph\u1EA3i bi\u1EBFt b\u1EABy n\u00E0y. -->\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n role=\"toolbar\"\n tabindex=\"0\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\"\n (mousedown)=\"handlerStopEvent($event)\"\n (keydown)=\"handlerStopEvent($event)\"\n (click)=\"handlerStopEvent($event)\">\n <ng-container\n *ngTemplateOutlet=\"templateToolbar()!; context: toolbarContexts()[itemIndex]\" />\n </div>\n } @else {\n <div\n class=\"libs-ui-grid-layout-toolbar\"\n [class.libs-ui-grid-layout-toolbar-child]=\"isNestedCanvas()\">\n <span class=\"libs-ui-grid-layout-dim-badge\">\n {{ item.rows }} h\u00E0ng \u00D7 {{ item.cols }} c\u1ED9t\n </span>\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'\"\n [iconOnlyType]=\"true\"\n [sizeButton]=\"'smaller'\"\n [classIconLeft]=\"'libs-ui-icon-setting'\"\n [popover]=\"{ config: { content: 'C\u1EA5u h\u00ECnh th\u1EBB', zIndex: 1300 } }\"\n (outClick)=\"handlerConfig($event, item)\" />\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\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\n \uD83D\uDD34 Khu\u00F4n \u0111\u1EB7t trong `ng-template` \u0111\u1EC3 \u0111i \u0111\u01B0\u1EE3c HAI \u0111\u01B0\u1EDDng:\n - Container KH\u00D4NG c\u00F3 v\u1ECF ri\u00EAng \u2192 v\u1EBD th\u1EB3ng t\u1EA1i \u0111\u00E2y, y nh\u01B0 tr\u01B0\u1EDBc.\n - Container C\u00D3 `getComponentOutlet` \u2192 th\u01B0 vi\u1EC7n truy\u1EC1n khu\u00F4n n\u00E0y xu\u1ED1ng component v\u1ECF qua\n input `childCanvas`, v\u1ECF t\u1EF1 \u0111\u1EB7t n\u00F3 v\u00E0o gi\u1EEFa ti\u00EAu \u0111\u1EC1 v\u00E0 ph\u1EA7n th\u00E2n c\u1EE7a m\u00ECnh. V\u1EBD th\u00EAm \u1EDF\n \u0111\u00E2y n\u1EEFa l\u00E0 hai m\u1EB7t ph\u1EB3ng l\u1ED3ng ch\u1ED3ng nhau. -->\n <ng-template #childCanvasTpl>\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 [hiddenToolbar]=\"hiddenToolbar()\"\n [templateToolbar]=\"templateToolbar()\"\n [selectedNodeId]=\"selectedNodeId()\"\n (outSelectNode)=\"outSelectNode.emit($event)\"\n (outConfigNode)=\"outConfigNode.emit($event)\"\n (outChange)=\"outChange.emit($event)\"\n (outRemoveNode)=\"outRemoveNode.emit($event)\"\n (outAddBlockInside)=\"outAddBlockInside.emit($event)\"\n (outToggleType)=\"outToggleType.emit($event)\" />\n </ng-template>\n\n @if (!item.getComponentOutlet) {\n <ng-container [ngTemplateOutlet]=\"childCanvasTpl\" />\n }\n }\n </div>\n </gridster-item>\n }\n </gridster>\n </div>\n}\n", styles: [":host{display:block;position:relative;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:6px;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}:host ::ng-deep gridster-item.libs-ui-grid-layout-selected>.libs-ui-grid-layout-block-shell{outline:2px solid #2563eb!important;outline-offset:-2px!important;border-radius:8px!important;box-shadow:0 0 0 4px #2563eb26!important}:host ::ng-deep gridster.display-grid .gridster-column,:host ::ng-deep gridster gridster.display-grid .gridster-column{border-right:1px solid rgba(226,232,240,.7)!important;border-left:none!important;pointer-events:none!important;height:100%!important;min-height:100%!important}:host ::ng-deep gridster.display-grid .gridster-column:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-column:first-child{border-left:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster.display-grid .gridster-row,:host ::ng-deep gridster gridster.display-grid .gridster-row{border-bottom:1px solid rgba(226,232,240,.7)!important;border-top:none!important;pointer-events:none!important}:host ::ng-deep gridster.display-grid .gridster-row:first-child,:host ::ng-deep gridster gridster.display-grid .gridster-row:first-child{border-top:1px solid rgba(226,232,240,.7)!important}:host ::ng-deep gridster gridster.display-grid{background-color:#f8fafc80!important}:host ::ng-deep gridster-preview{background:#2563eb1f!important;border:1.5px dashed #2563eb!important;border-radius:6px!important;z-index:25!important}.libs-ui-grid-layout-dim-badge{display:inline-flex;align-items:center;height:20px;padding:0 6px;background-color:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;color:#64748b;font-size:10px;font-weight:500;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;pointer-events:none;white-space:nowrap}@container (max-width: 80px){.libs-ui-grid-layout-drag-bar{top:26px}}.libs-ui-grid-layout-stage{box-sizing:border-box;transform-origin:top center;transition:zoom .15s ease-out}#canvas-zoom-dock{box-shadow:0 10px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.libs-ui-grid-layout-stage>gridster>gridster-item>.libs-ui-grid-layout-block-shell{inset:calc(var(--libs-ui-grid-layout-visual-gap, 0px) / 2);width:calc(100% - var(--libs-ui-grid-layout-visual-gap, 0px));height:calc(100% - var(--libs-ui-grid-layout-visual-gap, 0px));box-sizing:border-box}\n"] }]
1882
1995
  }], ctorParameters: () => [], propDecorators: { handlerDocumentKeyDown: [{
1883
1996
  type: HostListener,
1884
1997
  args: ['document:keydown', ['$event']]
@@ -1940,7 +2053,9 @@ class LibsUiGridLayoutDragController {
1940
2053
  * Gọi ở `dragover` của khung chứa mặt phẳng. Trả ghost để nơi dùng vẽ khung xem trước, đồng thời
1941
2054
  * tự bật ô li của lưới đang được kéo tới.
1942
2055
  */
1943
- onDragOver(event, container, root, totalCols) {
2056
+ onDragOver(event, container, root, totalCols,
2057
+ // Chiều cao một hàng lưới (pixel) để khung xem trước đúng cỡ; không truyền thì giữ mặc định cũ.
2058
+ rowStep) {
1944
2059
  event.preventDefault();
1945
2060
  if (event.dataTransfer) {
1946
2061
  event.dataTransfer.dropEffect = 'copy';
@@ -1949,7 +2064,7 @@ class LibsUiGridLayoutDragController {
1949
2064
  return undefined;
1950
2065
  }
1951
2066
  this.showRootGrid(container);
1952
- const ghost = calculateDropGhost(event, container, root, this.payload, totalCols);
2067
+ const ghost = calculateDropGhost(event, container, root, this.payload, totalCols, rowStep);
1953
2068
  if (!ghost?.isInsideGroup || !ghost.hoveredGridster) {
1954
2069
  this.cleanupHoveredGroup();
1955
2070
  return ghost;