@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.
- package/fesm2022/toolbox-web-grid-angular-features-clipboard.mjs +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-clipboard.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular.mjs +105 -9
- package/fesm2022/toolbox-web-grid-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/toolbox-web-grid-angular.d.ts +17 -2
- package/types/toolbox-web-grid-angular.d.ts.map +1 -1
|
@@ -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 {
|
|
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
|
|
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
|
-
//
|
|
1502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|