@libs-ui/services-grid-layout 0.2.357-21 → 0.2.357-22

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.
@@ -384,6 +384,79 @@ const calculateDropGhost = (event, canvasContainer, root, payload, totalCols = 2
384
384
  hoveredGridster: rootGridster ?? undefined,
385
385
  };
386
386
  };
387
+ /** Tìm khối theo id trong cây, đi sâu vào mọi container. */
388
+ const findNodeRecursive = (root, id) => {
389
+ if (root.id === id) {
390
+ return root;
391
+ }
392
+ for (const child of root.children ?? []) {
393
+ const found = findNodeRecursive(child, id);
394
+ if (found) {
395
+ return found;
396
+ }
397
+ }
398
+ return undefined;
399
+ };
400
+ /** Tìm khối CHA trực tiếp của một khối, đi sâu vào mọi container. */
401
+ const findParentRecursive = (root, id) => {
402
+ for (const child of root.children ?? []) {
403
+ if (child.id === id) {
404
+ return root;
405
+ }
406
+ const found = findParentRecursive(child, id);
407
+ if (found) {
408
+ return found;
409
+ }
410
+ }
411
+ return undefined;
412
+ };
413
+ /**
414
+ * Thả khối vào đúng ô mà ghost đang chỉ, đẩy các khối va chạm xuống dưới.
415
+ *
416
+ * Ghost đang nằm trong một container (`isInsideGroup`) thì thả vào ruột container đó; không tìm thấy
417
+ * container thì trả `false` chứ KHÔNG rơi về lưới gốc — thả nhầm cấp khó nhận ra hơn là không thả.
418
+ */
419
+ const dropNodeAtCoordinates = (root, ghost, newNode) => {
420
+ const nodeToAdd = {
421
+ ...newNode,
422
+ id: newNode.id ?? uuid(),
423
+ x: ghost.col,
424
+ y: ghost.row,
425
+ cols: ghost.cols,
426
+ rows: ghost.rows,
427
+ };
428
+ if (ghost.isInsideGroup && ghost.targetParentId) {
429
+ const parentNode = findNodeRecursive(root, ghost.targetParentId);
430
+ if (!parentNode) {
431
+ return false;
432
+ }
433
+ parentNode.children = parentNode.children ?? [];
434
+ resolveCollisionsAndInsert(parentNode.children, nodeToAdd);
435
+ return true;
436
+ }
437
+ root.children = root.children ?? [];
438
+ resolveCollisionsAndInsert(root.children, nodeToAdd);
439
+ return true;
440
+ };
441
+ /** Khối có phải CONTAINER không — tức có khối con bên trong. */
442
+ const isContainerNode = (node) => (node.children?.length ?? 0) > 0;
443
+ /** Khối có nằm LỒNG bên trong một container không (không phải con trực tiếp của gốc). */
444
+ const isNodeNestedInside = (root, nodeId) => {
445
+ if (!root?.children) {
446
+ return false;
447
+ }
448
+ return !root.children.some((child) => child.id === nodeId);
449
+ };
450
+ /**
451
+ * Có được đổi khối đơn ↔ container không.
452
+ *
453
+ * 🔴 Ba hàm cờ này PHẢI là nguồn duy nhất cho cả thanh công cụ của thư viện lẫn menu do nơi dùng tự
454
+ * dựng. Trước đây nơi dùng phải chép lại quy tắc để dựng menu trước lúc render, và hai bản lệch nhau
455
+ * mỗi khi thư viện đổi trần lồng cấp mà không có gì báo.
456
+ */
457
+ const canToggleTypeNode = (root, node) => isContainerNode(node) || !isNodeNestedInside(root, node.id);
458
+ /** Có được thêm khối vào BÊN TRONG khối này không. */
459
+ const canAddInsideNode = (root, node) => isContainerNode(node) && !isNodeNestedInside(root, node.id);
387
460
 
388
461
  /**
389
462
  * Thao tác trên cây khối: tìm, thêm, xoá, dồn khít, chặn lồng cấp quá sâu.
@@ -598,6 +671,13 @@ class LibsUiGridLayoutService {
598
671
  treeService = new LibsUiGridLayoutTreeService();
599
672
  /** Lượt tải component đang chờ, theo id khối — để huỷ khi khối bị gỡ giữa chừng. */
