@toolbox-web/grid-angular 0.14.3 → 0.16.0
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-undo-redo.mjs +32 -0
- package/fesm2022/toolbox-web-grid-angular-features-undo-redo.mjs.map +1 -1
- package/fesm2022/toolbox-web-grid-angular.mjs +59 -7
- 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 +25 -7
- package/types/toolbox-web-grid-angular-features-undo-redo.d.ts.map +1 -1
- package/types/toolbox-web-grid-angular.d.ts +20 -2
- package/types/toolbox-web-grid-angular.d.ts.map +1 -1
|
@@ -181,6 +181,38 @@ function injectGridUndoRedo() {
|
|
|
181
181
|
},
|
|
182
182
|
getUndoStack: () => getPlugin()?.getUndoStack() ?? [],
|
|
183
183
|
getRedoStack: () => getPlugin()?.getRedoStack() ?? [],
|
|
184
|
+
recordEdit: (rowIndex, field, oldValue, newValue) => {
|
|
185
|
+
const plugin = getPlugin();
|
|
186
|
+
if (!plugin) {
|
|
187
|
+
console.warn(`[tbw-grid:undoRedo] UndoRedoPlugin not found.\n\n` +
|
|
188
|
+
` → Enable undo/redo on the grid:\n` +
|
|
189
|
+
` <tbw-grid [editing]="'dblclick'" [undoRedo]="true" />`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
plugin.recordEdit(rowIndex, field, oldValue, newValue);
|
|
193
|
+
syncSignals();
|
|
194
|
+
},
|
|
195
|
+
beginTransaction: () => {
|
|
196
|
+
const plugin = getPlugin();
|
|
197
|
+
if (!plugin) {
|
|
198
|
+
console.warn(`[tbw-grid:undoRedo] UndoRedoPlugin not found.\n\n` +
|
|
199
|
+
` → Enable undo/redo on the grid:\n` +
|
|
200
|
+
` <tbw-grid [editing]="'dblclick'" [undoRedo]="true" />`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
plugin.beginTransaction();
|
|
204
|
+
},
|
|
205
|
+
endTransaction: () => {
|
|
206
|
+
const plugin = getPlugin();
|
|
207
|
+
if (!plugin) {
|
|
208
|
+
console.warn(`[tbw-grid:undoRedo] UndoRedoPlugin not found.\n\n` +
|
|
209
|
+
` → Enable undo/redo on the grid:\n` +
|
|
210
|
+
` <tbw-grid [editing]="'dblclick'" [undoRedo]="true" />`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
plugin.endTransaction();
|
|
214
|
+
syncSignals();
|
|
215
|
+
},
|
|
184
216
|
};
|
|
185
217
|
}
|
|
186
218
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbox-web-grid-angular-features-undo-redo.mjs","sources":["../../../../libs/grid-angular/features/undo-redo/src/index.ts","../../../../libs/grid-angular/features/undo-redo/src/toolbox-web-grid-angular-features-undo-redo.ts"],"sourcesContent":["/**\n * Undo/Redo feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `undoRedo` input on Grid directive.\n * Also exports `injectGridUndoRedo()` for programmatic undo/redo control.\n * Requires editing feature to be enabled.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/editing';\n * import '@toolbox-web/grid-angular/features/undo-redo';\n *\n * <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />\n * ```\n *\n * @example Using injectGridUndoRedo\n * ```typescript\n * import { injectGridUndoRedo } from '@toolbox-web/grid-angular/features/undo-redo';\n *\n * @Component({...})\n * export class MyComponent {\n * private undoRedo = injectGridUndoRedo();\n *\n * undo() { this.undoRedo.undo(); }\n * redo() { this.undoRedo.redo(); }\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 { UndoRedoPlugin, type EditAction } from '@toolbox-web/grid/plugins/undo-redo';\n\nregisterFeature('undoRedo', (config) => {\n if (config === true) {\n return new UndoRedoPlugin();\n }\n return new UndoRedoPlugin(config ?? undefined);\n});\n\n/**\n * Undo/Redo methods returned from injectGridUndoRedo.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n */\nexport interface UndoRedoMethods {\n /**\n * Undo the last edit action.\n * @returns The undone action, or null if nothing to undo\n */\n undo: () => EditAction | null;\n\n /**\n * Redo the last undone action.\n * @returns The redone action, or null if nothing to redo\n */\n redo: () => EditAction | null;\n\n /**\n * Reactive signal indicating whether undo is available.\n * Updates automatically when edits are made, undone, redone, or history is cleared.\n *\n * @example\n * ```typescript\n * // In template:\n * // <button [disabled]=\"!undoRedo.canUndo()\">Undo</button>\n *\n * // In computed:\n * readonly undoAvailable = computed(() => this.undoRedo.canUndo());\n * ```\n */\n canUndo: Signal<boolean>;\n\n /**\n * Reactive signal indicating whether redo is available.\n * Updates automatically when edits are made, undone, redone, or history is cleared.\n *\n * @example\n * ```typescript\n * // In template:\n * // <button [disabled]=\"!undoRedo.canRedo()\">Redo</button>\n *\n * // In computed:\n * readonly redoAvailable = computed(() => this.undoRedo.canRedo());\n * ```\n */\n canRedo: Signal<boolean>;\n\n /**\n * Clear all undo/redo history.\n */\n clearHistory: () => void;\n\n /**\n * Get a copy of the current undo stack.\n */\n getUndoStack: () => EditAction[];\n\n /**\n * Get a copy of the current redo stack.\n */\n getRedoStack: () => EditAction[];\n\n /**\n * Signal indicating if grid is ready.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic undo/redo 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 } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/editing';\n * import '@toolbox-web/grid-angular/features/undo-redo';\n * import { injectGridUndoRedo } from '@toolbox-web/grid-angular/features/undo-redo';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * template: `\n * <button (click)=\"undoRedo.undo()\" [disabled]=\"!undoRedo.canUndo()\">Undo</button>\n * <button (click)=\"undoRedo.redo()\" [disabled]=\"!undoRedo.canRedo()\">Redo</button>\n * <tbw-grid [rows]=\"rows\" [editing]=\"'dblclick'\" [undoRedo]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * undoRedo = injectGridUndoRedo();\n * }\n * ```\n */\nexport function injectGridUndoRedo(): UndoRedoMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n // Reactive undo/redo availability signals\n const canUndoSignal = signal(false);\n const canRedoSignal = signal(false);\n\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n let listenerAttached = false;\n\n /**\n * Sync canUndo/canRedo signals with the current plugin state.\n */\n const syncSignals = (): void => {\n const plugin = getPlugin();\n if (plugin) {\n canUndoSignal.set(plugin.canUndo());\n canRedoSignal.set(plugin.canRedo());\n }\n };\n\n /**\n * Attach event listeners to the grid for undo/redo state changes.\n * Listens for `undo`, `redo`, and `cell-commit` DOM events.\n */\n const attachListeners = (grid: DataGridElement): void => {\n if (listenerAttached) return;\n listenerAttached = true;\n\n grid.addEventListener('undo', syncSignals);\n grid.addEventListener('redo', syncSignals);\n grid.addEventListener('cell-commit', syncSignals);\n\n destroyRef.onDestroy(() => {\n grid.removeEventListener('undo', syncSignals);\n grid.removeEventListener('redo', syncSignals);\n grid.removeEventListener('cell-commit', syncSignals);\n });\n };\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 attachListeners(grid);\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): UndoRedoPlugin | undefined => {\n return getGrid()?.getPlugin(UndoRedoPlugin);\n };\n\n // Eagerly discover the grid after the first render so event listeners\n // are attached and isReady updates without requiring a programmatic\n // method call. Falls back to MutationObserver for lazy-rendered content.\n afterNextRender(() => {\n const grid = getGrid();\n if (grid) {\n grid.ready?.().then(syncSignals);\n return;\n }\n\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 canUndo: canUndoSignal.asReadonly(),\n canRedo: canRedoSignal.asReadonly(),\n\n undo: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return null;\n }\n const result = plugin.undo();\n syncSignals();\n return result;\n },\n\n redo: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return null;\n }\n const result = plugin.redo();\n syncSignals();\n return result;\n },\n\n clearHistory: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return;\n }\n plugin.clearHistory();\n syncSignals();\n },\n\n getUndoStack: () => getPlugin()?.getUndoStack() ?? [],\n\n getRedoStack: () => getPlugin()?.getRedoStack() ?? [],\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-undo-redo.mjs","sources":["../../../../libs/grid-angular/features/undo-redo/src/index.ts","../../../../libs/grid-angular/features/undo-redo/src/toolbox-web-grid-angular-features-undo-redo.ts"],"sourcesContent":["/**\n * Undo/Redo feature for @toolbox-web/grid-angular\n *\n * Import this module to enable the `undoRedo` input on Grid directive.\n * Also exports `injectGridUndoRedo()` for programmatic undo/redo control.\n * Requires editing feature to be enabled.\n *\n * @example\n * ```typescript\n * import '@toolbox-web/grid-angular/features/editing';\n * import '@toolbox-web/grid-angular/features/undo-redo';\n *\n * <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />\n * ```\n *\n * @example Using injectGridUndoRedo\n * ```typescript\n * import { injectGridUndoRedo } from '@toolbox-web/grid-angular/features/undo-redo';\n *\n * @Component({...})\n * export class MyComponent {\n * private undoRedo = injectGridUndoRedo();\n *\n * undo() { this.undoRedo.undo(); }\n * redo() { this.undoRedo.redo(); }\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 { UndoRedoPlugin, type UndoRedoAction } from '@toolbox-web/grid/plugins/undo-redo';\n\nregisterFeature('undoRedo', (config) => {\n if (config === true) {\n return new UndoRedoPlugin();\n }\n return new UndoRedoPlugin(config ?? undefined);\n});\n\n/**\n * Undo/Redo methods returned from injectGridUndoRedo.\n *\n * Uses lazy discovery - the grid is found on first method call, not during initialization.\n */\nexport interface UndoRedoMethods {\n /**\n * Undo the last edit action.\n * @returns The undone action (or compound action), or null if nothing to undo\n */\n undo: () => UndoRedoAction | null;\n\n /**\n * Redo the last undone action.\n * @returns The redone action (or compound action), or null if nothing to redo\n */\n redo: () => UndoRedoAction | null;\n\n /**\n * Reactive signal indicating whether undo is available.\n * Updates automatically when edits are made, undone, redone, or history is cleared.\n *\n * @example\n * ```typescript\n * // In template:\n * // <button [disabled]=\"!undoRedo.canUndo()\">Undo</button>\n *\n * // In computed:\n * readonly undoAvailable = computed(() => this.undoRedo.canUndo());\n * ```\n */\n canUndo: Signal<boolean>;\n\n /**\n * Reactive signal indicating whether redo is available.\n * Updates automatically when edits are made, undone, redone, or history is cleared.\n *\n * @example\n * ```typescript\n * // In template:\n * // <button [disabled]=\"!undoRedo.canRedo()\">Redo</button>\n *\n * // In computed:\n * readonly redoAvailable = computed(() => this.undoRedo.canRedo());\n * ```\n */\n canRedo: Signal<boolean>;\n\n /**\n * Clear all undo/redo history.\n */\n clearHistory: () => void;\n\n /**\n * Get a copy of the current undo stack.\n */\n getUndoStack: () => UndoRedoAction[];\n\n /**\n * Get a copy of the current redo stack.\n */\n getRedoStack: () => UndoRedoAction[];\n\n /**\n * Manually record an edit action.\n * If a transaction is active, the action is buffered; otherwise it's pushed to the undo stack.\n */\n recordEdit: (rowIndex: number, field: string, oldValue: unknown, newValue: unknown) => void;\n\n /**\n * Begin a transaction. All edits recorded until `endTransaction()` are grouped\n * into a single compound action for undo/redo.\n * @throws If a transaction is already active\n */\n beginTransaction: () => void;\n\n /**\n * End the active transaction and push the compound action to the undo stack.\n * If only one edit was recorded, it's pushed as a plain EditAction.\n * If no edits were recorded, the transaction is discarded.\n * @throws If no transaction is active\n */\n endTransaction: () => void;\n\n /**\n * Signal indicating if grid is ready.\n */\n isReady: Signal<boolean>;\n}\n\n/**\n * Angular inject function for programmatic undo/redo 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 } from '@angular/core';\n * import { Grid } from '@toolbox-web/grid-angular';\n * import '@toolbox-web/grid-angular/features/editing';\n * import '@toolbox-web/grid-angular/features/undo-redo';\n * import { injectGridUndoRedo } from '@toolbox-web/grid-angular/features/undo-redo';\n *\n * @Component({\n * selector: 'app-my-grid',\n * imports: [Grid],\n * template: `\n * <button (click)=\"undoRedo.undo()\" [disabled]=\"!undoRedo.canUndo()\">Undo</button>\n * <button (click)=\"undoRedo.redo()\" [disabled]=\"!undoRedo.canRedo()\">Redo</button>\n * <tbw-grid [rows]=\"rows\" [editing]=\"'dblclick'\" [undoRedo]=\"true\"></tbw-grid>\n * `\n * })\n * export class MyGridComponent {\n * undoRedo = injectGridUndoRedo();\n * }\n * ```\n */\nexport function injectGridUndoRedo(): UndoRedoMethods {\n const elementRef = inject(ElementRef);\n const destroyRef = inject(DestroyRef);\n const isReady = signal(false);\n\n // Reactive undo/redo availability signals\n const canUndoSignal = signal(false);\n const canRedoSignal = signal(false);\n\n let cachedGrid: DataGridElement | null = null;\n let readyPromiseStarted = false;\n let listenerAttached = false;\n\n /**\n * Sync canUndo/canRedo signals with the current plugin state.\n */\n const syncSignals = (): void => {\n const plugin = getPlugin();\n if (plugin) {\n canUndoSignal.set(plugin.canUndo());\n canRedoSignal.set(plugin.canRedo());\n }\n };\n\n /**\n * Attach event listeners to the grid for undo/redo state changes.\n * Listens for `undo`, `redo`, and `cell-commit` DOM events.\n */\n const attachListeners = (grid: DataGridElement): void => {\n if (listenerAttached) return;\n listenerAttached = true;\n\n grid.addEventListener('undo', syncSignals);\n grid.addEventListener('redo', syncSignals);\n grid.addEventListener('cell-commit', syncSignals);\n\n destroyRef.onDestroy(() => {\n grid.removeEventListener('undo', syncSignals);\n grid.removeEventListener('redo', syncSignals);\n grid.removeEventListener('cell-commit', syncSignals);\n });\n };\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 attachListeners(grid);\n if (!readyPromiseStarted) {\n readyPromiseStarted = true;\n grid.ready?.().then(() => isReady.set(true));\n }\n }\n return grid;\n };\n\n const getPlugin = (): UndoRedoPlugin | undefined => {\n return getGrid()?.getPlugin(UndoRedoPlugin);\n };\n\n // Eagerly discover the grid after the first render so event listeners\n // are attached and isReady updates without requiring a programmatic\n // method call. Falls back to MutationObserver for lazy-rendered content.\n afterNextRender(() => {\n const grid = getGrid();\n if (grid) {\n grid.ready?.().then(syncSignals);\n return;\n }\n\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 canUndo: canUndoSignal.asReadonly(),\n canRedo: canRedoSignal.asReadonly(),\n\n undo: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return null;\n }\n const result = plugin.undo();\n syncSignals();\n return result;\n },\n\n redo: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return null;\n }\n const result = plugin.redo();\n syncSignals();\n return result;\n },\n\n clearHistory: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return;\n }\n plugin.clearHistory();\n syncSignals();\n },\n\n getUndoStack: () => getPlugin()?.getUndoStack() ?? [],\n\n getRedoStack: () => getPlugin()?.getRedoStack() ?? [],\n\n recordEdit: (rowIndex: number, field: string, oldValue: unknown, newValue: unknown) => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return;\n }\n plugin.recordEdit(rowIndex, field, oldValue, newValue);\n syncSignals();\n },\n\n beginTransaction: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return;\n }\n plugin.beginTransaction();\n },\n\n endTransaction: () => {\n const plugin = getPlugin();\n if (!plugin) {\n console.warn(\n `[tbw-grid:undoRedo] UndoRedoPlugin not found.\\n\\n` +\n ` → Enable undo/redo on the grid:\\n` +\n ` <tbw-grid [editing]=\"'dblclick'\" [undoRedo]=\"true\" />`,\n );\n return;\n }\n plugin.endTransaction();\n syncSignals();\n },\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -2026,6 +2026,13 @@ class GridAdapter {
|
|
|
2026
2026
|
// This keeps Angular component editors in sync without manual DOM patching.
|
|
2027
2027
|
ctx.onValueChange?.((newVal) => {
|
|
2028
2028
|
try {
|
|
2029
|
+
// Notify the editor so it can clear stale internal state (e.g., searchText
|
|
2030
|
+
// in autocomplete editors) before the value input updates. This ensures the
|
|
2031
|
+
// template reads fresh state during the synchronous detectChanges() below.
|
|
2032
|
+
const instance = componentRef.instance;
|
|
2033
|
+
if (typeof instance['onExternalValueChange'] === 'function') {
|
|
2034
|
+
instance.onExternalValueChange(newVal);
|
|
2035
|
+
}
|
|
2029
2036
|
componentRef.setInput('value', newVal);
|
|
2030
2037
|
componentRef.changeDetectorRef.detectChanges();
|
|
2031
2038
|
}
|
|
@@ -2789,6 +2796,7 @@ class BaseGridEditor {
|
|
|
2789
2796
|
// ============================================================================
|
|
2790
2797
|
/**
|
|
2791
2798
|
* Emits when the user commits a new value.
|
|
2799
|
+
* Emits `null` when a nullable field is cleared.
|
|
2792
2800
|
*/
|
|
2793
2801
|
commit = output();
|
|
2794
2802
|
/**
|
|
@@ -2920,6 +2928,22 @@ class BaseGridEditor {
|
|
|
2920
2928
|
onEditClose() {
|
|
2921
2929
|
// Default: no-op. Subclasses override.
|
|
2922
2930
|
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Called by the grid adapter when the cell value changes externally
|
|
2933
|
+
* (e.g., via `updateRow()` cascade or undo/redo).
|
|
2934
|
+
*
|
|
2935
|
+
* Override in subclasses to reset internal state (search text, selection
|
|
2936
|
+
* flags, etc.) so the editor displays the updated value.
|
|
2937
|
+
*
|
|
2938
|
+
* This runs **synchronously** before the value input is updated, giving
|
|
2939
|
+
* the editor a chance to clear stale state before the next change-detection
|
|
2940
|
+
* pass re-reads the template.
|
|
2941
|
+
*
|
|
2942
|
+
* @param _newVal The new cell value being pushed from the grid
|
|
2943
|
+
*/
|
|
2944
|
+
onExternalValueChange(_newVal) {
|
|
2945
|
+
// Default: no-op. Subclasses override as needed.
|
|
2946
|
+
}
|
|
2923
2947
|
/**
|
|
2924
2948
|
* Commit a new value. Emits the commit output AND dispatches a DOM event.
|
|
2925
2949
|
* The DOM event enables the grid's auto-wiring to catch the commit.
|
|
@@ -3113,10 +3137,9 @@ class BaseGridEditorCVA extends BaseGridEditor {
|
|
|
3113
3137
|
this.cvaValue.set(value);
|
|
3114
3138
|
this._onChange(value);
|
|
3115
3139
|
this._onTouched();
|
|
3116
|
-
// Update grid
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
}
|
|
3140
|
+
// Update grid (null is valid for nullable columns — the grid's cell-commit
|
|
3141
|
+
// handler and EditingPlugin will apply it correctly)
|
|
3142
|
+
this.commitValue(value);
|
|
3120
3143
|
}
|
|
3121
3144
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: BaseGridEditorCVA, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
3122
3145
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.1", type: BaseGridEditorCVA, isStandalone: true, usesInheritance: true, ngImport: i0 });
|
|
@@ -3207,6 +3230,7 @@ let anchorCounter = 0;
|
|
|
3207
3230
|
* - **Escape handling** — closes the panel and returns focus to the inline input
|
|
3208
3231
|
* - **Synthetic Tab dispatch** — advances grid focus after overlay close
|
|
3209
3232
|
* - **Automatic teardown** — removes the panel from `<body>` and cleans up listeners
|
|
3233
|
+
* - **External focus registration** — auto-registers the panel via `grid.registerExternalFocusContainer()` so the grid keeps `data-has-focus` and editors stay open while the overlay has focus
|
|
3210
3234
|
*
|
|
3211
3235
|
* ## Usage
|
|
3212
3236
|
*
|
|
@@ -3329,6 +3353,9 @@ class BaseOverlayEditor extends BaseGridEditor {
|
|
|
3329
3353
|
}
|
|
3330
3354
|
// Move panel to body so it escapes grid overflow clipping
|
|
3331
3355
|
document.body.appendChild(panel);
|
|
3356
|
+
// Register the panel as an external focus container on the grid
|
|
3357
|
+
// so focus moving into the overlay is treated as "still in the grid"
|
|
3358
|
+
this._getGridElement()?.registerExternalFocusContainer?.(panel);
|
|
3332
3359
|
// Set up click-outside detection
|
|
3333
3360
|
this._abortCtrl = new AbortController();
|
|
3334
3361
|
document.addEventListener('pointerdown', (e) => this._onDocumentPointerDown(e), {
|
|
@@ -3388,6 +3415,10 @@ class BaseOverlayEditor extends BaseGridEditor {
|
|
|
3388
3415
|
this._abortCtrl = null;
|
|
3389
3416
|
this._focusObserver?.disconnect();
|
|
3390
3417
|
this._focusObserver = null;
|
|
3418
|
+
// Unregister the panel from the grid's external focus container registry
|
|
3419
|
+
if (this._panel) {
|
|
3420
|
+
this._getGridElement()?.unregisterExternalFocusContainer?.(this._panel);
|
|
3421
|
+
}
|
|
3391
3422
|
if (this._panel?.parentNode) {
|
|
3392
3423
|
this._panel.parentNode.removeChild(this._panel);
|
|
3393
3424
|
}
|
|
@@ -3500,6 +3531,10 @@ class BaseOverlayEditor extends BaseGridEditor {
|
|
|
3500
3531
|
_getCell() {
|
|
3501
3532
|
return this._elementRef.nativeElement.closest('[part="cell"]') ?? null;
|
|
3502
3533
|
}
|
|
3534
|
+
/** Find the parent `<tbw-grid>` element for this editor. */
|
|
3535
|
+
_getGridElement() {
|
|
3536
|
+
return this._elementRef.nativeElement.closest('tbw-grid') ?? null;
|
|
3537
|
+
}
|
|
3503
3538
|
/**
|
|
3504
3539
|
* JS fallback positioning for browsers without CSS Anchor Positioning.
|
|
3505
3540
|
* Uses `getBoundingClientRect()` with viewport overflow detection.
|
|
@@ -3597,13 +3632,18 @@ class BaseOverlayEditor extends BaseGridEditor {
|
|
|
3597
3632
|
if (!cell)
|
|
3598
3633
|
return;
|
|
3599
3634
|
let justOpened = false;
|
|
3635
|
+
let pendingHideRaf = 0;
|
|
3600
3636
|
this._focusObserver = new MutationObserver((mutations) => {
|
|
3601
3637
|
for (const mutation of mutations) {
|
|
3602
3638
|
if (mutation.type !== 'attributes' || mutation.attributeName !== 'class')
|
|
3603
3639
|
continue;
|
|
3604
3640
|
const isFocused = cell.classList.contains('cell-focus');
|
|
3605
3641
|
if (isFocused && !this._isOpen) {
|
|
3606
|
-
// Cell just gained focus — open overlay
|
|
3642
|
+
// Cell just gained focus — cancel any pending hide and open overlay.
|
|
3643
|
+
if (pendingHideRaf) {
|
|
3644
|
+
cancelAnimationFrame(pendingHideRaf);
|
|
3645
|
+
pendingHideRaf = 0;
|
|
3646
|
+
}
|
|
3607
3647
|
justOpened = true;
|
|
3608
3648
|
this.showOverlay();
|
|
3609
3649
|
this.onOverlayOpened();
|
|
@@ -3615,8 +3655,20 @@ class BaseOverlayEditor extends BaseGridEditor {
|
|
|
3615
3655
|
}, 0);
|
|
3616
3656
|
}
|
|
3617
3657
|
else if (!isFocused && this._isOpen && !justOpened) {
|
|
3618
|
-
// Cell lost focus — hide
|
|
3619
|
-
|
|
3658
|
+
// Cell lost focus — defer hide to allow render cycles to settle.
|
|
3659
|
+
// Re-renders (e.g., from ResizeObserver after a footer appears)
|
|
3660
|
+
// may transiently toggle cell-focus within the same frame.
|
|
3661
|
+
// Deferring to the next animation frame lets the render pipeline
|
|
3662
|
+
// finish before we decide whether the overlay should actually close.
|
|
3663
|
+
if (pendingHideRaf)
|
|
3664
|
+
cancelAnimationFrame(pendingHideRaf);
|
|
3665
|
+
pendingHideRaf = requestAnimationFrame(() => {
|
|
3666
|
+
pendingHideRaf = 0;
|
|
3667
|
+
// Re-check settled state — cell-focus may have been re-applied
|
|
3668
|
+
if (!cell.classList.contains('cell-focus') && this._isOpen) {
|
|
3669
|
+
this.hideOverlay(true);
|
|
3670
|
+
}
|
|
3671
|
+
});
|
|
3620
3672
|
}
|
|
3621
3673
|
}
|
|
3622
3674
|
});
|