@toolbox-web/grid-angular 0.13.0 → 0.13.1

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.
@@ -1,5 +1,5 @@
1
- import { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';
2
1
  import { registerFeature } from '@toolbox-web/grid-angular';
2
+ import { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';
3
3
 
4
4
  /**
5
5
  * Clipboard feature for @toolbox-web/grid-angular
@@ -1 +1 @@
1
- {"version":3,"file":"toolbox-web-grid-angular-features-clipboard.mjs","sources":["../../../../libs/grid-angular/features/clipboard/src/index.ts","../../../../libs/grid-angular/features/clipboard/src/toolbox-web-grid-angular-features-clipboard.ts"],"sourcesContent":["/**\n * Clipboard feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `clipboard` input on Grid directive.\n * Requires selection feature to be enabled.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/selection';\n * import '@toolbox-web/grid-angular/features/clipboard';\n *\n * <tbw-grid [selection]=\"'range'\" [clipboard]=\"true\" />\n * ```\n *\n * @packageDocumentation\n */\n\nimport { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';\nimport { registerFeature } from '@toolbox-web/grid-angular';\n\nregisterFeature('clipboard', (config) => {\n if (config === true) {\n return new ClipboardPlugin();\n }\n return new ClipboardPlugin(config ?? undefined);\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;AAeG;AAKH,eAAe,CAAC,WAAW,EAAE,CAAC,MAAM,KAAI;AACtC,IAAA,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,IAAI,eAAe,EAAE;IAC9B;AACA,IAAA,OAAO,IAAI,eAAe,CAAC,MAAM,IAAI,SAAS,CAAC;AACjD,CAAC,CAAC;;ACzBF;;AAEG"}
1
+ {"version":3,"file":"toolbox-web-grid-angular-features-clipboard.mjs","sources":["../../../../libs/grid-angular/features/clipboard/src/index.ts","../../../../libs/grid-angular/features/clipboard/src/toolbox-web-grid-angular-features-clipboard.ts"],"sourcesContent":["/**\n * Clipboard feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `clipboard` input on Grid directive.\n * Requires selection feature to be enabled.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/selection';\n * import '@toolbox-web/grid-angular/features/clipboard';\n *\n * <tbw-grid [selection]=\"'range'\" [clipboard]=\"true\" />\n * ```\n *\n * @packageDocumentation\n */\n\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';\n\nregisterFeature('clipboard', (config) => {\n if (config === true) {\n return new ClipboardPlugin();\n }\n return new ClipboardPlugin(config ?? undefined);\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;AAeG;AAKH,eAAe,CAAC,WAAW,EAAE,CAAC,MAAM,KAAI;AACtC,IAAA,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,IAAI,eAAe,EAAE;IAC9B;AACA,IAAA,OAAO,IAAI,eAAe,CAAC,MAAM,IAAI,SAAS,CAAC;AACjD,CAAC,CAAC;;ACzBF;;AAEG"}
@@ -381,9 +381,20 @@ class GridFormArray {
381
381
  // Subscribe to valueChanges to sync grid rows when FormArray content changes.
382
382
  // Use startWith to immediately sync the current value.
383
383
  // Note: We use getRawValue() to include disabled controls.
384
+ //
385
+ // In grid mode, editors bind directly to FormControls, so every keystroke
386
+ // fires valueChanges. We skip the sync when an editor input is focused to
387
+ // prevent destroying/recreating editors mid-edit (which orphans overlays
388
+ // like mat-autocomplete/mat-select panels and causes focus loss).
384
389
  this.valueChangesSubscription = formArray.valueChanges
385
390
  .pipe(startWith(formArray.getRawValue()), takeUntilDestroyed(this.destroyRef))
386
391
  .subscribe(() => {
392
+ // Skip sync while an editor is actively focused in the grid.
393
+ // The FormArray already has the latest values via its own controls;
394
+ // re-setting grid.rows would create new object references and trigger
395
+ // an unnecessary render cycle.
396
+ if (this.#isEditorFocused(grid))
397
+ return;
387
398
  grid.rows = formArray.getRawValue();
388
399
  });
389
400
  }, ...(ngDevMode ? [{ debugName: "syncFormArrayToGrid" }] : []));
@@ -458,6 +469,20 @@ class GridFormArray {
458
469
  const editingPlugin = grid.getPluginByName?.('editing');
459
470
  return editingPlugin?.config?.mode === 'grid';
460
471
  }
472
+ /**
473
+ * Checks if a focusable editor element inside the grid currently has focus.
474
+ * Used to skip valueChanges → grid.rows sync while a user is actively editing,
475
+ * preventing editor destruction (which orphans overlay panels like autocomplete/select).
476
+ */
477
+ #isEditorFocused(grid) {
478
+ if (!this.#isGridMode())
479
+ return false;
480
+ const active = document.activeElement;
481
+ if (!active)
482
+ return false;
483
+ // Check if the focused element is inside the grid
484
+ return grid.contains(active) && active.closest('.editing') != null;
485
+ }
461
486
  /**
462
487
  * Sets up reactive validation syncing for grid mode.
463
488
  * Subscribes to statusChanges on all FormControls to update validation state in real-time.
@@ -552,6 +577,8 @@ class GridFormArray {
552
577
  */
553
578
  #storeFormContext(grid) {
554
579
  const getRowFormGroup = (rowIndex) => this.#getRowFormGroup(rowIndex);
580
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
581
+ const self = this;
555
582
  const context = {
556
583
  getRow: (rowIndex) => {
557
584
  const formArray = this.formArray();
@@ -571,7 +598,9 @@ class GridFormArray {
571
598
  getValue: () => {
572
599
  return this.formArray().getRawValue();
573
600
  },
574
- hasFormGroups: this.#isFormArrayOfFormGroups(),
601
+ get hasFormGroups() {
602
+ return self.#isFormArrayOfFormGroups();
603
+ },
575
604
  getControl: (rowIndex, field) => {
576
605
  const rowFormGroup = getRowFormGroup(rowIndex);
577
606
  if (!rowFormGroup)
@@ -1340,6 +1369,10 @@ class GridAdapter {
1340
1369
  viewContainerRef;
1341
1370
  viewRefs = [];
1342
1371
  componentRefs = [];
1372
+ /** Editor-specific view refs tracked separately for per-cell cleanup via releaseCell. */
1373
+ editorViewRefs = [];
1374
+ /** Editor-specific component refs tracked separately for per-cell cleanup via releaseCell. */
1375
+ editorComponentRefs = [];
1343
1376
  typeRegistry = null;
1344
1377
  constructor(injector, appRef, viewContainerRef) {
1345
1378
  this.injector = injector;
@@ -1498,8 +1531,24 @@ class GridAdapter {
1498
1531
  this.viewRefs.push(viewRef);
1499
1532
  // Trigger change detection
1500
1533
  viewRef.detectChanges();
1501
- // Get the first root node (the component's host element)
1502
- const rootNode = viewRef.rootNodes[0];
1534
+ // Find the first Element root node. When *tbwRenderer is used on <ng-container>,
1535
+ // rootNodes[0] is a comment node (<!--ng-container-->); the actual content is in
1536
+ // subsequent root nodes. For single-element templates, rootNodes[0] IS the element.
1537
+ let rootNode = viewRef.rootNodes[0];
1538
+ const elementNodes = viewRef.rootNodes.filter((n) => n.nodeType === Node.ELEMENT_NODE);
1539
+ if (elementNodes.length === 1) {
1540
+ // Single element among the root nodes — use it directly
1541
+ rootNode = elementNodes[0];
1542
+ }
1543
+ else if (elementNodes.length > 1) {
1544
+ // Multiple element nodes — wrap in a span container so all are rendered
1545
+ const wrapper = document.createElement('span');
1546
+ wrapper.style.display = 'contents';
1547
+ for (const node of viewRef.rootNodes) {
1548
+ wrapper.appendChild(node);
1549
+ }
1550
+ rootNode = wrapper;
1551
+ }
1503
1552
  // Cache for reuse on scroll recycles
1504
1553
  if (cellEl) {
1505
1554
  cellCache.set(cellEl, { viewRef, rootNode });
@@ -1580,7 +1629,8 @@ class GridAdapter {
1580
1629
  };
1581
1630
  // Create embedded view from template
1582
1631
  const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
1583
- this.viewRefs.push(viewRef);
1632
+ // Track in editor-specific array for per-cell cleanup via releaseCell
1633
+ this.editorViewRefs.push(viewRef);
1584
1634
  // Trigger change detection
1585
1635
  viewRef.detectChanges();
1586
1636
  // Get the first root node (the component's host element)
@@ -1798,7 +1848,7 @@ class GridAdapter {
1798
1848
  * Shared logic between renderer and editor component creation.
1799
1849
  * @internal
1800
1850
  */
1801
- mountComponent(componentClass, inputs) {
1851
+ mountComponent(componentClass, inputs, isEditor = false) {
1802
1852
  // Create a host element for the component
1803
1853
  const hostElement = document.createElement('span');
1804
1854
  hostElement.style.display = 'contents';
@@ -1811,7 +1861,13 @@ class GridAdapter {
1811
1861
  this.setComponentInputs(componentRef, inputs);
1812
1862
  // Attach to app for change detection
1813
1863
  this.appRef.attachView(componentRef.hostView);
1814
- this.componentRefs.push(componentRef);
1864
+ // Track in editor-specific array for per-cell cleanup, or general array for renderers
1865
+ if (isEditor) {
1866
+ this.editorComponentRefs.push(componentRef);
1867
+ }
1868
+ else {
1869
+ this.componentRefs.push(componentRef);
1870
+ }
1815
1871
  // Trigger change detection
1816
1872
  componentRef.changeDetectorRef.detectChanges();
1817
1873
  return { hostElement, componentRef };
@@ -1878,7 +1934,7 @@ class GridAdapter {
1878
1934
  value: ctx.value,
1879
1935
  row: ctx.row,
1880
1936
  column: ctx.column,
1881
- });
1937
+ }, true);
1882
1938
  this.wireEditorCallbacks(hostElement, componentRef, (value) => ctx.commit(value), () => ctx.cancel());
1883
1939
  // Auto-update editor when value changes externally (e.g., via updateRow cascade).
1884
1940
  // This keeps Angular component editors in sync without manual DOM patching.
@@ -1959,6 +2015,33 @@ class GridAdapter {
1959
2015
  }
1960
2016
  }
1961
2017
  }
2018
+ /**
2019
+ * Called when a cell's content is about to be wiped (e.g., exiting edit mode,
2020
+ * scroll-recycling a row, or rebuilding a row).
2021
+ *
2022
+ * Destroys any editor embedded views or component refs whose DOM is
2023
+ * inside the given cell element. This prevents memory leaks from
2024
+ * orphaned Angular views that would otherwise stay in the change
2025
+ * detection tree indefinitely.
2026
+ */
2027
+ releaseCell(cellEl) {
2028
+ // Release editor embedded views whose root nodes are inside this cell
2029
+ for (let i = this.editorViewRefs.length - 1; i >= 0; i--) {
2030
+ const ref = this.editorViewRefs[i];
2031
+ if (ref.rootNodes.some((n) => cellEl.contains(n))) {
2032
+ ref.destroy();
2033
+ this.editorViewRefs.splice(i, 1);
2034
+ }
2035
+ }
2036
+ // Release editor component refs whose host element is inside this cell
2037
+ for (let i = this.editorComponentRefs.length - 1; i >= 0; i--) {
2038
+ const ref = this.editorComponentRefs[i];
2039
+ if (cellEl.contains(ref.location.nativeElement)) {
2040
+ ref.destroy();
2041
+ this.editorComponentRefs.splice(i, 1);
2042
+ }
2043
+ }
2044
+ }
1962
2045
  /**
1963
2046
  * Clean up all view references and component references.
1964
2047
  * Call this when your app/component is destroyed.
@@ -1966,8 +2049,12 @@ class GridAdapter {
1966
2049
  destroy() {
1967
2050
  this.viewRefs.forEach((ref) => ref.destroy());
1968
2051
  this.viewRefs = [];
2052
+ this.editorViewRefs.forEach((ref) => ref.destroy());
2053
+ this.editorViewRefs = [];
1969
2054
  this.componentRefs.forEach((ref) => ref.destroy());
1970
2055
  this.componentRefs = [];
2056
+ this.editorComponentRefs.forEach((ref) => ref.destroy());
2057
+ this.editorComponentRefs = [];
1971
2058
  }
1972
2059
  }
1973
2060
  /**
@@ -2975,10 +3062,14 @@ const OVERLAY_STYLES = /* css */ `
2975
3062
  right: anchor(right);
2976
3063
  position-try-fallbacks: flip-block;
2977
3064
  }
2978
- .tbw-overlay-panel[data-pos="over-left"] {
3065
+ .tbw-overlay-panel[data-pos="over-top-left"] {
2979
3066
  top: anchor(top);
2980
3067
  left: anchor(left);
2981
3068
  }
3069
+ .tbw-overlay-panel[data-pos="over-bottom-left"] {
3070
+ bottom: anchor(bottom);
3071
+ left: anchor(left);
3072
+ }
2982
3073
  }
2983
3074
  `;
2984
3075
  function ensureOverlayStyles() {
@@ -3334,11 +3425,16 @@ class BaseOverlayEditor extends BaseGridEditor {
3334
3425
  top = cellRect.top - panelRect.height;
3335
3426
  break;
3336
3427
  }
3337
- case 'over-left': {
3428
+ case 'over-top-left': {
3338
3429
  top = cellRect.top;
3339
3430
  left = cellRect.left;
3340
3431
  break;
3341
3432
  }
3433
+ case 'over-bottom-left': {
3434
+ top = cellRect.bottom - panelRect.height;
3435
+ left = cellRect.left;
3436
+ break;
3437
+ }
3342
3438
  case 'below':
3343
3439
  default: {
3344
3440
  top = cellRect.bottom;