@toolbox-web/grid-angular 0.12.1 → 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/README.md +98 -0
- 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-features-export.mjs +16 -1
- package/fesm2022/toolbox-web-grid-angular-features-export.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-filtering.mjs +16 -1
- package/fesm2022/toolbox-web-grid-angular-features-filtering.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-pinned-columns.mjs +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-pinned-columns.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-print.mjs +16 -1
- package/fesm2022/toolbox-web-grid-angular-features-print.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs +35 -1
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular-features-undo-redo.mjs +63 -5
- package/fesm2022/toolbox-web-grid-angular-features-undo-redo.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular.mjs +1002 -14
- package/fesm2022/toolbox-web-grid-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/toolbox-web-grid-angular-features-undo-redo.d.ts +24 -4
- package/types/toolbox-web-grid-angular-features-undo-redo.d.ts.map +1 -1
- package/types/toolbox-web-grid-angular.d.ts +578 -9
- package/types/toolbox-web-grid-angular.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -822,6 +822,85 @@ if (context?.hasFormGroups) {
|
|
|
822
822
|
| `getRowErrors(idx)` | `Record<string, unknown> \| null` | Aggregated errors from all controls, or null |
|
|
823
823
|
| `getRowFormGroup(idx)` | `FormGroup \| undefined` | The FormGroup for the row |
|
|
824
824
|
|
|
825
|
+
## Base Classes for Custom Editors & Filters
|
|
826
|
+
|
|
827
|
+
The adapter provides base classes that eliminate boilerplate when building custom editors and filter panels.
|
|
828
|
+
|
|
829
|
+
| Base Class | Extends | Purpose |
|
|
830
|
+
| --- | --- | --- |
|
|
831
|
+
| `BaseGridEditor` | — | Common inputs (`value`, `row`, `column`, `control`), outputs (`commit`, `cancel`), validation helpers |
|
|
832
|
+
| `BaseGridEditorCVA` | `BaseGridEditor` | Adds `ControlValueAccessor` for dual grid + standalone form use |
|
|
833
|
+
| `BaseOverlayEditor` | `BaseGridEditor` | Floating overlay panel with CSS Anchor Positioning, focus gating, click-outside detection |
|
|
834
|
+
| `BaseFilterPanel` | — | Ready-made `params` input for `FilteringPlugin`, with `applyAndClose()` / `clearAndClose()` helpers |
|
|
835
|
+
|
|
836
|
+
### BaseOverlayEditor Example
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
|
|
840
|
+
import { BaseOverlayEditor } from '@toolbox-web/grid-angular';
|
|
841
|
+
|
|
842
|
+
@Component({
|
|
843
|
+
selector: 'app-date-editor',
|
|
844
|
+
template: `
|
|
845
|
+
<input
|
|
846
|
+
#inlineInput
|
|
847
|
+
readonly
|
|
848
|
+
[value]="currentValue()"
|
|
849
|
+
(click)="onInlineClick()"
|
|
850
|
+
(keydown)="onInlineKeydown($event)"
|
|
851
|
+
/>
|
|
852
|
+
<div #panel class="tbw-overlay-panel" style="width: 280px; padding: 12px;">
|
|
853
|
+
<input type="date" [value]="currentValue()" (change)="selectAndClose($event.target.value)" />
|
|
854
|
+
<button (click)="hideOverlay()">Cancel</button>
|
|
855
|
+
</div>
|
|
856
|
+
`
|
|
857
|
+
})
|
|
858
|
+
export class DateEditorComponent extends BaseOverlayEditor<MyRow, string> implements AfterViewInit {
|
|
859
|
+
@ViewChild('panel') panelRef!: ElementRef<HTMLElement>;
|
|
860
|
+
@ViewChild('inlineInput') inputRef!: ElementRef<HTMLInputElement>;
|
|
861
|
+
|
|
862
|
+
protected override overlayPosition = 'below' as const;
|
|
863
|
+
|
|
864
|
+
ngAfterViewInit(): void {
|
|
865
|
+
this.initOverlay(this.panelRef.nativeElement);
|
|
866
|
+
if (this.isCellFocused()) this.showOverlay();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
protected getInlineInput() { return this.inputRef?.nativeElement ?? null; }
|
|
870
|
+
protected onOverlayOutsideClick() { this.hideOverlay(); }
|
|
871
|
+
|
|
872
|
+
selectAndClose(date: string): void {
|
|
873
|
+
this.commitValue(date);
|
|
874
|
+
this.hideOverlay();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### BaseFilterPanel Example
|
|
880
|
+
|
|
881
|
+
```typescript
|
|
882
|
+
import { Component, ViewChild, ElementRef } from '@angular/core';
|
|
883
|
+
import { BaseFilterPanel } from '@toolbox-web/grid-angular';
|
|
884
|
+
|
|
885
|
+
@Component({
|
|
886
|
+
selector: 'app-text-filter',
|
|
887
|
+
template: `
|
|
888
|
+
<input #input (keydown.enter)="applyAndClose()" />
|
|
889
|
+
<button (click)="applyAndClose()">Apply</button>
|
|
890
|
+
<button (click)="clearAndClose()">Clear</button>
|
|
891
|
+
`
|
|
892
|
+
})
|
|
893
|
+
export class TextFilterComponent extends BaseFilterPanel {
|
|
894
|
+
@ViewChild('input') input!: ElementRef<HTMLInputElement>;
|
|
895
|
+
|
|
896
|
+
applyFilter(): void {
|
|
897
|
+
this.params().applyTextFilter('contains', this.input.nativeElement.value);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
> See the [full Base Classes documentation](https://toolboxjs.com/?path=/docs/grid-angular-base-classes--docs) for detailed API tables, all overlay positions, and CVA usage.
|
|
903
|
+
|
|
825
904
|
## API Reference
|
|
826
905
|
|
|
827
906
|
### Exported Directives
|
|
@@ -837,6 +916,15 @@ if (context?.hasFormGroups) {
|
|
|
837
916
|
| `GridDetailView` | `tbw-grid-detail` | Master-detail panel template |
|
|
838
917
|
| `GridToolPanel` | `tbw-grid-tool-panel` | Custom sidebar panel |
|
|
839
918
|
|
|
919
|
+
### Base Classes
|
|
920
|
+
|
|
921
|
+
| Class | Description |
|
|
922
|
+
| --- | --- |
|
|
923
|
+
| `BaseGridEditor<TRow, TValue>` | Base class for inline cell editors with validation helpers |
|
|
924
|
+
| `BaseGridEditorCVA<TRow, TValue>` | `BaseGridEditor` + `ControlValueAccessor` for dual grid/form editors |
|
|
925
|
+
| `BaseOverlayEditor<TRow, TValue>` | `BaseGridEditor` + floating overlay panel infrastructure |
|
|
926
|
+
| `BaseFilterPanel` | Base class for custom filter panels with `params` input |
|
|
927
|
+
|
|
840
928
|
### Type Registry
|
|
841
929
|
|
|
842
930
|
| Export | Description |
|
|
@@ -915,6 +1003,16 @@ import type {
|
|
|
915
1003
|
AngularTypeDefault,
|
|
916
1004
|
// Reactive Forms
|
|
917
1005
|
FormArrayContext,
|
|
1006
|
+
// Overlay position type
|
|
1007
|
+
OverlayPosition,
|
|
1008
|
+
} from '@toolbox-web/grid-angular';
|
|
1009
|
+
|
|
1010
|
+
// Base classes for custom editors and filter panels
|
|
1011
|
+
import {
|
|
1012
|
+
BaseGridEditor,
|
|
1013
|
+
BaseGridEditorCVA,
|
|
1014
|
+
BaseOverlayEditor,
|
|
1015
|
+
BaseFilterPanel,
|
|
918
1016
|
} from '@toolbox-web/grid-angular';
|
|
919
1017
|
|
|
920
1018
|
// Type guard for component class detection
|
|
@@ -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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject, ElementRef, signal } from '@angular/core';
|
|
1
|
+
import { inject, ElementRef, DestroyRef, signal, afterNextRender } from '@angular/core';
|
|
2
2
|
import { registerFeature } from '@toolbox-web/grid-angular';
|
|
3
3
|
import { ExportPlugin } from '@toolbox-web/grid/plugins/export';
|
|
4
4
|
|
|
@@ -74,6 +74,7 @@ registerFeature('export', (config) => {
|
|
|
74
74
|
*/
|
|
75
75
|
function injectGridExport() {
|
|
76
76
|
const elementRef = inject(ElementRef);
|
|
77
|
+
const destroyRef = inject(DestroyRef);
|
|
77
78
|
const isReady = signal(false, ...(ngDevMode ? [{ debugName: "isReady" }] : []));
|
|
78
79
|
// Lazy discovery: cached grid reference
|
|
79
80
|
let cachedGrid = null;
|
|
@@ -99,6 +100,20 @@ function injectGridExport() {
|
|
|
99
100
|
const getPlugin = () => {
|
|
100
101
|
return getGrid()?.getPlugin(ExportPlugin);
|
|
101
102
|
};
|
|
103
|
+
// Eagerly discover the grid after the first render so isReady updates
|
|
104
|
+
// without requiring a programmatic method call. Falls back to a
|
|
105
|
+
// MutationObserver for lazy-rendered tabs, *ngIf, @defer, etc.
|
|
106
|
+
afterNextRender(() => {
|
|
107
|
+
if (getGrid())
|
|
108
|
+
return;
|
|
109
|
+
const host = elementRef.nativeElement;
|
|
110
|
+
const observer = new MutationObserver(() => {
|
|
111
|
+
if (getGrid())
|
|
112
|
+
observer.disconnect();
|
|
113
|
+
});
|
|
114
|
+
observer.observe(host, { childList: true, subtree: true });
|
|
115
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
116
|
+
});
|
|
102
117
|
return {
|
|
103
118
|
isReady: isReady.asReadonly(),
|
|
104
119
|
exportToCsv: (filename, params) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-export.mjs","sources":["../../../../libs/grid-angular/features/export/src/index.ts","../../../../libs/grid-angular/features/export/src/toolbox-web-grid-angular-features-export.ts"],"sourcesContent":["/**\n * Export feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `export` input on Grid directive.\n * Also exports `injectGridExport()` for programmatic export control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/export';\n *\n * <tbw-grid [export]=\"true\" />\n * <tbw-grid [export]=\"{ fileName: 'data.csv' }\" />\n * ```\n *\n * @example Using injectGridExport\n * ```typescript\n * import { injectGridExport } from '@toolbox-web/grid-angular/features/export';\n *\n * @Component({...})\n * export class MyComponent {\n * private gridExport = injectGridExport();\n *\n * exportData() {\n * this.gridExport.exportToCsv('employees.csv');\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { ExportPlugin, type ExportFormat, type ExportParams } from '@toolbox-web/grid/plugins/export';\n\nregisterFeature('export', (config) => {\n if (config === true) {\n return new ExportPlugin();\n }\n return new ExportPlugin(config ?? undefined);\n});\n\n/**\n * Export methods returned from injectGridExport.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n * This ensures it works with lazy-rendered tabs, conditional rendering, etc.\n */\nexport interface ExportMethods {\n /**\n * Export grid data to CSV file.\n * @param filename - Optional filename (defaults to 'export.csv')\n * @param params - Optional export parameters\n */\n exportToCsv: (filename?: string, params?: Partial<ExportParams>) => void;\n\n /**\n * Export grid data to Excel file (XML Spreadsheet format).\n * @param filename - Optional filename (defaults to 'export.xlsx')\n * @param params - Optional export parameters\n */\n exportToExcel: (filename?: string, params?: Partial<ExportParams>) => void;\n\n /**\n * Export grid data to JSON file.\n * @param filename - Optional filename (defaults to 'export.json')\n * @param params - Optional export parameters\n */\n exportToJson: (filename?: string, params?: Partial<ExportParams>) => void;\n\n /**\n * Check if an export is currently in progress.\n */\n isExporting: () => boolean;\n\n /**\n * Get information about the last export.\n */\n getLastExport: () => { format: ExportFormat; timestamp: Date } | null;\n\n /**\n * Signal indicating if grid is ready.\n * The grid is discovered lazily, so this updates when first method call succeeds.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic export control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization. This ensures it works reliably with:\n * - Lazy-rendered tabs\n * - Conditional rendering (*ngIf)\n * - Dynamic component loading\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/export';\n * import { injectGridExport } from '@toolbox-web/grid-angular/features/export';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handleExport()\">Export CSV</button>\n * <tbw-grid [rows]=\"rows\" [export]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * gridExport = injectGridExport();\n *\n * handleExport() {\n * this.gridExport.exportToCsv('employees.csv');\n * }\n * }\n * ```\n */\nexport function injectGridExport(): ExportMethods {\n const elementRef = inject(ElementRef);\n const isReady = signal(false);\n\n // Lazy discovery: cached grid reference\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n\n /**\n * Lazily find the grid element. Called on each method invocation.\n * Caches the reference once found and triggers ready() check.\n */\n const getGrid = (): DataGridElement | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement | null;\n if (grid) {\n cachedGrid = grid;\n // Start ready() check only once\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): ExportPlugin | undefined => {\n return getGrid()?.getPlugin(ExportPlugin);\n };\n\n return {\n isReady: isReady.asReadonly(),\n\n exportToCsv: (filename?: string, params?: Partial<ExportParams>) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:export] ExportPlugin not found.\\n\\n` +\n ` → Enable export on the grid:\\n` +\n ` <tbw-grid [export]=\"true\" />`,\n );\n return;\n }\n plugin.exportCsv({ ...params, fileName: filename ?? params?.fileName ?? 'export.csv' });\n },\n\n exportToExcel: (filename?: string, params?: Partial<ExportParams>) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:export] ExportPlugin not found.\\n\\n` +\n ` → Enable export on the grid:\\n` +\n ` <tbw-grid [export]=\"true\" />`,\n );\n return;\n }\n plugin.exportExcel({ ...params, fileName: filename ?? params?.fileName ?? 'export.xlsx' });\n },\n\n exportToJson: (filename?: string, params?: Partial<ExportParams>) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:export] ExportPlugin not found.\\n\\n` +\n ` → Enable export on the grid:\\n` +\n ` <tbw-grid [export]=\"true\" />`,\n );\n return;\n }\n plugin.exportJson({ ...params, fileName: filename ?? params?.fileName ?? 'export.json' });\n },\n\n isExporting: () => {\n return getPlugin()?.isExporting() ?? false;\n },\n\n getLastExport: () => {\n return getPlugin()?.getLastExport() ?? null;\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BG;AAOH,eAAe,CAAC,QAAQ,EAAE,CAAC,MAAM,KAAI;AACnC,IAAA,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,IAAI,YAAY,EAAE;IAC3B;AACA,IAAA,OAAO,IAAI,YAAY,CAAC,MAAM,IAAI,SAAS,CAAC;AAC9C,CAAC,CAAC;AA+CF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCG;SACa,gBAAgB,GAAA;AAC9B,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,mDAAC;;IAG7B,IAAI,UAAU,GAA2B,IAAI;IAC7C,IAAI,mBAAmB,GAAG,KAAK;AAE/B;;;AAGG;IACH,MAAM,OAAO,GAAG,MAA6B;AAC3C,QAAA,IAAI,UAAU;AAAE,YAAA,OAAO,UAAU;QAEjC,MAAM,IAAI,GAAG,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,UAAU,CAA2B;QACzF,IAAI,IAAI,EAAE;YACR,UAAU,GAAG,IAAI;;YAEjB,IAAI,CAAC,mBAAmB,EAAE;gBACxB,mBAAmB,GAAG,IAAI;AAC1B,gBAAA,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C;QACF;AACA,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;IAED,MAAM,SAAS,GAAG,MAA+B;AAC/C,QAAA,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC,YAAY,CAAC;AAC3C,IAAA,CAAC;
|
|
1
|
+
{"version":3,"file":"toolbox-web-grid-angular-features-export.mjs","sources":["../../../../libs/grid-angular/features/export/src/index.ts","../../../../libs/grid-angular/features/export/src/toolbox-web-grid-angular-features-export.ts"],"sourcesContent":["/**\n * Export feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `export` input on Grid directive.\n * Also exports `injectGridExport()` for programmatic export control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/export';\n *\n * <tbw-grid [export]=\"true\" />\n * <tbw-grid [export]=\"{ fileName: 'data.csv' }\" />\n * ```\n *\n * @example Using injectGridExport\n * ```typescript\n * import { injectGridExport } from '@toolbox-web/grid-angular/features/export';\n *\n * @Component({...})\n * export class MyComponent {\n * private gridExport = injectGridExport();\n *\n * exportData() {\n * this.gridExport.exportToCsv('employees.csv');\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { afterNextRender, DestroyRef, ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { ExportPlugin, type ExportFormat, type ExportParams } from '@toolbox-web/grid/plugins/export';\n\nregisterFeature('export', (config) => {\n if (config === true) {\n return new ExportPlugin();\n }\n return new ExportPlugin(config ?? undefined);\n});\n\n/**\n * Export methods returned from injectGridExport.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n * This ensures it works with lazy-rendered tabs, conditional rendering, etc.\n */\nexport interface ExportMethods {\n /**\n * Export grid data to CSV file.\n * @param filename - Optional filename (defaults to 'export.csv')\n * @param params - Optional export parameters\n */\n exportToCsv: (filename?: string, params?: Partial<ExportParams>) => void;\n\n /**\n * Export grid data to Excel file (XML Spreadsheet format).\n * @param filename - Optional filename (defaults to 'export.xlsx')\n * @param params - Optional export parameters\n */\n exportToExcel: (filename?: string, params?: Partial<ExportParams>) => void;\n\n /**\n * Export grid data to JSON file.\n * @param filename - Optional filename (defaults to 'export.json')\n * @param params - Optional export parameters\n */\n exportToJson: (filename?: string, params?: Partial<ExportParams>) => void;\n\n /**\n * Check if an export is currently in progress.\n */\n isExporting: () => boolean;\n\n /**\n * Get information about the last export.\n */\n getLastExport: () => { format: ExportFormat; timestamp: Date } | null;\n\n /**\n * Signal indicating if grid is ready.\n * The grid is discovered lazily, so this updates when first method call succeeds.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic export control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization. This ensures it works reliably with:\n * - Lazy-rendered tabs\n * - Conditional rendering (*ngIf)\n * - Dynamic component loading\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/export';\n * import { injectGridExport } from '@toolbox-web/grid-angular/features/export';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handleExport()\">Export CSV</button>\n * <tbw-grid [rows]=\"rows\" [export]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * gridExport = injectGridExport();\n *\n * handleExport() {\n * this.gridExport.exportToCsv('employees.csv');\n * }\n * }\n * ```\n */\nexport function injectGridExport(): ExportMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n // Lazy discovery: cached grid reference\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n\n /**\n * Lazily find the grid element. Called on each method invocation.\n * Caches the reference once found and triggers ready() check.\n */\n const getGrid = (): DataGridElement | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement | null;\n if (grid) {\n cachedGrid = grid;\n // Start ready() check only once\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): ExportPlugin | undefined => {\n return getGrid()?.getPlugin(ExportPlugin);\n };\n\n // Eagerly discover the grid after the first render so isReady updates\n // without requiring a programmatic method call. Falls back to a\n // MutationObserver for lazy-rendered tabs, *ngIf, @defer, etc.\n afterNextRender(() => {\n if (getGrid()) return;\n\n const host = elementRef.nativeElement as HTMLElement;\n const observer = new MutationObserver(() => {\n if (getGrid()) observer.disconnect();\n });\n observer.observe(host, { childList: true, subtree: true });\n\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n return {\n isReady: isReady.asReadonly(),\n\n exportToCsv: (filename?: string, params?: Partial<ExportParams>) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:export] ExportPlugin not found.\\n\\n` +\n ` → Enable export on the grid:\\n` +\n ` <tbw-grid [export]=\"true\" />`,\n );\n return;\n }\n plugin.exportCsv({ ...params, fileName: filename ?? params?.fileName ?? 'export.csv' });\n },\n\n exportToExcel: (filename?: string, params?: Partial<ExportParams>) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:export] ExportPlugin not found.\\n\\n` +\n ` → Enable export on the grid:\\n` +\n ` <tbw-grid [export]=\"true\" />`,\n );\n return;\n }\n plugin.exportExcel({ ...params, fileName: filename ?? params?.fileName ?? 'export.xlsx' });\n },\n\n exportToJson: (filename?: string, params?: Partial<ExportParams>) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:export] ExportPlugin not found.\\n\\n` +\n ` → Enable export on the grid:\\n` +\n ` <tbw-grid [export]=\"true\" />`,\n );\n return;\n }\n plugin.exportJson({ ...params, fileName: filename ?? params?.fileName ?? 'export.json' });\n },\n\n isExporting: () => {\n return getPlugin()?.isExporting() ?? false;\n },\n\n getLastExport: () => {\n return getPlugin()?.getLastExport() ?? null;\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BG;AAOH,eAAe,CAAC,QAAQ,EAAE,CAAC,MAAM,KAAI;AACnC,IAAA,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,IAAI,YAAY,EAAE;IAC3B;AACA,IAAA,OAAO,IAAI,YAAY,CAAC,MAAM,IAAI,SAAS,CAAC;AAC9C,CAAC,CAAC;AA+CF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCG;SACa,gBAAgB,GAAA;AAC9B,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,mDAAC;;IAG7B,IAAI,UAAU,GAA2B,IAAI;IAC7C,IAAI,mBAAmB,GAAG,KAAK;AAE/B;;;AAGG;IACH,MAAM,OAAO,GAAG,MAA6B;AAC3C,QAAA,IAAI,UAAU;AAAE,YAAA,OAAO,UAAU;QAEjC,MAAM,IAAI,GAAG,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,UAAU,CAA2B;QACzF,IAAI,IAAI,EAAE;YACR,UAAU,GAAG,IAAI;;YAEjB,IAAI,CAAC,mBAAmB,EAAE;gBACxB,mBAAmB,GAAG,IAAI;AAC1B,gBAAA,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C;QACF;AACA,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;IAED,MAAM,SAAS,GAAG,MAA+B;AAC/C,QAAA,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC,YAAY,CAAC;AAC3C,IAAA,CAAC;;;;IAKD,eAAe,CAAC,MAAK;AACnB,QAAA,IAAI,OAAO,EAAE;YAAE;AAEf,QAAA,MAAM,IAAI,GAAG,UAAU,CAAC,aAA4B;AACpD,QAAA,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CAAC,MAAK;AACzC,YAAA,IAAI,OAAO,EAAE;gBAAE,QAAQ,CAAC,UAAU,EAAE;AACtC,QAAA,CAAC,CAAC;AACF,QAAA,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAE1D,UAAU,CAAC,SAAS,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;AACnD,IAAA,CAAC,CAAC;IAEF,OAAO;AACL,QAAA,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE;AAE7B,QAAA,WAAW,EAAE,CAAC,QAAiB,EAAE,MAA8B,KAAI;AACjE,YAAA,MAAM,MAAM,GAAG,SAAS,EAAE;YAC1B,IAAI,CAAC,MAAM,EAAE;gBACX,OAAO,CAAC,IAAI,CACV,CAAA,6CAAA,CAA+C;oBAC7C,CAAA,gCAAA,CAAkC;AAClC,oBAAA,CAAA,gCAAA,CAAkC,CACrC;gBACD;YACF;AACA,YAAA,MAAM,CAAC,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,QAAQ,IAAI,MAAM,EAAE,QAAQ,IAAI,YAAY,EAAE,CAAC;QACzF,CAAC;AAED,QAAA,aAAa,EAAE,CAAC,QAAiB,EAAE,MAA8B,KAAI;AACnE,YAAA,MAAM,MAAM,GAAG,SAAS,EAAE;YAC1B,IAAI,CAAC,MAAM,EAAE;gBACX,OAAO,CAAC,IAAI,CACV,CAAA,6CAAA,CAA+C;oBAC7C,CAAA,gCAAA,CAAkC;AAClC,oBAAA,CAAA,gCAAA,CAAkC,CACrC;gBACD;YACF;AACA,YAAA,MAAM,CAAC,WAAW,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,QAAQ,IAAI,MAAM,EAAE,QAAQ,IAAI,aAAa,EAAE,CAAC;QAC5F,CAAC;AAED,QAAA,YAAY,EAAE,CAAC,QAAiB,EAAE,MAA8B,KAAI;AAClE,YAAA,MAAM,MAAM,GAAG,SAAS,EAAE;YAC1B,IAAI,CAAC,MAAM,EAAE;gBACX,OAAO,CAAC,IAAI,CACV,CAAA,6CAAA,CAA+C;oBAC7C,CAAA,gCAAA,CAAkC;AAClC,oBAAA,CAAA,gCAAA,CAAkC,CACrC;gBACD;YACF;AACA,YAAA,MAAM,CAAC,UAAU,CAAC,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,QAAQ,IAAI,MAAM,EAAE,QAAQ,IAAI,aAAa,EAAE,CAAC;QAC3F,CAAC;QAED,WAAW,EAAE,MAAK;AAChB,YAAA,OAAO,SAAS,EAAE,EAAE,WAAW,EAAE,IAAI,KAAK;QAC5C,CAAC;QAED,aAAa,EAAE,MAAK;AAClB,YAAA,OAAO,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,IAAI;QAC7C,CAAC;KACF;AACH;;AC3NA;;AAEG;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject, ElementRef, signal } from '@angular/core';
|
|
1
|
+
import { inject, ElementRef, DestroyRef, signal, afterNextRender } from '@angular/core';
|
|
2
2
|
import { registerFeature } from '@toolbox-web/grid-angular';
|
|
3
3
|
import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';
|
|
4
4
|
|
|
@@ -74,6 +74,7 @@ registerFeature('filtering', (config) => {
|
|
|
74
74
|
*/
|
|
75
75
|
function injectGridFiltering() {
|
|
76
76
|
const elementRef = inject(ElementRef);
|
|
77
|
+
const destroyRef = inject(DestroyRef);
|
|
77
78
|
const isReady = signal(false, ...(ngDevMode ? [{ debugName: "isReady" }] : []));
|
|
78
79
|
let cachedGrid = null;
|
|
79
80
|
let readyPromiseStarted = false;
|
|
@@ -93,6 +94,20 @@ function injectGridFiltering() {
|
|
|
93
94
|
const getPlugin = () => {
|
|
94
95
|
return getGrid()?.getPlugin(FilteringPlugin);
|
|
95
96
|
};
|
|
97
|
+
// Eagerly discover the grid after the first render so isReady updates
|
|
98
|
+
// without requiring a programmatic method call. Falls back to a
|
|
99
|
+
// MutationObserver for lazy-rendered tabs, *ngIf, @defer, etc.
|
|
100
|
+
afterNextRender(() => {
|
|
101
|
+
if (getGrid())
|
|
102
|
+
return;
|
|
103
|
+
const host = elementRef.nativeElement;
|
|
104
|
+
const observer = new MutationObserver(() => {
|
|
105
|
+
if (getGrid())
|
|
106
|
+
observer.disconnect();
|
|
107
|
+
});
|
|
108
|
+
observer.observe(host, { childList: true, subtree: true });
|
|
109
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
110
|
+
});
|
|
96
111
|
return {
|
|
97
112
|
isReady: isReady.asReadonly(),
|
|
98
113
|
setFilter: (field, filter) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-filtering.mjs","sources":["../../../../libs/grid-angular/features/filtering/src/index.ts","../../../../libs/grid-angular/features/filtering/src/toolbox-web-grid-angular-features-filtering.ts"],"sourcesContent":["/**\n * Filtering feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `filtering` input on Grid directive.\n * Also exports `injectGridFiltering()` for programmatic filter control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/filtering';\n *\n * <tbw-grid [filtering]=\"true\" />\n * <tbw-grid [filtering]=\"{ debounceMs: 200 }\" />\n * ```\n *\n * @example Using injectGridFiltering\n * ```typescript\n * import { injectGridFiltering } from '@toolbox-web/grid-angular/features/filtering';\n *\n * @Component({...})\n * export class MyComponent {\n * private filtering = injectGridFiltering();\n *\n * filterByStatus(status: string) {\n * this.filtering.setFilter('status', { operator: 'equals', value: status });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { FilteringPlugin, type FilterModel } from '@toolbox-web/grid/plugins/filtering';\n\nregisterFeature('filtering', (config) => {\n if (config === true) {\n return new FilteringPlugin();\n }\n return new FilteringPlugin(config ?? undefined);\n});\n\n/**\n * Filtering methods returned from injectGridFiltering.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n */\nexport interface FilteringMethods {\n /**\n * Set a filter on a specific field.\n * @param field - The field name to filter\n * @param filter - Filter configuration, or null to remove\n */\n setFilter: (field: string, filter: Omit<FilterModel, 'field'> | null) => void;\n\n /**\n * Get the current filter for a field.\n */\n getFilter: (field: string) => FilterModel | undefined;\n\n /**\n * Get all active filters.\n */\n getFilters: () => FilterModel[];\n\n /**\n * Set all filters at once (replaces existing).\n */\n setFilterModel: (filters: FilterModel[]) => void;\n\n /**\n * Clear all active filters.\n */\n clearAllFilters: () => void;\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter: (field: string) => void;\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered: (field: string) => boolean;\n\n /**\n * Get the count of rows after filtering.\n */\n getFilteredRowCount: () => number;\n\n /**\n * Get unique values for a field (for building filter dropdowns).\n */\n getUniqueValues: (field: string) => unknown[];\n\n /**\n * Signal indicating if grid is ready.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic filter control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization.\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/filtering';\n * import { injectGridFiltering } from '@toolbox-web/grid-angular/features/filtering';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <input (input)=\"applyFilter($event)\" placeholder=\"Filter by name...\" />\n * <span>{{ filtering.getFilteredRowCount() }} results</span>\n * <button (click)=\"filtering.clearAllFilters()\">Clear</button>\n * <tbw-grid [rows]=\"rows\" [filtering]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * filtering = injectGridFiltering();\n *\n * applyFilter(event: Event) {\n * const value = (event.target as HTMLInputElement).value;\n * this.filtering.setFilter('name', value ? { operator: 'contains', value } : null);\n * }\n * }\n * ```\n */\nexport function injectGridFiltering(): FilteringMethods {\n const elementRef = inject(ElementRef);\n const isReady = signal(false);\n\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n\n const getGrid = (): DataGridElement | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement | null;\n if (grid) {\n cachedGrid = grid;\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): FilteringPlugin | undefined => {\n return getGrid()?.getPlugin(FilteringPlugin);\n };\n\n return {\n isReady: isReady.asReadonly(),\n\n setFilter: (field: string, filter: Omit<FilterModel, 'field'> | null) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.setFilter(field, filter);\n },\n\n getFilter: (field: string) => getPlugin()?.getFilter(field),\n\n getFilters: () => getPlugin()?.getFilters() ?? [],\n\n setFilterModel: (filters: FilterModel[]) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.setFilterModel(filters);\n },\n\n clearAllFilters: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.clearAllFilters();\n },\n\n clearFieldFilter: (field: string) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.clearFieldFilter(field);\n },\n\n isFieldFiltered: (field: string) => getPlugin()?.isFieldFiltered(field) ?? false,\n\n getFilteredRowCount: () => getPlugin()?.getFilteredRowCount() ?? 0,\n\n getUniqueValues: (field: string) => getPlugin()?.getUniqueValues(field) ?? [],\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toolbox-web-grid-angular-features-filtering.mjs","sources":["../../../../libs/grid-angular/features/filtering/src/index.ts","../../../../libs/grid-angular/features/filtering/src/toolbox-web-grid-angular-features-filtering.ts"],"sourcesContent":["/**\n * Filtering feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `filtering` input on Grid directive.\n * Also exports `injectGridFiltering()` for programmatic filter control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/filtering';\n *\n * <tbw-grid [filtering]=\"true\" />\n * <tbw-grid [filtering]=\"{ debounceMs: 200 }\" />\n * ```\n *\n * @example Using injectGridFiltering\n * ```typescript\n * import { injectGridFiltering } from '@toolbox-web/grid-angular/features/filtering';\n *\n * @Component({...})\n * export class MyComponent {\n * private filtering = injectGridFiltering();\n *\n * filterByStatus(status: string) {\n * this.filtering.setFilter('status', { operator: 'equals', value: status });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { afterNextRender, DestroyRef, ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { FilteringPlugin, type FilterModel } from '@toolbox-web/grid/plugins/filtering';\n\nregisterFeature('filtering', (config) => {\n if (config === true) {\n return new FilteringPlugin();\n }\n return new FilteringPlugin(config ?? undefined);\n});\n\n/**\n * Filtering methods returned from injectGridFiltering.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n */\nexport interface FilteringMethods {\n /**\n * Set a filter on a specific field.\n * @param field - The field name to filter\n * @param filter - Filter configuration, or null to remove\n */\n setFilter: (field: string, filter: Omit<FilterModel, 'field'> | null) => void;\n\n /**\n * Get the current filter for a field.\n */\n getFilter: (field: string) => FilterModel | undefined;\n\n /**\n * Get all active filters.\n */\n getFilters: () => FilterModel[];\n\n /**\n * Set all filters at once (replaces existing).\n */\n setFilterModel: (filters: FilterModel[]) => void;\n\n /**\n * Clear all active filters.\n */\n clearAllFilters: () => void;\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter: (field: string) => void;\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered: (field: string) => boolean;\n\n /**\n * Get the count of rows after filtering.\n */\n getFilteredRowCount: () => number;\n\n /**\n * Get unique values for a field (for building filter dropdowns).\n */\n getUniqueValues: (field: string) => unknown[];\n\n /**\n * Signal indicating if grid is ready.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic filter control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization.\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/filtering';\n * import { injectGridFiltering } from '@toolbox-web/grid-angular/features/filtering';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <input (input)=\"applyFilter($event)\" placeholder=\"Filter by name...\" />\n * <span>{{ filtering.getFilteredRowCount() }} results</span>\n * <button (click)=\"filtering.clearAllFilters()\">Clear</button>\n * <tbw-grid [rows]=\"rows\" [filtering]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * filtering = injectGridFiltering();\n *\n * applyFilter(event: Event) {\n * const value = (event.target as HTMLInputElement).value;\n * this.filtering.setFilter('name', value ? { operator: 'contains', value } : null);\n * }\n * }\n * ```\n */\nexport function injectGridFiltering(): FilteringMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n\n const getGrid = (): DataGridElement | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement | null;\n if (grid) {\n cachedGrid = grid;\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): FilteringPlugin | undefined => {\n return getGrid()?.getPlugin(FilteringPlugin);\n };\n\n // Eagerly discover the grid after the first render so isReady updates\n // without requiring a programmatic method call. Falls back to a\n // MutationObserver for lazy-rendered tabs, *ngIf, @defer, etc.\n afterNextRender(() => {\n if (getGrid()) return;\n\n const host = elementRef.nativeElement as HTMLElement;\n const observer = new MutationObserver(() => {\n if (getGrid()) observer.disconnect();\n });\n observer.observe(host, { childList: true, subtree: true });\n\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n return {\n isReady: isReady.asReadonly(),\n\n setFilter: (field: string, filter: Omit<FilterModel, 'field'> | null) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.setFilter(field, filter);\n },\n\n getFilter: (field: string) => getPlugin()?.getFilter(field),\n\n getFilters: () => getPlugin()?.getFilters() ?? [],\n\n setFilterModel: (filters: FilterModel[]) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.setFilterModel(filters);\n },\n\n clearAllFilters: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.clearAllFilters();\n },\n\n clearFieldFilter: (field: string) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:filtering] FilteringPlugin not found.\\n\\n` +\n ` → Enable filtering on the grid:\\n` +\n ` <tbw-grid [filtering]=\"true\" />`,\n );\n return;\n }\n plugin.clearFieldFilter(field);\n },\n\n isFieldFiltered: (field: string) => getPlugin()?.isFieldFiltered(field) ?? false,\n\n getFilteredRowCount: () => getPlugin()?.getFilteredRowCount() ?? 0,\n\n getUniqueValues: (field: string) => getPlugin()?.getUniqueValues(field) ?? [],\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@ import { registerFeature } from '@toolbox-web/grid-angular';
|
|
|
11
11
|
* import '@toolbox-web/grid-angular/features/pinned-columns';
|
|
12
12
|
*
|
|
13
13
|
* <tbw-grid [pinnedColumns]="true" [columns]="[
|
|
14
|
-
* { field: 'id',
|
|
14
|
+
* { field: 'id', pinned: 'left' },
|
|
15
15
|
* { field: 'name' }
|
|
16
16
|
* ]" />
|
|
17
17
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-pinned-columns.mjs","sources":["../../../../libs/grid-angular/features/pinned-columns/src/index.ts","../../../../libs/grid-angular/features/pinned-columns/src/toolbox-web-grid-angular-features-pinned-columns.ts"],"sourcesContent":["/**\n * Pinned columns feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `pinnedColumns` input on Grid directive.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/pinned-columns';\n *\n * <tbw-grid [pinnedColumns]=\"true\" [columns]=\"[\n * { field: 'id',
|
|
1
|
+
{"version":3,"file":"toolbox-web-grid-angular-features-pinned-columns.mjs","sources":["../../../../libs/grid-angular/features/pinned-columns/src/index.ts","../../../../libs/grid-angular/features/pinned-columns/src/toolbox-web-grid-angular-features-pinned-columns.ts"],"sourcesContent":["/**\n * Pinned columns feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `pinnedColumns` input on Grid directive.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/pinned-columns';\n *\n * <tbw-grid [pinnedColumns]=\"true\" [columns]=\"[\n * { field: 'id', pinned: 'left' },\n * { field: 'name' }\n * ]\" />\n * ```\n *\n * @packageDocumentation\n */\n\nimport { PinnedColumnsPlugin } from '@toolbox-web/grid/plugins/pinned-columns';\nimport { registerFeature } from '@toolbox-web/grid-angular';\n\nregisterFeature('pinnedColumns', () => {\n return new PinnedColumnsPlugin();\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;AAgBG;AAKH,eAAe,CAAC,eAAe,EAAE,MAAK;IACpC,OAAO,IAAI,mBAAmB,EAAE;AAClC,CAAC,CAAC;;ACvBF;;AAEG"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject, ElementRef, signal } from '@angular/core';
|
|
1
|
+
import { inject, ElementRef, DestroyRef, signal, afterNextRender } from '@angular/core';
|
|
2
2
|
import { registerFeature } from '@toolbox-web/grid-angular';
|
|
3
3
|
import { PrintPlugin } from '@toolbox-web/grid/plugins/print';
|
|
4
4
|
|
|
@@ -73,6 +73,7 @@ registerFeature('print', (config) => {
|
|
|
73
73
|
*/
|
|
74
74
|
function injectGridPrint() {
|
|
75
75
|
const elementRef = inject(ElementRef);
|
|
76
|
+
const destroyRef = inject(DestroyRef);
|
|
76
77
|
const isReady = signal(false, ...(ngDevMode ? [{ debugName: "isReady" }] : []));
|
|
77
78
|
let cachedGrid = null;
|
|
78
79
|
let readyPromiseStarted = false;
|
|
@@ -92,6 +93,20 @@ function injectGridPrint() {
|
|
|
92
93
|
const getPlugin = () => {
|
|
93
94
|
return getGrid()?.getPlugin(PrintPlugin);
|
|
94
95
|
};
|
|
96
|
+
// Eagerly discover the grid after the first render so isReady updates
|
|
97
|
+
// without requiring a programmatic method call. Falls back to a
|
|
98
|
+
// MutationObserver for lazy-rendered tabs, *ngIf, @defer, etc.
|
|
99
|
+
afterNextRender(() => {
|
|
100
|
+
if (getGrid())
|
|
101
|
+
return;
|
|
102
|
+
const host = elementRef.nativeElement;
|
|
103
|
+
const observer = new MutationObserver(() => {
|
|
104
|
+
if (getGrid())
|
|
105
|
+
observer.disconnect();
|
|
106
|
+
});
|
|
107
|
+
observer.observe(host, { childList: true, subtree: true });
|
|
108
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
109
|
+
});
|
|
95
110
|
return {
|
|
96
111
|
isReady: isReady.asReadonly(),
|
|
97
112
|
print: async (params) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-print.mjs","sources":["../../../../libs/grid-angular/features/print/src/index.ts","../../../../libs/grid-angular/features/print/src/toolbox-web-grid-angular-features-print.ts"],"sourcesContent":["/**\n * Print feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `print` input on Grid directive.\n * Also exports `injectGridPrint()` for programmatic print control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/print';\n *\n * <tbw-grid [print]=\"true\" />\n * ```\n *\n * @example Using injectGridPrint\n * ```typescript\n * import { injectGridPrint } from '@toolbox-web/grid-angular/features/print';\n *\n * @Component({...})\n * export class MyComponent {\n * private gridPrint = injectGridPrint();\n *\n * printReport() {\n * this.gridPrint.print({ title: 'My Report' });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { PrintPlugin, type PrintParams } from '@toolbox-web/grid/plugins/print';\n\nregisterFeature('print', (config) => {\n if (config === true) {\n return new PrintPlugin();\n }\n return new PrintPlugin(config ?? undefined);\n});\n\n/**\n * Print methods returned from injectGridPrint.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n */\nexport interface PrintMethods {\n /**\n * Print the grid.\n * Opens browser print dialog after preparing the grid for printing.\n * @param params - Optional print parameters\n */\n print: (params?: PrintParams) => Promise<void>;\n\n /**\n * Check if a print operation is currently in progress.\n */\n isPrinting: () => boolean;\n\n /**\n * Signal indicating if grid is ready.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic print control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization.\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/print';\n * import { injectGridPrint } from '@toolbox-web/grid-angular/features/print';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handlePrint()\" [disabled]=\"gridPrint.isPrinting()\">\n * {{ gridPrint.isPrinting() ? 'Printing...' : 'Print' }}\n * </button>\n * <tbw-grid [rows]=\"rows\" [print]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * gridPrint = injectGridPrint();\n *\n * async handlePrint() {\n * await this.gridPrint.print({ title: 'Employee Report', isolate: true });\n * console.log('Print dialog closed');\n * }\n * }\n * ```\n */\nexport function injectGridPrint(): PrintMethods {\n const elementRef = inject(ElementRef);\n const isReady = signal(false);\n\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n\n const getGrid = (): DataGridElement | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement | null;\n if (grid) {\n cachedGrid = grid;\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): PrintPlugin | undefined => {\n return getGrid()?.getPlugin(PrintPlugin);\n };\n\n return {\n isReady: isReady.asReadonly(),\n\n print: async (params?: PrintParams) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:print] PrintPlugin not found.\\n\\n` +\n ` → Enable print on the grid:\\n` +\n ` <tbw-grid [print]=\"true\" />`,\n );\n return;\n }\n await plugin.print(params);\n },\n\n isPrinting: () => getPlugin()?.isPrinting() ?? false,\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BG;AAOH,eAAe,CAAC,OAAO,EAAE,CAAC,MAAM,KAAI;AAClC,IAAA,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,IAAI,WAAW,EAAE;IAC1B;AACA,IAAA,OAAO,IAAI,WAAW,CAAC,MAAM,IAAI,SAAS,CAAC;AAC7C,CAAC,CAAC;AA0BF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCG;SACa,eAAe,GAAA;AAC7B,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,mDAAC;IAE7B,IAAI,UAAU,GAA2B,IAAI;IAC7C,IAAI,mBAAmB,GAAG,KAAK;IAE/B,MAAM,OAAO,GAAG,MAA6B;AAC3C,QAAA,IAAI,UAAU;AAAE,YAAA,OAAO,UAAU;QAEjC,MAAM,IAAI,GAAG,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,UAAU,CAA2B;QACzF,IAAI,IAAI,EAAE;YACR,UAAU,GAAG,IAAI;YACjB,IAAI,CAAC,mBAAmB,EAAE;gBACxB,mBAAmB,GAAG,IAAI;AAC1B,gBAAA,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C;QACF;AACA,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;IAED,MAAM,SAAS,GAAG,MAA8B;AAC9C,QAAA,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC,WAAW,CAAC;AAC1C,IAAA,CAAC;
|
|
1
|
+
{"version":3,"file":"toolbox-web-grid-angular-features-print.mjs","sources":["../../../../libs/grid-angular/features/print/src/index.ts","../../../../libs/grid-angular/features/print/src/toolbox-web-grid-angular-features-print.ts"],"sourcesContent":["/**\n * Print feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `print` input on Grid directive.\n * Also exports `injectGridPrint()` for programmatic print control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/print';\n *\n * <tbw-grid [print]=\"true\" />\n * ```\n *\n * @example Using injectGridPrint\n * ```typescript\n * import { injectGridPrint } from '@toolbox-web/grid-angular/features/print';\n *\n * @Component({...})\n * export class MyComponent {\n * private gridPrint = injectGridPrint();\n *\n * printReport() {\n * this.gridPrint.print({ title: 'My Report' });\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { afterNextRender, DestroyRef, ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport { PrintPlugin, type PrintParams } from '@toolbox-web/grid/plugins/print';\n\nregisterFeature('print', (config) => {\n if (config === true) {\n return new PrintPlugin();\n }\n return new PrintPlugin(config ?? undefined);\n});\n\n/**\n * Print methods returned from injectGridPrint.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n */\nexport interface PrintMethods {\n /**\n * Print the grid.\n * Opens browser print dialog after preparing the grid for printing.\n * @param params - Optional print parameters\n */\n print: (params?: PrintParams) => Promise<void>;\n\n /**\n * Check if a print operation is currently in progress.\n */\n isPrinting: () => boolean;\n\n /**\n * Signal indicating if grid is ready.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic print control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization.\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/print';\n * import { injectGridPrint } from '@toolbox-web/grid-angular/features/print';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handlePrint()\" [disabled]=\"gridPrint.isPrinting()\">\n * {{ gridPrint.isPrinting() ? 'Printing...' : 'Print' }}\n * </button>\n * <tbw-grid [rows]=\"rows\" [print]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * gridPrint = injectGridPrint();\n *\n * async handlePrint() {\n * await this.gridPrint.print({ title: 'Employee Report', isolate: true });\n * console.log('Print dialog closed');\n * }\n * }\n * ```\n */\nexport function injectGridPrint(): PrintMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n\n const getGrid = (): DataGridElement | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement | null;\n if (grid) {\n cachedGrid = grid;\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): PrintPlugin | undefined => {\n return getGrid()?.getPlugin(PrintPlugin);\n };\n\n // Eagerly discover the grid after the first render so isReady updates\n // without requiring a programmatic method call. Falls back to a\n // MutationObserver for lazy-rendered tabs, *ngIf, @defer, etc.\n afterNextRender(() => {\n if (getGrid()) return;\n\n const host = elementRef.nativeElement as HTMLElement;\n const observer = new MutationObserver(() => {\n if (getGrid()) observer.disconnect();\n });\n observer.observe(host, { childList: true, subtree: true });\n\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n return {\n isReady: isReady.asReadonly(),\n\n print: async (params?: PrintParams) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:print] PrintPlugin not found.\\n\\n` +\n ` → Enable print on the grid:\\n` +\n ` <tbw-grid [print]=\"true\" />`,\n );\n return;\n }\n await plugin.print(params);\n },\n\n isPrinting: () => getPlugin()?.isPrinting() ?? false,\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BG;AAOH,eAAe,CAAC,OAAO,EAAE,CAAC,MAAM,KAAI;AAClC,IAAA,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,IAAI,WAAW,EAAE;IAC1B;AACA,IAAA,OAAO,IAAI,WAAW,CAAC,MAAM,IAAI,SAAS,CAAC;AAC7C,CAAC,CAAC;AA0BF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCG;SACa,eAAe,GAAA;AAC7B,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;AACrC,IAAA,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,mDAAC;IAE7B,IAAI,UAAU,GAA2B,IAAI;IAC7C,IAAI,mBAAmB,GAAG,KAAK;IAE/B,MAAM,OAAO,GAAG,MAA6B;AAC3C,QAAA,IAAI,UAAU;AAAE,YAAA,OAAO,UAAU;QAEjC,MAAM,IAAI,GAAG,UAAU,CAAC,aAAa,CAAC,aAAa,CAAC,UAAU,CAA2B;QACzF,IAAI,IAAI,EAAE;YACR,UAAU,GAAG,IAAI;YACjB,IAAI,CAAC,mBAAmB,EAAE;gBACxB,mBAAmB,GAAG,IAAI;AAC1B,gBAAA,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C;QACF;AACA,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;IAED,MAAM,SAAS,GAAG,MAA8B;AAC9C,QAAA,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC,WAAW,CAAC;AAC1C,IAAA,CAAC;;;;IAKD,eAAe,CAAC,MAAK;AACnB,QAAA,IAAI,OAAO,EAAE;YAAE;AAEf,QAAA,MAAM,IAAI,GAAG,UAAU,CAAC,aAA4B;AACpD,QAAA,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CAAC,MAAK;AACzC,YAAA,IAAI,OAAO,EAAE;gBAAE,QAAQ,CAAC,UAAU,EAAE;AACtC,QAAA,CAAC,CAAC;AACF,QAAA,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAE1D,UAAU,CAAC,SAAS,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC;AACnD,IAAA,CAAC,CAAC;IAEF,OAAO;AACL,QAAA,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE;AAE7B,QAAA,KAAK,EAAE,OAAO,MAAoB,KAAI;AACpC,YAAA,MAAM,MAAM,GAAG,SAAS,EAAE;YAC1B,IAAI,CAAC,MAAM,EAAE;gBACX,OAAO,CAAC,IAAI,CACV,CAAA,2CAAA,CAA6C;oBAC3C,CAAA,+BAAA,CAAiC;AACjC,oBAAA,CAAA,+BAAA,CAAiC,CACpC;gBACD;YACF;AACA,YAAA,MAAM,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAC5B,CAAC;QAED,UAAU,EAAE,MAAM,SAAS,EAAE,EAAE,UAAU,EAAE,IAAI,KAAK;KACrD;AACH;;AC/JA;;AAEG;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject, ElementRef, DestroyRef, signal } from '@angular/core';
|
|
1
|
+
import { inject, ElementRef, DestroyRef, signal, afterNextRender } from '@angular/core';
|
|
2
2
|
import { registerFeature } from '@toolbox-web/grid-angular';
|
|
3
3
|
import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';
|
|
4
4
|
|
|
@@ -141,6 +141,40 @@ function injectGridSelection() {
|
|
|
141
141
|
const getPlugin = () => {
|
|
142
142
|
return getGrid()?.getPlugin(SelectionPlugin);
|
|
143
143
|
};
|
|
144
|
+
/**
|
|
145
|
+
* Sync reactive signals with the current plugin state.
|
|
146
|
+
* Called once when the grid is first discovered and ready.
|
|
147
|
+
*/
|
|
148
|
+
const syncSignals = () => {
|
|
149
|
+
const plugin = getPlugin();
|
|
150
|
+
if (plugin) {
|
|
151
|
+
selectionSignal.set(plugin.getSelection());
|
|
152
|
+
const mode = plugin.config?.mode;
|
|
153
|
+
selectedRowIndicesSignal.set(mode === 'row' ? plugin.getSelectedRowIndices() : []);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// Discover the grid after the first render so the selection-change
|
|
157
|
+
// listener is attached without requiring a programmatic method call.
|
|
158
|
+
// Uses a MutationObserver as fallback for lazy-rendered tabs, *ngIf,
|
|
159
|
+
// @defer, etc. where the grid may not be in the DOM on first render.
|
|
160
|
+
afterNextRender(() => {
|
|
161
|
+
const grid = getGrid();
|
|
162
|
+
if (grid) {
|
|
163
|
+
grid.ready?.().then(syncSignals);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Grid not in DOM yet — watch for it to appear.
|
|
167
|
+
const host = elementRef.nativeElement;
|
|
168
|
+
const observer = new MutationObserver(() => {
|
|
169
|
+
const discovered = getGrid();
|
|
170
|
+
if (discovered) {
|
|
171
|
+
observer.disconnect();
|
|
172
|
+
discovered.ready?.().then(syncSignals);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
observer.observe(host, { childList: true, subtree: true });
|
|
176
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
177
|
+
});
|
|
144
178
|
return {
|
|
145
179
|
isReady: isReady.asReadonly(),
|
|
146
180
|
selection: selectionSignal.asReadonly(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-selection.mjs","sources":["../../../../libs/grid-angular/features/selection/src/index.ts","../../../../libs/grid-angular/features/selection/src/toolbox-web-grid-angular-features-selection.ts"],"sourcesContent":["/**\n * Selection feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `selection` input on Grid directive.\n * Also exports `injectGridSelection()` for programmatic selection control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/selection';\n *\n * <tbw-grid [selection]=\"'range'\" />\n * ```\n *\n * @example Using injectGridSelection\n * ```typescript\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({...})\n * export class MyComponent {\n * private selection = injectGridSelection<Employee>();\n *\n * selectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelected() {\n * return this.selection.getSelection();\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { DestroyRef, ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport {\n SelectionPlugin,\n type CellRange,\n type SelectionChangeDetail,\n type SelectionResult,\n} from '@toolbox-web/grid/plugins/selection';\n\nregisterFeature('selection', (config) => {\n // Handle shorthand: 'cell', 'row', 'range'\n if (config === 'cell' || config === 'row' || config === 'range') {\n return new SelectionPlugin({ mode: config });\n }\n // Full config object\n return new SelectionPlugin(config ?? undefined);\n});\n\n/**\n * Selection methods returned from injectGridSelection.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n * This ensures it works with lazy-rendered tabs, conditional rendering, etc.\n */\nexport interface SelectionMethods {\n /**\n * Select all rows (row mode) or all cells (range mode).\n */\n selectAll: () => void;\n\n /**\n * Clear all selection.\n */\n clearSelection: () => void;\n\n /**\n * Get the current selection state (imperative, point-in-time snapshot).\n * For reactive selection state, use the `selection` signal instead.\n */\n getSelection: () => SelectionResult | null;\n\n /**\n * Check if a specific cell is selected.\n */\n isCellSelected: (row: number, col: number) => boolean;\n\n /**\n * Set selection ranges programmatically.\n */\n setRanges: (ranges: CellRange[]) => void;\n\n /**\n * Reactive selection state. Updates automatically whenever the selection changes.\n * Null when no SelectionPlugin is active or no selection has been made yet.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selection()?.ranges?.length ?? 0 }} cells selected\n *\n * // In computed:\n * readonly hasSelection = computed(() => (this.selection.selection()?.ranges?.length ?? 0) > 0);\n * ```\n */\n selection: Signal<SelectionResult | null>;\n\n /**\n * Reactive selected row indices (sorted ascending). Updates automatically.\n * Convenience signal for row-mode selection — returns `[]` in cell/range modes\n * or when nothing is selected.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selectedRowIndices().length }} rows selected\n *\n * // In computed:\n * readonly selectedRows = computed(() =>\n * this.selection.selectedRowIndices().map(i => this.rows[i])\n * );\n * ```\n */\n selectedRowIndices: Signal<number[]>;\n\n /**\n * Signal indicating if grid is ready.\n * The grid is discovered lazily, so this updates when first method call succeeds.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic selection control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization. This ensures it works reliably with:\n * - Lazy-rendered tabs\n * - Conditional rendering (*ngIf)\n * - Dynamic component loading\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/selection';\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handleSelectAll()\">Select All</button>\n * <tbw-grid [rows]=\"rows\" [selection]=\"'range'\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * selection = injectGridSelection();\n *\n * handleSelectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelectedRows() {\n * const selection = this.selection.getSelection();\n * if (!selection) return [];\n * // Derive rows from selection.ranges as needed\n * }\n * }\n * ```\n */\nexport function injectGridSelection<TRow = unknown>(): SelectionMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n // Reactive selection state\n const selectionSignal = signal<SelectionResult | null>(null);\n const selectedRowIndicesSignal = signal<number[]>([]);\n\n // Lazy discovery: cached grid reference\n let cachedGrid: DataGridElement<TRow> | null = null;\n let readyPromiseStarted = false;\n let listenerAttached = false;\n\n /**\n * Handle selection-change events from the grid.\n * Updates both reactive signals.\n */\n const onSelectionChange = (e: Event): void => {\n const detail = (e as CustomEvent<SelectionChangeDetail>).detail;\n const plugin = getPlugin();\n if (plugin) {\n selectionSignal.set(plugin.getSelection());\n selectedRowIndicesSignal.set(detail.mode === 'row' ? plugin.getSelectedRowIndices() : []);\n }\n };\n\n /**\n * Attach the selection-change event listener to the grid element.\n * Called once when the grid is first discovered.\n */\n const attachListener = (grid: DataGridElement<TRow>): void => {\n if (listenerAttached) return;\n listenerAttached = true;\n\n grid.addEventListener('selection-change', onSelectionChange);\n\n destroyRef.onDestroy(() => {\n grid.removeEventListener('selection-change', onSelectionChange);\n });\n };\n\n /**\n * Lazily find the grid element. Called on each method invocation.\n * Caches the reference once found and triggers ready() check.\n */\n const getGrid = (): DataGridElement<TRow> | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement<TRow> | null;\n if (grid) {\n cachedGrid = grid;\n attachListener(grid);\n // Start ready() check only once\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): SelectionPlugin | undefined => {\n return getGrid()?.getPlugin(SelectionPlugin);\n };\n\n return {\n isReady: isReady.asReadonly(),\n selection: selectionSignal.asReadonly(),\n selectedRowIndices: selectedRowIndicesSignal.asReadonly(),\n\n selectAll: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:selection] SelectionPlugin not found.\\n\\n` +\n ` → Enable selection on the grid:\\n` +\n ` <tbw-grid [selection]=\"'range'\" />`,\n );\n return;\n }\n const grid = getGrid();\n // Cast to any to access protected config\n const mode = (plugin as any).config?.mode;\n\n if (mode === 'row') {\n const rowCount = grid?.rows?.length ?? 0;\n const allIndices = new Set<number>();\n for (let i = 0; i < rowCount; i++) allIndices.add(i);\n (plugin as any).selected = allIndices;\n (plugin as any).requestAfterRender?.();\n } else if (mode === 'range') {\n const rowCount = grid?.rows?.length ?? 0;\n const colCount = (grid as any)?._columns?.length ?? 0;\n if (rowCount > 0 && colCount > 0) {\n plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: rowCount - 1, col: colCount - 1 } }]);\n }\n }\n },\n\n clearSelection: () => {\n getPlugin()?.clearSelection();\n },\n\n getSelection: () => {\n return getPlugin()?.getSelection() ?? null;\n },\n\n isCellSelected: (row: number, col: number) => {\n return getPlugin()?.isCellSelected(row, col) ?? false;\n },\n\n setRanges: (ranges: CellRange[]) => {\n getPlugin()?.setRanges(ranges);\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toolbox-web-grid-angular-features-selection.mjs","sources":["../../../../libs/grid-angular/features/selection/src/index.ts","../../../../libs/grid-angular/features/selection/src/toolbox-web-grid-angular-features-selection.ts"],"sourcesContent":["/**\n * Selection feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `selection` input on Grid directive.\n * Also exports `injectGridSelection()` for programmatic selection control.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/selection';\n *\n * <tbw-grid [selection]=\"'range'\" />\n * ```\n *\n * @example Using injectGridSelection\n * ```typescript\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({...})\n * export class MyComponent {\n * private selection = injectGridSelection<Employee>();\n *\n * selectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelected() {\n * return this.selection.getSelection();\n * }\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { afterNextRender, DestroyRef, ElementRef, inject, signal, type Signal } from '@angular/core';\nimport type { DataGridElement } from '@toolbox-web/grid';\nimport { registerFeature } from '@toolbox-web/grid-angular';\nimport {\n SelectionPlugin,\n type CellRange,\n type SelectionChangeDetail,\n type SelectionResult,\n} from '@toolbox-web/grid/plugins/selection';\n\nregisterFeature('selection', (config) => {\n // Handle shorthand: 'cell', 'row', 'range'\n if (config === 'cell' || config === 'row' || config === 'range') {\n return new SelectionPlugin({ mode: config });\n }\n // Full config object\n return new SelectionPlugin(config ?? undefined);\n});\n\n/**\n * Selection methods returned from injectGridSelection.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n * This ensures it works with lazy-rendered tabs, conditional rendering, etc.\n */\nexport interface SelectionMethods {\n /**\n * Select all rows (row mode) or all cells (range mode).\n */\n selectAll: () => void;\n\n /**\n * Clear all selection.\n */\n clearSelection: () => void;\n\n /**\n * Get the current selection state (imperative, point-in-time snapshot).\n * For reactive selection state, use the `selection` signal instead.\n */\n getSelection: () => SelectionResult | null;\n\n /**\n * Check if a specific cell is selected.\n */\n isCellSelected: (row: number, col: number) => boolean;\n\n /**\n * Set selection ranges programmatically.\n */\n setRanges: (ranges: CellRange[]) => void;\n\n /**\n * Reactive selection state. Updates automatically whenever the selection changes.\n * Null when no SelectionPlugin is active or no selection has been made yet.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selection()?.ranges?.length ?? 0 }} cells selected\n *\n * // In computed:\n * readonly hasSelection = computed(() => (this.selection.selection()?.ranges?.length ?? 0) > 0);\n * ```\n */\n selection: Signal<SelectionResult | null>;\n\n /**\n * Reactive selected row indices (sorted ascending). Updates automatically.\n * Convenience signal for row-mode selection — returns `[]` in cell/range modes\n * or when nothing is selected.\n *\n * @example\n * ```typescript\n * readonly selection = injectGridSelection();\n *\n * // In template:\n * // {{ selection.selectedRowIndices().length }} rows selected\n *\n * // In computed:\n * readonly selectedRows = computed(() =>\n * this.selection.selectedRowIndices().map(i => this.rows[i])\n * );\n * ```\n */\n selectedRowIndices: Signal<number[]>;\n\n /**\n * Signal indicating if grid is ready.\n * The grid is discovered lazily, so this updates when first method call succeeds.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic selection control.\n *\n * Uses **lazy grid discovery** - the grid element is found when methods are called,\n * not during initialization. This ensures it works reliably with:\n * - Lazy-rendered tabs\n * - Conditional rendering (*ngIf)\n * - Dynamic component loading\n *\n * @example\n * ```typescript\n * import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/selection';\n * import { injectGridSelection } from '@toolbox-web/grid-angular/features/selection';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * schemas: [CUSTOM_ELEMENTS_SCHEMA],\n * template: `\n * <button (click)=\"handleSelectAll()\">Select All</button>\n * <tbw-grid [rows]=\"rows\" [selection]=\"'range'\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * selection = injectGridSelection();\n *\n * handleSelectAll() {\n * this.selection.selectAll();\n * }\n *\n * getSelectedRows() {\n * const selection = this.selection.getSelection();\n * if (!selection) return [];\n * // Derive rows from selection.ranges as needed\n * }\n * }\n * ```\n */\nexport function injectGridSelection<TRow = unknown>(): SelectionMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n // Reactive selection state\n const selectionSignal = signal<SelectionResult | null>(null);\n const selectedRowIndicesSignal = signal<number[]>([]);\n\n // Lazy discovery: cached grid reference\n let cachedGrid: DataGridElement<TRow> | null = null;\n let readyPromiseStarted = false;\n let listenerAttached = false;\n\n /**\n * Handle selection-change events from the grid.\n * Updates both reactive signals.\n */\n const onSelectionChange = (e: Event): void => {\n const detail = (e as CustomEvent<SelectionChangeDetail>).detail;\n const plugin = getPlugin();\n if (plugin) {\n selectionSignal.set(plugin.getSelection());\n selectedRowIndicesSignal.set(detail.mode === 'row' ? plugin.getSelectedRowIndices() : []);\n }\n };\n\n /**\n * Attach the selection-change event listener to the grid element.\n * Called once when the grid is first discovered.\n */\n const attachListener = (grid: DataGridElement<TRow>): void => {\n if (listenerAttached) return;\n listenerAttached = true;\n\n grid.addEventListener('selection-change', onSelectionChange);\n\n destroyRef.onDestroy(() => {\n grid.removeEventListener('selection-change', onSelectionChange);\n });\n };\n\n /**\n * Lazily find the grid element. Called on each method invocation.\n * Caches the reference once found and triggers ready() check.\n */\n const getGrid = (): DataGridElement<TRow> | null => {\n if (cachedGrid) return cachedGrid;\n\n const grid = elementRef.nativeElement.querySelector('tbw-grid') as DataGridElement<TRow> | null;\n if (grid) {\n cachedGrid = grid;\n attachListener(grid);\n // Start ready() check only once\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): SelectionPlugin | undefined => {\n return getGrid()?.getPlugin(SelectionPlugin);\n };\n\n /**\n * Sync reactive signals with the current plugin state.\n * Called once when the grid is first discovered and ready.\n */\n const syncSignals = (): void => {\n const plugin = getPlugin();\n if (plugin) {\n selectionSignal.set(plugin.getSelection());\n const mode = (plugin as any).config?.mode;\n selectedRowIndicesSignal.set(mode === 'row' ? plugin.getSelectedRowIndices() : []);\n }\n };\n\n // Discover the grid after the first render so the selection-change\n // listener is attached without requiring a programmatic method call.\n // Uses a MutationObserver as fallback for lazy-rendered tabs, *ngIf,\n // @defer, etc. where the grid may not be in the DOM on first render.\n afterNextRender(() => {\n const grid = getGrid();\n if (grid) {\n grid.ready?.().then(syncSignals);\n return;\n }\n\n // Grid not in DOM yet — watch for it to appear.\n const host = elementRef.nativeElement as HTMLElement;\n const observer = new MutationObserver(() => {\n const discovered = getGrid();\n if (discovered) {\n observer.disconnect();\n discovered.ready?.().then(syncSignals);\n }\n });\n observer.observe(host, { childList: true, subtree: true });\n\n destroyRef.onDestroy(() => observer.disconnect());\n });\n\n return {\n isReady: isReady.asReadonly(),\n selection: selectionSignal.asReadonly(),\n selectedRowIndices: selectedRowIndicesSignal.asReadonly(),\n\n selectAll: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:selection] SelectionPlugin not found.\\n\\n` +\n ` → Enable selection on the grid:\\n` +\n ` <tbw-grid [selection]=\"'range'\" />`,\n );\n return;\n }\n const grid = getGrid();\n // Cast to any to access protected config\n const mode = (plugin as any).config?.mode;\n\n if (mode === 'row') {\n const rowCount = grid?.rows?.length ?? 0;\n const allIndices = new Set<number>();\n for (let i = 0; i < rowCount; i++) allIndices.add(i);\n (plugin as any).selected = allIndices;\n (plugin as any).requestAfterRender?.();\n } else if (mode === 'range') {\n const rowCount = grid?.rows?.length ?? 0;\n const colCount = (grid as any)?._columns?.length ?? 0;\n if (rowCount > 0 && colCount > 0) {\n plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: rowCount - 1, col: colCount - 1 } }]);\n }\n }\n },\n\n clearSelection: () => {\n getPlugin()?.clearSelection();\n },\n\n getSelection: () => {\n return getPlugin()?.getSelection() ?? null;\n },\n\n isCellSelected: (row: number, col: number) => {\n return getPlugin()?.isCellSelected(row, col) ?? false;\n },\n\n setRanges: (ranges: CellRange[]) => {\n getPlugin()?.setRanges(ranges);\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { inject, ElementRef, signal } from '@angular/core';
|
|
1
|
+
import { inject, ElementRef, DestroyRef, signal, afterNextRender } from '@angular/core';
|
|
2
2
|
import { registerFeature } from '@toolbox-web/grid-angular';
|
|
3
3
|
import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';
|
|
4
4
|
|
|
@@ -69,15 +69,48 @@ registerFeature('undoRedo', (config) => {
|
|
|
69
69
|
*/
|
|
70
70
|
function injectGridUndoRedo() {
|
|
71
71
|
const elementRef = inject(ElementRef);
|
|
72
|
+
const destroyRef = inject(DestroyRef);
|
|
72
73
|
const isReady = signal(false, ...(ngDevMode ? [{ debugName: "isReady" }] : []));
|
|
74
|
+
// Reactive undo/redo availability signals
|
|
75
|
+
const canUndoSignal = signal(false, ...(ngDevMode ? [{ debugName: "canUndoSignal" }] : []));
|
|
76
|
+
const canRedoSignal = signal(false, ...(ngDevMode ? [{ debugName: "canRedoSignal" }] : []));
|
|
73
77
|
let cachedGrid = null;
|
|
74
78
|
let readyPromiseStarted = false;
|
|
79
|
+
let listenerAttached = false;
|
|
80
|
+
/**
|
|
81
|
+
* Sync canUndo/canRedo signals with the current plugin state.
|
|
82
|
+
*/
|
|
83
|
+
const syncSignals = () => {
|
|
84
|
+
const plugin = getPlugin();
|
|
85
|
+
if (plugin) {
|
|
86
|
+
canUndoSignal.set(plugin.canUndo());
|
|
87
|
+
canRedoSignal.set(plugin.canRedo());
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Attach event listeners to the grid for undo/redo state changes.
|
|
92
|
+
* Listens for `undo`, `redo`, and `cell-commit` DOM events.
|
|
93
|
+
*/
|
|
94
|
+
const attachListeners = (grid) => {
|
|
95
|
+
if (listenerAttached)
|
|
96
|
+
return;
|
|
97
|
+
listenerAttached = true;
|
|
98
|
+
grid.addEventListener('undo', syncSignals);
|
|
99
|
+
grid.addEventListener('redo', syncSignals);
|
|
100
|
+
grid.addEventListener('cell-commit', syncSignals);
|
|
101
|
+
destroyRef.onDestroy(() => {
|
|
102
|
+
grid.removeEventListener('undo', syncSignals);
|
|
103
|
+
grid.removeEventListener('redo', syncSignals);
|
|
104
|
+
grid.removeEventListener('cell-commit', syncSignals);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
75
107
|
const getGrid = () => {
|
|
76
108
|
if (cachedGrid)
|
|
77
109
|
return cachedGrid;
|
|
78
110
|
const grid = elementRef.nativeElement.querySelector('tbw-grid');
|
|
79
111
|
if (grid) {
|
|
80
112
|
cachedGrid = grid;
|
|
113
|
+
attachListeners(grid);
|
|
81
114
|
if (!readyPromiseStarted) {
|
|
82
115
|
readyPromiseStarted = true;
|
|
83
116
|
grid.ready?.().then(() => isReady.set(true));
|
|
@@ -88,8 +121,30 @@ function injectGridUndoRedo() {
|
|
|
88
121
|
const getPlugin = () => {
|
|
89
122
|
return getGrid()?.getPlugin(UndoRedoPlugin);
|
|
90
123
|
};
|
|
124
|
+
// Eagerly discover the grid after the first render so event listeners
|
|
125
|
+
// are attached and isReady updates without requiring a programmatic
|
|
126
|
+
// method call. Falls back to MutationObserver for lazy-rendered content.
|
|
127
|
+
afterNextRender(() => {
|
|
128
|
+
const grid = getGrid();
|
|
129
|
+
if (grid) {
|
|
130
|
+
grid.ready?.().then(syncSignals);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const host = elementRef.nativeElement;
|
|
134
|
+
const observer = new MutationObserver(() => {
|
|
135
|
+
const discovered = getGrid();
|
|
136
|
+
if (discovered) {
|
|
137
|
+
observer.disconnect();
|
|
138
|
+
discovered.ready?.().then(syncSignals);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
observer.observe(host, { childList: true, subtree: true });
|
|
142
|
+
destroyRef.onDestroy(() => observer.disconnect());
|
|
143
|
+
});
|
|
91
144
|
return {
|
|
92
145
|
isReady: isReady.asReadonly(),
|
|
146
|
+
canUndo: canUndoSignal.asReadonly(),
|
|
147
|
+
canRedo: canRedoSignal.asReadonly(),
|
|
93
148
|
undo: () => {
|
|
94
149
|
const plugin = getPlugin();
|
|
95
150
|
if (!plugin) {
|
|
@@ -98,7 +153,9 @@ function injectGridUndoRedo() {
|
|
|
98
153
|
` <tbw-grid [editing]="'dblclick'" [undoRedo]="true" />`);
|
|
99
154
|
return null;
|
|
100
155
|
}
|
|
101
|
-
|
|
156
|
+
const result = plugin.undo();
|
|
157
|
+
syncSignals();
|
|
158
|
+
return result;
|
|
102
159
|
},
|
|
103
160
|
redo: () => {
|
|
104
161
|
const plugin = getPlugin();
|
|
@@ -108,10 +165,10 @@ function injectGridUndoRedo() {
|
|
|
108
165
|
` <tbw-grid [editing]="'dblclick'" [undoRedo]="true" />`);
|
|
109
166
|
return null;
|
|
110
167
|
}
|
|
111
|
-
|
|
168
|
+
const result = plugin.redo();
|
|
169
|
+
syncSignals();
|
|
170
|
+
return result;
|
|
112
171
|
},
|
|
113
|
-
canUndo: () => getPlugin()?.canUndo() ?? false,
|
|
114
|
-
canRedo: () => getPlugin()?.canRedo() ?? false,
|
|
115
172
|
clearHistory: () => {
|
|
116
173
|
const plugin = getPlugin();
|
|
117
174
|
if (!plugin) {
|
|
@@ -121,6 +178,7 @@ function injectGridUndoRedo() {
|
|
|
121
178
|
return;
|
|
122
179
|
}
|
|
123
180
|
plugin.clearHistory();
|
|
181
|
+
syncSignals();
|
|
124
182
|
},
|
|
125
183
|
getUndoStack: () => getPlugin()?.getUndoStack() ?? [],
|
|
126
184
|
getRedoStack: () => getPlugin()?.getRedoStack() ?? [],
|