600
673
  pendingLoads = new Map();
674
+ /**
675
+ * Bộ theo dõi bề rộng của từng lưới LỒNG, để ngắt khi service bị huỷ.
676
+ *
677
+ * 🔴 Giữ tham chiếu là BẮT BUỘC: `ResizeObserver` không tự tắt theo vòng đời component. Không dọn
678
+ * thì mỗi lần nạp lại bố cục (đổi tab) lại chồng thêm một bộ, và chúng vẫn chạy trên DOM đã gỡ.
679
+ */
680
+ nestedResizeObservers = new Set();
601
681
  constructor() {
602
682
  // 🔴 Service khai trong `providers` của component nên Angular huỷ nó cùng component — thư viện
603
683
  // tự dọn ở đây, nơi dùng KHÔNG phải gọi `destroy()` trong `ngOnDestroy` (người dùng chốt
@@ -721,6 +801,8 @@ class LibsUiGridLayoutService {
721
801
  scale: this.zoomLevel(),
722
802
  ...(config.cols ? { minCols: config.cols, maxCols: config.cols, maxItemCols: config.cols } : {}),
723
803
  ...(config.rowHeight && depth === 0 ? { fixedRowHeight: config.rowHeight } : {}),
804
+ ...(depth === 0 && config.minRows !== undefined ? { minRows: config.minRows } : {}),
805
+ ...(depth > 0 && config.nestedMinRows !== undefined ? { minRows: config.nestedMinRows } : {}),
724
806
  // Khe và chiều cao hàng khác nhau theo cấp: lưới trang thoáng, lưới lồng khít.
725
807
  ...(depth === 0 && config.margin !== undefined ? { margin: config.margin } : {}),
726
808
  ...(depth > 0 && config.nestedMargin !== undefined ? { margin: config.nestedMargin } : {}),
@@ -744,6 +826,7 @@ class LibsUiGridLayoutService {
744
826
  // 28/08/2026: "bật chồng khối nhưng khi kéo thì các khối vẫn di chuyển theo").
745
827
  // Cho chồng thì phải TẮT đẩy khối, nếu không khối bị dạt đi trước khi kịp đè lên nhau.
746
828
  ...(canOverlap ? { pushItems: false, pushResizeItems: false, swap: false, swapWhileDragging: false } : {}),
829
+ ...this.buildNestedResizeHooks(base, depth),
747
830
  itemChangeCallback: (item) => {
748
831
  // 🔴 Khối vừa thao tác phải lên LỚP TRÊN CÙNG.
749
832
  // `layerIndex` gán một lần theo thứ tự cây lúc khởi tạo, nên khối kéo lên chỉ nổi TRONG LÚC
@@ -758,6 +841,60 @@ class LibsUiGridLayoutService {
758
841
  },
759
842
  };
760
843
  }
844
+ /**
845
+ * Bắt lưới LỒNG đo lại NGAY khi khung chứa nó đổi bề rộng — dùng lúc người dùng kéo mép khối cha.
846
+ *
847
+ * Gridster có sẵn đường tự đo (`resize$`), nhưng luồng đó là `switchMap(() => timer(100))`: mỗi
848
+ * lần phát lại huỷ timer trước, mà kéo chuột bắn dày hơn 100ms nên timer không bao giờ tới đích.
849
+ * Đo được (11/09/2026): kéo mép nhóm thì lưới con co theo cha từng khung hình (234 → 384 px) còn
850
+ * THẺ con đứng im ở 106 px suốt cú kéo, chỉ nhảy đúng sau khi thả tay.
851
+ *
852
+ * Giữ CẢ HAI đường: `itemResizeCallback` bắn đồng bộ ngay khi gridster ghi bề rộng mới lên khối
853
+ * (sớm hơn `ResizeObserver` một khung hình) lo độ tức thì; `ResizeObserver` lo những lần khung đổi
854
+ * vì lý do khác — đổi zoom, thu gọn cột bên trái.
855
+ *
856
+ * Tắt bằng `config.autoResizeNestedGrids: false`.
857
+ */
858
+ buildNestedResizeHooks(base, depth) {
859
+ if (this.config?.autoResizeNestedGrids === false) {
860
+ return {};
861
+ }
862
+ const gocItemResize = base.itemResizeCallback;
863
+ const gocInit = base.initCallback;
864
+ return {
865
+ itemResizeCallback: (item, itemComponent) => {
866
+ gocItemResize?.(item, itemComponent);
867
+ const el = itemComponent?.el;
868
+ el?.querySelectorAll('gridster').forEach((nested) => this.resizeGridsterElement(nested));
869
+ },
870
+ ...(depth > 0
871
+ ? { initCallback: (gridster) => this.registerNestedGrid(gridster, gocInit) }
872
+ : {}),
873
+ };
874
+ }
875
+ /** Gọi `resize()` của một thẻ `<gridster>` qua chính instance mà nó tự gắn lên phần tử. */
876
+ resizeGridsterElement(nested) {
877
+ const giuInstance = nested;
878
+ giuInstance.__gridsterInstance?.options?.api?.resize?.();
879
+ }
880
+ /**
881
+ * Gắn instance lên chính phần tử `<gridster>` rồi theo dõi bề rộng của nó.
882
+ *
883
+ * Phải gắn ngược lên DOM vì `itemResizeCallback` của khối CHA chỉ cầm được `HTMLElement`, không có
884
+ * đường nào khác đi từ đó về instance của lưới lồng bên trong.
885
+ */
886
+ registerNestedGrid(gridster, goc) {
887
+ goc?.(gridster);
888
+ const instance = gridster;
889
+ const el = instance?.el;
890
+ if (!el || typeof ResizeObserver === 'undefined') {
891
+ return;
892
+ }
893
+ el.__gridsterInstance = instance;
894
+ const theoDoi = new ResizeObserver(() => instance?.options?.api?.resize?.());
895
+ theoDoi.observe(el);
896
+ this.nestedResizeObservers.add(theoDoi);
897
+ }
761
898
  /**
762
899
  *
763
900
  * Gắn component của nơi dùng lên một khối.
@@ -818,8 +955,12 @@ class LibsUiGridLayoutService {
818
955
  for (const ref of this.componentRefs.values()) {
819
956
  ref.destroy();
820
957
  }
958
+ for (const observer of this.nestedResizeObservers) {
959
+ observer.disconnect();
960
+ }
821
961
  this.pendingLoads.clear();
822
962
  this.componentRefs.clear();
963
+ this.nestedResizeObservers.clear();
823
964
  this.config = undefined;
824
965
  }
825
966
  // ─────────────────────────────────────────────────────────────────────────────────────────
@@ -981,8 +1122,67 @@ class LibsUiGridLayoutService {
981
1122
  }
982
1123
  return done;
983
1124
  }
1125
+ /**
1126
+ * Số cột thật của lưới cấp trang.
1127
+ *
1128
+ * 🔴 Mặc định phải TRÙNG `minCols/maxCols` của `gridLayoutRootConfig()`. Trước đây chỗ này trả 24
1129
+ * trong khi lưới vẽ 12: nơi dùng không truyền `cols` thì va chạm, `toContainer` và `compact` tính
1130
+ * theo 24 cột còn màn hình chỉ có 12 — khối rơi ra ngoài mép phải mà không ai hiểu vì sao.
1131
+ */
984
1132
  get totalColumns() {
985
- return this.config?.cols ?? 24;
1133
+ return this.config?.cols ?? gridLayoutRootConfig().maxCols ?? 12;
1134
+ }
1135
+ /**
1136
+ * Đổi số cột / số hàng của một khối rồi vẽ lại.
1137
+ *
1138
+ * Kéo `cols` rộng quá mép phải thì dịch khối sang trái cho vừa, thay vì để nó tràn ra ngoài lưới.
1139
+ */
1140
+ resizeBlock(id, size) {
1141
+ const node = this.findBlock(id);
1142
+ if (!node) {
1143
+ return false;
1144
+ }
1145
+ if (size.cols !== undefined) {
1146
+ node.cols = size.cols;
1147
+ if (node.x + size.cols > this.totalColumns) {
1148
+ node.x = Math.max(0, this.totalColumns - size.cols);
1149
+ }
1150
+ }
1151
+ if (size.rows !== undefined) {
1152
+ node.rows = size.rows;
1153
+ }
1154
+ this.refresh();
1155
+ return true;
1156
+ }
1157
+ /**
1158
+ * Nhân bản một khối — đặt kế bên phải nếu còn chỗ, không thì xuống dòng dưới.
1159
+ *
1160
+ * `transformData` để nơi dùng đổi phần dữ liệu riêng của mình (vd nối thêm "(Bản sao)" vào tiêu
1161
+ * đề); thư viện không tự đoán trường nào là tiêu đề.
1162
+ */
1163
+ duplicateBlock(id, transformData) {
1164
+ const root = this.rootNode();
1165
+ const node = root ? findNodeRecursive(root, id) : undefined;
1166
+ if (!root || !node) {
1167
+ return undefined;
1168
+ }
1169
+ const parent = findParentRecursive(root, id);
1170
+ const isNested = Boolean(parent && parent.id !== root.id);
1171
+ const clonedData = cloneDeep(node.data);
1172
+ let targetX = node.x + node.cols;
1173
+ let targetY = node.y;
1174
+ if (targetX + node.cols > this.totalColumns) {
1175
+ targetX = 0;
1176
+ targetY = node.y + node.rows;
1177
+ }
1178
+ return this.addBlock({
1179
+ x: targetX,
1180
+ y: targetY,
1181
+ cols: node.cols,
1182
+ rows: node.rows,
1183
+ data: transformData ? transformData(clonedData) : clonedData,
1184
+ getComponentOutlet: node.getComponentOutlet,
1185
+ }, isNested ? parent?.id : undefined);
986
1186
  }
987
1187
  /** Biến khối đơn thành CONTAINER — nội dung cũ thành khối con đầu tiên, không mất dữ liệu. */
988
1188
  toContainer(id) {
@@ -1363,6 +1563,9 @@ class LibsUiGridLayoutCanvasComponent {
1363
1563
  this.service.FunctionsControl.setZoom(val, this.storageKey());
1364
1564
  this.syncZoomOut();
1365
1565
  },
1566
+ // Vùng cuộn của mặt phẳng — nơi dùng cần nó để tự cuộn tới một khối, hoặc đo toạ độ khi kéo.
1567
+ // Không mở ở đây thì nơi dùng phải dò bằng tên lớp CSS nội bộ, và tên đó đổi lúc nào không hay.
1568
+ scrollArea: () => this.scrollAreaRef()?.nativeElement,
1366
1569
  };
1367
1570
  }
1368
1571
  handlerZoomIn() {
@@ -1684,6 +1887,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
1684
1887
  args: ['document:click', ['$event']]
1685
1888
  }] } });
1686
1889
 
1890
+ /** Lớp CSS bật ô li của một lưới trong lúc người dùng đang kéo khối vào. */
1891
+ const LOP_HIEN_O_LI = 'display-grid';
1892
+ /**
1893
+ * Giữ trạng thái một lượt kéo khối từ palette bên ngoài vào mặt phẳng.
1894
+ *
1895
+ * Việc nó lo: bật ô li của lưới đang được kéo tới (lưới gốc, và lưới của container khi con trỏ đi
1896
+ * vào trong), tắt đúng lúc, và trả về ghost để nơi dùng vẽ khung xem trước.
1897
+ *
1898
+ * 🔴 Tự bật/tắt ô li bằng tay ở nơi dùng là chỗ rất dễ sót: kéo ra khỏi container rồi mà quên tắt
1899
+ * thì ô li của nó nằm lại trên màn, kéo sang lưới khác lại chồng thêm một lớp nữa.
1900
+ */
1901
+ class LibsUiGridLayoutDragController {
1902
+ payload;
1903
+ hoveredElement;
1904
+ rootGridsterElement;
1905
+ /** Payload của khối đang được kéo; `undefined` khi không có lượt kéo nào. */
1906
+ get ActivePayload() {
1907
+ return this.payload;
1908
+ }
1909
+ /** Gọi ở `dragstart` của thẻ trong palette. */
1910
+ onDragStart(payload) {
1911
+ this.payload = payload;
1912
+ }
1913
+ /** Gọi ở `dragend` — dọn sạch mọi ô li đang bật. */
1914
+ onDragEnd(container) {
1915
+ this.payload = undefined;
1916
+ this.cleanupAll(container);
1917
+ }
1918
+ /** Tắt ô li của container đang hover. */
1919
+ cleanupHoveredGroup() {
1920
+ if (!this.hoveredElement) {
1921
+ return;
1922
+ }
1923
+ this.hoveredElement.classList.remove(LOP_HIEN_O_LI);
1924
+ this.hoveredElement = undefined;
1925
+ }
1926
+ /** Tắt ô li của lưới gốc, và quét sạch lớp còn sót trong khung chứa. */
1927
+ cleanupRootGrid(container) {
1928
+ if (this.rootGridsterElement) {
1929
+ this.rootGridsterElement.classList.remove(LOP_HIEN_O_LI);
1930
+ this.rootGridsterElement = undefined;
1931
+ }
1932
+ container?.querySelectorAll(`gridster.${LOP_HIEN_O_LI}`).forEach((el) => el.classList.remove(LOP_HIEN_O_LI));
1933
+ }
1934
+ /** Tắt toàn bộ ô li đang bật. */
1935
+ cleanupAll(container) {
1936
+ this.cleanupHoveredGroup();
1937
+ this.cleanupRootGrid(container);
1938
+ }
1939
+ /**
1940
+ * 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
+ * tự bật ô li của lưới đang được kéo tới.
1942
+ */
1943
+ onDragOver(event, container, root, totalCols) {
1944
+ event.preventDefault();
1945
+ if (event.dataTransfer) {
1946
+ event.dataTransfer.dropEffect = 'copy';
1947
+ }
1948
+ if (!container || !root || !this.payload) {
1949
+ return undefined;
1950
+ }
1951
+ this.showRootGrid(container);
1952
+ const ghost = calculateDropGhost(event, container, root, this.payload, totalCols);
1953
+ if (!ghost?.isInsideGroup || !ghost.hoveredGridster) {
1954
+ this.cleanupHoveredGroup();
1955
+ return ghost;
1956
+ }
1957
+ if (this.hoveredElement !== ghost.hoveredGridster) {
1958
+ this.cleanupHoveredGroup();
1959
+ ghost.hoveredGridster.classList.add(LOP_HIEN_O_LI);
1960
+ this.hoveredElement = ghost.hoveredGridster;
1961
+ }
1962
+ return ghost;
1963
+ }
1964
+ /** Bật ô li của lưới CẤP TRANG trong suốt lượt kéo, tắt lưới cũ nếu khung chứa đã đổi. */
1965
+ showRootGrid(container) {
1966
+ const rootGridster = container.querySelector('gridster') ?? undefined;
1967
+ if (!rootGridster) {
1968
+ return;
1969
+ }
1970
+ if (this.rootGridsterElement && this.rootGridsterElement !== rootGridster) {
1971
+ this.rootGridsterElement.classList.remove(LOP_HIEN_O_LI);
1972
+ }
1973
+ rootGridster.classList.add(LOP_HIEN_O_LI);
1974
+ this.rootGridsterElement = rootGridster;
1975
+ }
1976
+ }
1977
+
1687
1978
  // Bề mặt CÔNG KHAI — chỉ những gì nơi dùng thật sự cần.
1688
1979
  //
1689
1980
  // `LibsUiGridLayoutTreeService` CỐ Ý không xuất: mọi thao tác trên cây đã có mặt trên
@@ -1695,5 +1986,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
1695
1986
  * Generated bundle index. Do not edit.
1696
1987
  */
1697
1988
 
1698
- export { LibsUiGridLayoutCanvasComponent, LibsUiGridLayoutService, calculateDropGhost, findHoveredGroup, findSnapPositionInsideGroup, isRegionFree, parseDropPayload, resolveCollisionsAndInsert };
1989
+ export { LibsUiGridLayoutCanvasComponent, LibsUiGridLayoutDragController, LibsUiGridLayoutService, calculateDropGhost, canAddInsideNode, canToggleTypeNode, dropNodeAtCoordinates, findHoveredGroup, findNodeRecursive, findParentRecursive, findSnapPositionInsideGroup, isContainerNode, isNodeNestedInside, isRegionFree, parseDropPayload, resolveCollisionsAndInsert };
1699
1990
  //# sourceMappingURL=libs-ui-services-grid-layout.mjs.map