@ornery/ui-grid-vanilla 0.1.10 → 1.0.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/README.md +196 -0
- package/demo/main.ts +0 -1
- package/dist/index.d.mts +575 -55
- package/dist/index.d.ts +575 -55
- package/dist/index.js +4894 -1503
- package/dist/index.mjs +4994 -1556
- package/package.json +11 -8
- package/scss-plugin.ts +65 -0
- package/src/adopt-styles.ts +39 -0
- package/src/attribute-bridge.ts +291 -0
- package/src/auto-height.test.ts +142 -0
- package/src/auto-resize.test.ts +206 -0
- package/src/cell-nav.test.ts +262 -0
- package/src/components/grid-body-cell/grid-body-cell.ts +23 -0
- package/src/components/grid-body-cell/index.ts +1 -0
- package/src/components/grid-cell-editor/grid-cell-editor.ts +104 -0
- package/src/components/grid-cell-editor/index.ts +1 -0
- package/src/components/grid-filter-cell/grid-filter-cell.scss +14 -0
- package/src/components/grid-filter-cell/grid-filter-cell.template.ts +20 -0
- package/src/components/grid-filter-cell/grid-filter-cell.ts +121 -0
- package/src/components/grid-filter-cell/index.ts +1 -0
- package/src/components/grid-group-row/grid-group-row.scss +24 -0
- package/src/components/grid-group-row/grid-group-row.template.ts +23 -0
- package/src/components/grid-group-row/grid-group-row.ts +94 -0
- package/src/components/grid-group-row/index.ts +1 -0
- package/src/components/grid-header-cell/grid-header-cell.ts +17 -0
- package/src/components/grid-header-cell/index.ts +1 -0
- package/src/components/grid-pagination/grid-pagination.scss +39 -0
- package/src/components/grid-pagination/grid-pagination.template.ts +40 -0
- package/src/components/grid-pagination/grid-pagination.ts +204 -0
- package/src/components/grid-pagination/index.ts +1 -0
- package/src/components/grid-template/grid-template.ts +166 -0
- package/src/components/grid-template/index.ts +1 -0
- package/src/events.ts +815 -0
- package/src/exporter.test.ts +186 -0
- package/src/focus.ts +353 -0
- package/src/framework-slots.ts +440 -0
- package/src/grid-controller.ts +1402 -18
- package/src/icons.ts +88 -0
- package/src/importer.test.ts +147 -0
- package/src/index.integration.test.ts +2 -2
- package/src/index.ts +9 -0
- package/src/infinite-scroll.test.ts +220 -0
- package/src/keyboard-navigation.test.ts +579 -0
- package/src/patch.ts +722 -0
- package/src/render.ts +465 -0
- package/src/row-edit.test.ts +191 -0
- package/src/row-selection.test.ts +550 -0
- package/src/save-state.test.ts +235 -0
- package/src/scss.d.ts +4 -0
- package/src/templates/index.ts +62 -0
- package/src/test-setup.ts +23 -0
- package/src/ui-grid-empty.template.ts +23 -0
- package/src/ui-grid-shell.scss +25 -0
- package/src/ui-grid-shell.template.ts +45 -0
- package/src/ui-grid-standalone.element.ts +741 -1112
- package/src/utils/cell-class.ts +70 -0
- package/src/utils/display-items.ts +15 -0
- package/src/utils/dom.ts +64 -0
- package/src/validate.test.ts +170 -0
- package/tsup.config.ts +10 -0
- package/vite.config.ts +10 -1
package/src/grid-controller.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
beginGridCellEditCommand,
|
|
4
4
|
buildGridRows,
|
|
5
5
|
buildGridCellContext,
|
|
6
|
+
buildInitialPinnedState,
|
|
6
7
|
cancelGridCellEditCommand,
|
|
7
8
|
canGridExpandRows,
|
|
8
9
|
canGridMoveColumns,
|
|
@@ -48,6 +49,64 @@ import {
|
|
|
48
49
|
pinGridColumnCommand,
|
|
49
50
|
resolveGridLabels,
|
|
50
51
|
resolveGridRowId as coreResolveGridRowId,
|
|
52
|
+
resolveGridSelectionOptions,
|
|
53
|
+
createGridSelectionState,
|
|
54
|
+
toggleGridRowSelection as coreToggleGridRowSelection,
|
|
55
|
+
shiftGridRowSelection as coreShiftGridRowSelection,
|
|
56
|
+
selectAllGridRows as coreSelectAllGridRows,
|
|
57
|
+
selectAllVisibleGridRows as coreSelectAllVisibleGridRows,
|
|
58
|
+
clearAllGridSelection as coreClearAllGridSelection,
|
|
59
|
+
findGridRowByKey as coreFindGridRowByKey,
|
|
60
|
+
reconcileGridSelection as coreReconcileGridSelection,
|
|
61
|
+
mapSelectedRowsToEntities as coreMapSelectedRowsToEntities,
|
|
62
|
+
buildGridCsv,
|
|
63
|
+
buildGridExcelSheetData,
|
|
64
|
+
buildGridPdfDocDefinition,
|
|
65
|
+
buildGridExporterMenuItems,
|
|
66
|
+
buildGridImporterMenuItems,
|
|
67
|
+
buildGridRowEditMenuItems,
|
|
68
|
+
type GridMenuItem,
|
|
69
|
+
resolveGridExporterExcelOptions,
|
|
70
|
+
resolveGridExporterOptions,
|
|
71
|
+
resolveGridExporterPdfOptions,
|
|
72
|
+
resolveExporterFilename,
|
|
73
|
+
downloadGridCsvFile,
|
|
74
|
+
sanitizeDownloadFilename,
|
|
75
|
+
createGridRowEditState,
|
|
76
|
+
markGridRowDirty,
|
|
77
|
+
markGridRowClean,
|
|
78
|
+
markGridRowSaving,
|
|
79
|
+
markGridRowError,
|
|
80
|
+
isGridRowEditTimerEnabled,
|
|
81
|
+
resolveGridRowEditWaitInterval,
|
|
82
|
+
resolveGridImporterOptions,
|
|
83
|
+
parseGridImporterJson,
|
|
84
|
+
parseGridImporterCsv,
|
|
85
|
+
buildGridImporterObjectsFromCsv,
|
|
86
|
+
buildGridImporterObjectsFromJson,
|
|
87
|
+
createGridValidatorRegistry,
|
|
88
|
+
runGridCellValidators,
|
|
89
|
+
validateAllGridRows,
|
|
90
|
+
isGridCellInvalid,
|
|
91
|
+
getGridCellErrorMessages,
|
|
92
|
+
GridValidatorRegistry,
|
|
93
|
+
type GridValidatorFactory,
|
|
94
|
+
type GridValidatorMessageFn,
|
|
95
|
+
type GridRowEditState,
|
|
96
|
+
type GridImporterOptions,
|
|
97
|
+
type GridExporterExcelSheetData,
|
|
98
|
+
type GridExporterMenuItem,
|
|
99
|
+
type GridExporterOptions,
|
|
100
|
+
type GridExporterColumnType,
|
|
101
|
+
type GridExporterPdfDocDefinition,
|
|
102
|
+
type GridExporterRowType,
|
|
103
|
+
type GridSelectionState,
|
|
104
|
+
type GridInfiniteScrollState,
|
|
105
|
+
maybeRequestInfiniteScrollData,
|
|
106
|
+
completeInfiniteScrollDataLoad,
|
|
107
|
+
resetInfiniteScrollState,
|
|
108
|
+
saveInfiniteScrollPercentage,
|
|
109
|
+
setInfiniteScrollDirectionsState,
|
|
51
110
|
seekGridPaginationCommand,
|
|
52
111
|
setGridPaginationPageSizeCommand,
|
|
53
112
|
setPathValue,
|
|
@@ -62,10 +121,12 @@ import {
|
|
|
62
121
|
type DisplayItem,
|
|
63
122
|
type GridCellPosition,
|
|
64
123
|
type GridColumnDef,
|
|
124
|
+
type GridRowColumn,
|
|
65
125
|
type GridLabels,
|
|
66
126
|
type GridOptions,
|
|
67
127
|
type GridRecord,
|
|
68
128
|
type GridRow,
|
|
129
|
+
type GridSavedState,
|
|
69
130
|
type PipelineResult,
|
|
70
131
|
type PinDirection,
|
|
71
132
|
type PinnedColumnState,
|
|
@@ -75,14 +136,31 @@ import {
|
|
|
75
136
|
type UiGridApi,
|
|
76
137
|
} from '@ornery/ui-grid-core';
|
|
77
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Complete grid save state — parity with the old `ui.grid.saveState`
|
|
141
|
+
* module. Every field is optional so partial saves / restores work too.
|
|
142
|
+
*
|
|
143
|
+
* Opt-in/out behaviour mirrors the old module: by default save() serialises
|
|
144
|
+
* everything; consumers can disable per-field capture via
|
|
145
|
+
* `saveSort`/`saveFilter`/... options on `GridOptions`. Restore tolerates
|
|
146
|
+
* missing fields.
|
|
147
|
+
*/
|
|
78
148
|
export interface GridSaveState {
|
|
79
|
-
sortState
|
|
80
|
-
activeFilters
|
|
81
|
-
groupByColumns
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
149
|
+
sortState?: SortState;
|
|
150
|
+
activeFilters?: Record<string, string>;
|
|
151
|
+
groupByColumns?: string[];
|
|
152
|
+
collapsedGroups?: Record<string, boolean>;
|
|
153
|
+
pinnedColumns?: PinnedColumnState;
|
|
154
|
+
columnOrder?: string[];
|
|
155
|
+
columnWidthOverrides?: Record<string, string>;
|
|
156
|
+
currentPage?: number;
|
|
157
|
+
pageSize?: number;
|
|
158
|
+
selectedRowIds?: string[];
|
|
159
|
+
focusedCell?: GridCellPosition | null;
|
|
160
|
+
expandedRows?: Record<string, boolean>;
|
|
161
|
+
expandedTreeRows?: Record<string, boolean>;
|
|
162
|
+
scrollTop?: number;
|
|
163
|
+
scrollLeft?: number;
|
|
86
164
|
}
|
|
87
165
|
|
|
88
166
|
export interface GridControllerSnapshot {
|
|
@@ -103,6 +181,13 @@ export interface GridControllerSnapshot {
|
|
|
103
181
|
pageSize: number;
|
|
104
182
|
editingCell: GridCellPosition | null;
|
|
105
183
|
editingValue: string;
|
|
184
|
+
/** Row ids currently selected. A ReadonlySet for surgical DOM updates —
|
|
185
|
+
* the renderer toggles the .ui-grid-row-selected class on every cell
|
|
186
|
+
* whose data-row matches one of these ids. */
|
|
187
|
+
selectedRowIds: ReadonlySet<string>;
|
|
188
|
+
selectAll: boolean;
|
|
189
|
+
focusedRowId: string | null;
|
|
190
|
+
selectedCount: number;
|
|
106
191
|
}
|
|
107
192
|
|
|
108
193
|
export type GridControllerSubscriber = (snapshot: GridControllerSnapshot) => void;
|
|
@@ -139,6 +224,26 @@ export class VanillaGridController {
|
|
|
139
224
|
private pageSize = 0;
|
|
140
225
|
private editingCell: GridCellPosition | null = null;
|
|
141
226
|
private editingValue = '';
|
|
227
|
+
private exporterOverrides: GridExporterOptions = {};
|
|
228
|
+
private validatorRegistry: GridValidatorRegistry;
|
|
229
|
+
private rowEditState: GridRowEditState = createGridRowEditState();
|
|
230
|
+
private rowEditTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
231
|
+
private rowEditSavePromiseOverrides = new Map<string, Promise<void>>();
|
|
232
|
+
private selectionState: GridSelectionState = createGridSelectionState();
|
|
233
|
+
/** cellNav state. Persists the last focused cell so the public API's
|
|
234
|
+
* getFocusedCell returns the right value across re-renders, and tracks
|
|
235
|
+
* the focused-cells history for getCurrentSelection. */
|
|
236
|
+
private cellNavLastRowCol: { rowId: string; columnName: string } | null = null;
|
|
237
|
+
private cellNavFocusedCells: { rowId: string; columnName: string }[] = [];
|
|
238
|
+
/** Infinite-scroll state. Direction flags seed from options; dataLoading
|
|
239
|
+
* + previousVisibleRows are runtime bookkeeping for saveScrollPercentage /
|
|
240
|
+
* dataRemovedTop / dataRemovedBottom. */
|
|
241
|
+
private infiniteScrollState: GridInfiniteScrollState = {
|
|
242
|
+
scrollUp: false,
|
|
243
|
+
scrollDown: true,
|
|
244
|
+
dataLoading: false,
|
|
245
|
+
previousVisibleRows: 0,
|
|
246
|
+
};
|
|
142
247
|
private pipeline: PipelineResult;
|
|
143
248
|
private labels: GridLabels;
|
|
144
249
|
private visibleColumns: GridColumnDef[] = [];
|
|
@@ -151,7 +256,18 @@ export class VanillaGridController {
|
|
|
151
256
|
constructor(options: GridOptions) {
|
|
152
257
|
this.options = options;
|
|
153
258
|
this.labels = resolveGridLabels(options.labels);
|
|
259
|
+
// Validator registry is built from the resolved labels so default
|
|
260
|
+
// validator messages ("Field required", etc.) come out localized
|
|
261
|
+
// without the consumer wiring anything up.
|
|
262
|
+
this.validatorRegistry = createGridValidatorRegistry(this.labels);
|
|
154
263
|
this.columnOrder = options.columnDefs.map((column) => column.name);
|
|
264
|
+
this.groupByColumns = options.grouping?.groupBy ? [...options.grouping.groupBy] : [];
|
|
265
|
+
this.pinnedColumns = buildInitialPinnedState(options.columnDefs);
|
|
266
|
+
|
|
267
|
+
this.infiniteScrollState = resetInfiniteScrollState(
|
|
268
|
+
options.infiniteScrollUp === true,
|
|
269
|
+
options.infiniteScrollDown !== false,
|
|
270
|
+
);
|
|
155
271
|
|
|
156
272
|
this.pipeline = {
|
|
157
273
|
visibleRows: [],
|
|
@@ -181,8 +297,17 @@ export class VanillaGridController {
|
|
|
181
297
|
toggleGrouping: (columnName) => this.toggleGrouping(columnName),
|
|
182
298
|
clearGrouping: () => this.clearGrouping(),
|
|
183
299
|
benchmark: (iterations) => this.benchmark(iterations),
|
|
184
|
-
exportCsv: () =>
|
|
185
|
-
|
|
300
|
+
exportCsv: (rowType, colType) => this.exportCsv(rowType, colType),
|
|
301
|
+
buildCsv: (rowType, colType) => this.buildCsv(rowType, colType),
|
|
302
|
+
pdfExport: (rowType, colType) => this.exportPdf(rowType, colType),
|
|
303
|
+
buildPdfDocDefinition: (rowType, colType) =>
|
|
304
|
+
this.buildPdfDocDefinition(rowType, colType),
|
|
305
|
+
excelExport: (rowType, colType) => this.exportExcel(rowType, colType),
|
|
306
|
+
buildExcelSheetData: (rowType, colType) => this.buildExcelSheetData(rowType, colType),
|
|
307
|
+
getExporterMenuItems: () => this.buildExporterMenuItems(),
|
|
308
|
+
getExporterOptions: () => ({ ...this.exporterOverrides, ...resolveGridExporterOptions(this.options) }),
|
|
309
|
+
setExporterOptions: (overrides) => {
|
|
310
|
+
this.exporterOverrides = { ...this.exporterOverrides, ...overrides };
|
|
186
311
|
},
|
|
187
312
|
paginationGetPage: () => this.getCurrentPage(),
|
|
188
313
|
paginationGetTotalPages: () => this.getTotalPages(),
|
|
@@ -225,14 +350,163 @@ export class VanillaGridController {
|
|
|
225
350
|
this.cancelCellEdit();
|
|
226
351
|
},
|
|
227
352
|
getEditingCell: () => this.editingCell,
|
|
353
|
+
toggleRowSelection: (rowEntity, evt) => this.toggleRowSelectionByEntity(rowEntity, evt),
|
|
354
|
+
selectRow: (rowEntity, evt) => this.selectRow(rowEntity, evt),
|
|
355
|
+
selectRowByVisibleIndex: (rowNum, evt) => this.selectRowByVisibleIndex(rowNum, evt),
|
|
356
|
+
selectRowByKey: (isInEntity, key, comparator, evt, lookInRows) =>
|
|
357
|
+
this.selectRowByKey(isInEntity, key, comparator, evt, lookInRows),
|
|
358
|
+
unSelectRow: (rowEntity, evt) => this.unSelectRow(rowEntity, evt),
|
|
359
|
+
unSelectRowByVisibleIndex: (rowNum, evt) => this.unSelectRowByVisibleIndex(rowNum, evt),
|
|
360
|
+
unSelectRowByKey: (isInEntity, key, comparator, evt, lookInRows) =>
|
|
361
|
+
this.unSelectRowByKey(isInEntity, key, comparator, evt, lookInRows),
|
|
362
|
+
selectAllRows: (evt) => this.selectAllRows(evt),
|
|
363
|
+
selectAllVisibleRows: (evt) => this.selectAllVisibleRows(evt),
|
|
364
|
+
clearSelectedRows: (evt) => this.clearSelectedRows(evt),
|
|
365
|
+
getSelectedRows: () => this.getSelectedRows(),
|
|
366
|
+
getUnSelectedRows: () => this.getUnSelectedRows(),
|
|
367
|
+
getSelectedGridRows: () => this.getSelectedGridRows(),
|
|
368
|
+
getUnSelectedGridRows: () => this.getUnSelectedGridRows(),
|
|
369
|
+
getSelectedCount: () => this.selectionState.selectedRowIds.size,
|
|
370
|
+
setMultiSelect: (multiSelect) => this.setMultiSelect(multiSelect),
|
|
371
|
+
setModifierKeysToMultiSelect: (value) => this.setModifierKeysToMultiSelect(value),
|
|
372
|
+
getSelectAllState: () => this.selectionState.selectAll,
|
|
373
|
+
shiftSelectRow: (rowEntity, evt) => this.shiftSelectRow(rowEntity, evt),
|
|
374
|
+
// cellNav bindings — thin wrappers over the same focus state the
|
|
375
|
+
// element uses. The element raises `cellNav.navigate` via
|
|
376
|
+
// controller.setCellNavFocus(); these methods let consumers inspect
|
|
377
|
+
// and re-drive that state.
|
|
378
|
+
scrollToFocus: (rowEntity, colDef) => this.cellNavScrollToFocus(rowEntity, colDef),
|
|
379
|
+
getFocusedCell: () => this.cellNavGetFocusedCell(),
|
|
380
|
+
getCurrentSelection: () => this.cellNavGetCurrentSelection(),
|
|
381
|
+
rowColSelectIndex: (rowCol) => this.cellNavRowColSelectIndex(rowCol),
|
|
382
|
+
// SaveState bindings — ports ui.grid.saveState public API.
|
|
383
|
+
saveState: () => this.getState() as GridSavedState,
|
|
384
|
+
restoreState: (state) => this.setState(state as Partial<GridSaveState>),
|
|
385
|
+
// Infinite-scroll bindings — ports ui.grid.infiniteScroll public API.
|
|
386
|
+
infiniteScrollDataLoaded: (scrollUp, scrollDown) =>
|
|
387
|
+
this.infiniteScrollDataLoaded(scrollUp, scrollDown),
|
|
388
|
+
infiniteScrollReset: (scrollUp, scrollDown) =>
|
|
389
|
+
this.infiniteScrollResetScroll(scrollUp, scrollDown),
|
|
390
|
+
infiniteScrollSaveScrollPercentage: () => this.infiniteScrollSavePercentage(),
|
|
391
|
+
infiniteScrollDataRemovedTop: (scrollUp, scrollDown) =>
|
|
392
|
+
this.infiniteScrollDataRemovedTop(scrollUp, scrollDown),
|
|
393
|
+
infiniteScrollDataRemovedBottom: (scrollUp, scrollDown) =>
|
|
394
|
+
this.infiniteScrollDataRemovedBottom(scrollUp, scrollDown),
|
|
395
|
+
infiniteScrollSetDirections: (scrollUp, scrollDown) =>
|
|
396
|
+
this.infiniteScrollSetDirections(scrollUp, scrollDown),
|
|
397
|
+
// Row-edit bindings — port of ui.grid.rowEdit public API.
|
|
398
|
+
rowEditSetSavePromise: (rowEntity, promise) =>
|
|
399
|
+
this.rowEditSetSavePromise(rowEntity, promise),
|
|
400
|
+
rowEditGetDirtyRows: () => this.rowEditGetDirtyRows(),
|
|
401
|
+
rowEditGetErrorRows: () => this.rowEditGetErrorRows(),
|
|
402
|
+
rowEditFlushDirtyRows: () => this.rowEditFlushDirtyRows(),
|
|
403
|
+
rowEditRetryErroredRows: () => this.rowEditRetryErroredRows(),
|
|
404
|
+
rowEditSetRowsDirty: (rowEntities) => this.rowEditSetRowsDirty(rowEntities),
|
|
405
|
+
rowEditSetRowsClean: (rowEntities) => this.rowEditSetRowsClean(rowEntities),
|
|
406
|
+
rowEditGetMenuItems: () => this.buildRowEditMenuItems(),
|
|
407
|
+
// Importer bindings — port of ui.grid.importer public API.
|
|
408
|
+
importerImportAFile: () => this.importerRequestFile(),
|
|
409
|
+
importerImportThisFile: (file) => this.importerImportThisFile(file),
|
|
410
|
+
importerImportText: (text, type) => this.importerImportText(text, type),
|
|
411
|
+
importerGetMenuItems: () => this.buildImporterMenuItems(),
|
|
412
|
+
// Validate bindings — port of ui.grid.validate public API.
|
|
413
|
+
validateIsInvalid: (rowEntity, colDef) => isGridCellInvalid(rowEntity, colDef),
|
|
414
|
+
validateGetErrorMessages: (rowEntity, colDef) =>
|
|
415
|
+
getGridCellErrorMessages(rowEntity, colDef, this.validatorRegistry),
|
|
416
|
+
validateGetFormattedErrors: (rowEntity, colDef) =>
|
|
417
|
+
this.validateGetFormattedErrors(rowEntity, colDef),
|
|
418
|
+
validateGetTitleFormattedErrors: (rowEntity, colDef) =>
|
|
419
|
+
this.validateGetTitleFormattedErrors(rowEntity, colDef),
|
|
420
|
+
validateRunValidators: (rowEntity, colDef, newValue, oldValue) =>
|
|
421
|
+
this.validateRunValidators(rowEntity, colDef, newValue, oldValue),
|
|
422
|
+
validateSetValidator: (name, factory, messageFn) =>
|
|
423
|
+
this.validatorRegistry.setValidator(name, factory, messageFn),
|
|
424
|
+
validateGetInvalidRows: () => this.validateGetInvalidRows(),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Subscribe row-edit to the edit events so a committed change flips the
|
|
428
|
+
// row into dirty state + schedules a save, and cancelling restarts the
|
|
429
|
+
// timer when the row was already dirty.
|
|
430
|
+
this.gridApi.edit.on.afterCellEdit((rowEntity, col, newValue, previousValue) => {
|
|
431
|
+
const rowId = this.resolveRowId(rowEntity);
|
|
432
|
+
const gridRow = this.findRowById(rowId);
|
|
433
|
+
if (!gridRow) return;
|
|
434
|
+
// Auto-run validators when a column declares them. Ports the old
|
|
435
|
+
// ui.grid.validate → edit integration where afterCellEdit kicked
|
|
436
|
+
// runValidators on the same cell. Async validators settle later;
|
|
437
|
+
// the re-emit makes sure the UI picks up the resulting invalid
|
|
438
|
+
// flag once they do.
|
|
439
|
+
if (col?.validators) {
|
|
440
|
+
void this.validateRunValidators(rowEntity, col, newValue, previousValue).then(() => {
|
|
441
|
+
this.emit();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (newValue === previousValue && !gridRow.isDirty) return;
|
|
445
|
+
markGridRowDirty(this.rowEditState, gridRow);
|
|
446
|
+
this.considerSetRowEditTimer(gridRow);
|
|
447
|
+
this.emit();
|
|
448
|
+
});
|
|
449
|
+
this.gridApi.edit.on.beginCellEdit((rowEntity) => {
|
|
450
|
+
const rowId = this.resolveRowId(rowEntity);
|
|
451
|
+
const gridRow = this.findRowById(rowId);
|
|
452
|
+
if (gridRow) this.cancelRowEditTimer(gridRow);
|
|
453
|
+
});
|
|
454
|
+
this.gridApi.edit.on.cancelCellEdit((rowEntity) => {
|
|
455
|
+
const rowId = this.resolveRowId(rowEntity);
|
|
456
|
+
const gridRow = this.findRowById(rowId);
|
|
457
|
+
if (gridRow) this.considerSetRowEditTimer(gridRow);
|
|
458
|
+
});
|
|
459
|
+
// Cellnav navigate — if the user leaves a dirty row, start its timer
|
|
460
|
+
// (matches old grid: a row only autosaves when focus moves off it or
|
|
461
|
+
// the debounce elapses).
|
|
462
|
+
this.gridApi.cellNav.on.navigate((_newRowCol, oldRowCol) => {
|
|
463
|
+
if (!oldRowCol) return;
|
|
464
|
+
this.considerSetRowEditTimer(oldRowCol.row);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// When the i18n service swaps the active language, re-resolve labels
|
|
468
|
+
// + re-register the default validators (their messages are locale-
|
|
469
|
+
// driven) + re-render so every cell/tooltip picks up the new strings.
|
|
470
|
+
this.disposeLanguageListener = this.gridApi.i18n.on.languageChanged(() => {
|
|
471
|
+
this.labels = resolveGridLabels(this.options.labels);
|
|
472
|
+
const rebuilt = createGridValidatorRegistry(this.labels);
|
|
473
|
+
for (const name of ['required', 'minLength', 'maxLength']) {
|
|
474
|
+
this.validatorRegistry.setValidator(
|
|
475
|
+
name,
|
|
476
|
+
(arg: unknown) => rebuilt.getValidator(name, arg),
|
|
477
|
+
(arg: unknown) => rebuilt.getMessage(name, arg),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
this.refresh();
|
|
228
481
|
});
|
|
229
482
|
|
|
230
483
|
this.refresh();
|
|
231
484
|
}
|
|
232
485
|
|
|
486
|
+
/** Disposer for the i18n language-change listener — cleared when the
|
|
487
|
+
* controller is torn down (matches the pattern used for other global
|
|
488
|
+
* subscriptions). */
|
|
489
|
+
private disposeLanguageListener: (() => void) | null = null;
|
|
490
|
+
dispose(): void {
|
|
491
|
+
this.disposeLanguageListener?.();
|
|
492
|
+
this.disposeLanguageListener = null;
|
|
493
|
+
this.subscribers.clear();
|
|
494
|
+
}
|
|
495
|
+
|
|
233
496
|
setOptions(options: GridOptions): void {
|
|
234
497
|
this.options = options;
|
|
235
498
|
this.labels = resolveGridLabels(options.labels);
|
|
499
|
+
// Re-register the built-in validators so their messages track the
|
|
500
|
+
// refreshed locale. Consumer-registered validators survive untouched
|
|
501
|
+
// because we write them onto the same registry instance.
|
|
502
|
+
const rebuiltDefaults = createGridValidatorRegistry(this.labels);
|
|
503
|
+
for (const name of ['required', 'minLength', 'maxLength']) {
|
|
504
|
+
this.validatorRegistry.setValidator(
|
|
505
|
+
name,
|
|
506
|
+
(arg: unknown) => rebuiltDefaults.getValidator(name, arg),
|
|
507
|
+
(arg: unknown) => rebuiltDefaults.getMessage(name, arg),
|
|
508
|
+
);
|
|
509
|
+
}
|
|
236
510
|
if (this.columnOrder.length === 0) {
|
|
237
511
|
this.columnOrder = options.columnDefs.map((column) => column.name);
|
|
238
512
|
} else {
|
|
@@ -245,6 +519,37 @@ export class VanillaGridController {
|
|
|
245
519
|
}
|
|
246
520
|
}
|
|
247
521
|
|
|
522
|
+
// Apply declarative grouping from options when provided. Matches the
|
|
523
|
+
// Angular element's behaviour — the consumer can set
|
|
524
|
+
// `grouping: { groupBy: [...] }` and it takes effect immediately.
|
|
525
|
+
// When the option is absent, leave the current groupByColumns untouched
|
|
526
|
+
// so interactive toggles persist across non-grouping option updates.
|
|
527
|
+
if (options.grouping?.groupBy !== undefined) {
|
|
528
|
+
this.groupByColumns = [...options.grouping.groupBy];
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Re-seed pinned state from columnDefs whenever a columnDef declares
|
|
532
|
+
// `pinnedLeft` / `pinnedRight`. This mirrors the Angular element, which
|
|
533
|
+
// re-derives pinned state on every options update, so switching demo
|
|
534
|
+
// scenarios (different column sets) picks up their declarative pins.
|
|
535
|
+
const declaresPin = options.columnDefs.some(
|
|
536
|
+
(col) => col.pinnedLeft === true || col.pinnedRight === true,
|
|
537
|
+
);
|
|
538
|
+
if (declaresPin) {
|
|
539
|
+
this.pinnedColumns = buildInitialPinnedState(options.columnDefs);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Re-seed infinite-scroll directions when the options declare them,
|
|
543
|
+
// so consumers can flip scrollUp/scrollDown through `grid.options`
|
|
544
|
+
// without needing to go through setScrollDirections.
|
|
545
|
+
if (options.infiniteScrollUp !== undefined || options.infiniteScrollDown !== undefined) {
|
|
546
|
+
this.infiniteScrollState = setInfiniteScrollDirectionsState(
|
|
547
|
+
this.infiniteScrollState,
|
|
548
|
+
options.infiniteScrollUp ?? this.infiniteScrollState.scrollUp,
|
|
549
|
+
options.infiniteScrollDown ?? this.infiniteScrollState.scrollDown,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
248
553
|
this.apiRegistered = false;
|
|
249
554
|
this.refresh();
|
|
250
555
|
}
|
|
@@ -301,6 +606,10 @@ export class VanillaGridController {
|
|
|
301
606
|
pageSize: this.getEffectivePageSize(),
|
|
302
607
|
editingCell: this.editingCell,
|
|
303
608
|
editingValue: this.editingValue,
|
|
609
|
+
selectedRowIds: this.selectionState.selectedRowIds,
|
|
610
|
+
selectAll: this.selectionState.selectAll,
|
|
611
|
+
focusedRowId: this.selectionState.focusedRowId,
|
|
612
|
+
selectedCount: this.selectionState.selectedRowIds.size,
|
|
304
613
|
};
|
|
305
614
|
}
|
|
306
615
|
|
|
@@ -520,6 +829,7 @@ export class VanillaGridController {
|
|
|
520
829
|
row: GridRow | GridRecord | string,
|
|
521
830
|
columnName: string,
|
|
522
831
|
triggerEvent?: Event | KeyboardEvent | null,
|
|
832
|
+
initialValue?: string,
|
|
523
833
|
): void {
|
|
524
834
|
const rowId = this.resolveRowId(row);
|
|
525
835
|
const gridRow = this.findRowById(rowId);
|
|
@@ -545,6 +855,7 @@ export class VanillaGridController {
|
|
|
545
855
|
column,
|
|
546
856
|
getCellValue(gridRow.entity, column),
|
|
547
857
|
triggerEvent,
|
|
858
|
+
initialValue,
|
|
548
859
|
);
|
|
549
860
|
|
|
550
861
|
this.refresh();
|
|
@@ -556,6 +867,15 @@ export class VanillaGridController {
|
|
|
556
867
|
}
|
|
557
868
|
|
|
558
869
|
commitCellEdit(): void {
|
|
870
|
+
// Guard against re-entry: committing rebuilds the cell markup, which
|
|
871
|
+
// removes the focused <input> and fires blur — whose handler also tries
|
|
872
|
+
// to commit. Without this early-return, the nested render mutates DOM
|
|
873
|
+
// that the outer render hasn't finished unwinding yet, throwing
|
|
874
|
+
// "node to be removed is no longer a child of this node."
|
|
875
|
+
if (this.editingCell === null) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
559
879
|
commitGridCellEditCommand(this.gridApi, {
|
|
560
880
|
getEditingCell: () => this.editingCell,
|
|
561
881
|
getEditingValue: () => this.editingValue,
|
|
@@ -576,6 +896,10 @@ export class VanillaGridController {
|
|
|
576
896
|
}
|
|
577
897
|
|
|
578
898
|
cancelCellEdit(): void {
|
|
899
|
+
if (this.editingCell === null) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
579
903
|
cancelGridCellEditCommand(this.gridApi, {
|
|
580
904
|
getEditingCell: () => this.editingCell,
|
|
581
905
|
setEditingCell: (next) => {
|
|
@@ -591,6 +915,464 @@ export class VanillaGridController {
|
|
|
591
915
|
this.refresh();
|
|
592
916
|
}
|
|
593
917
|
|
|
918
|
+
// ---- Row selection -------------------------------------------------
|
|
919
|
+
// Thin method layer on top of grid.core.selection. Each call mutates
|
|
920
|
+
// this.selectionState + row instances in place, raises the appropriate
|
|
921
|
+
// public-API event (single vs batch), then refreshes so the element
|
|
922
|
+
// re-renders with the updated `isSelected` / `isFocused` flags.
|
|
923
|
+
|
|
924
|
+
/** Internal: run a core selection mutation, raise events, trigger a render. */
|
|
925
|
+
private applySelectionChange(
|
|
926
|
+
apply: () => { changed: GridRow[] },
|
|
927
|
+
evt?: Event | null,
|
|
928
|
+
): void {
|
|
929
|
+
const { changed } = apply();
|
|
930
|
+
if (changed.length === 0) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (this.options.enableSelectionBatchEvent !== false && changed.length > 1) {
|
|
934
|
+
this.gridApi.selection.raise.rowSelectionChangedBatch(changed, evt);
|
|
935
|
+
} else {
|
|
936
|
+
for (const row of changed) {
|
|
937
|
+
this.gridApi.selection.raise.rowSelectionChanged(row, evt);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
this.refresh();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private allGridRows(): GridRow[] {
|
|
944
|
+
// Use the cached pipeline row list when possible — it already reflects
|
|
945
|
+
// `options.data`. Fall back to rebuilding when the pipeline is empty
|
|
946
|
+
// (e.g. before first refresh).
|
|
947
|
+
return this.pipeline.visibleRows.length > 0
|
|
948
|
+
? this.pipeline.visibleRows
|
|
949
|
+
: this.buildRowsFromData(this.options.data);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
toggleRowSelectionByEntity(rowEntity: GridRecord, evt?: Event | null): void {
|
|
953
|
+
const row = this.findRowById(this.resolveRowId(rowEntity));
|
|
954
|
+
if (!row) return;
|
|
955
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
956
|
+
this.applySelectionChange(
|
|
957
|
+
() =>
|
|
958
|
+
coreToggleGridRowSelection(this.selectionState, this.allGridRows(), row, {
|
|
959
|
+
multiSelect: resolved.multiSelect,
|
|
960
|
+
noUnselect: resolved.noUnselect,
|
|
961
|
+
}),
|
|
962
|
+
evt,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
selectRow(rowEntity: GridRecord, evt?: Event | null): void {
|
|
967
|
+
const row = this.findRowById(this.resolveRowId(rowEntity));
|
|
968
|
+
if (!row || row.isSelected) return;
|
|
969
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
970
|
+
this.applySelectionChange(
|
|
971
|
+
() =>
|
|
972
|
+
coreToggleGridRowSelection(this.selectionState, this.allGridRows(), row, {
|
|
973
|
+
multiSelect: resolved.multiSelect,
|
|
974
|
+
noUnselect: resolved.noUnselect,
|
|
975
|
+
}),
|
|
976
|
+
evt,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
selectRowByVisibleIndex(rowNum: number, evt?: Event | null): void {
|
|
981
|
+
const rows = this.pipeline.visibleRows;
|
|
982
|
+
const row = rows[rowNum];
|
|
983
|
+
if (!row || row.isSelected) return;
|
|
984
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
985
|
+
this.applySelectionChange(
|
|
986
|
+
() =>
|
|
987
|
+
coreToggleGridRowSelection(this.selectionState, rows, row, {
|
|
988
|
+
multiSelect: resolved.multiSelect,
|
|
989
|
+
noUnselect: resolved.noUnselect,
|
|
990
|
+
canBeInvisible: false,
|
|
991
|
+
}),
|
|
992
|
+
evt,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
selectRowByKey(
|
|
997
|
+
isInEntity: boolean,
|
|
998
|
+
key: string,
|
|
999
|
+
comparator: unknown,
|
|
1000
|
+
evt?: Event | null,
|
|
1001
|
+
lookInRows?: readonly GridRow[],
|
|
1002
|
+
): void {
|
|
1003
|
+
const rows = lookInRows ?? this.allGridRows();
|
|
1004
|
+
const row = coreFindGridRowByKey(rows, isInEntity, key, comparator);
|
|
1005
|
+
if (!row || row.isSelected) return;
|
|
1006
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1007
|
+
this.applySelectionChange(
|
|
1008
|
+
() =>
|
|
1009
|
+
coreToggleGridRowSelection(this.selectionState, this.allGridRows(), row, {
|
|
1010
|
+
multiSelect: resolved.multiSelect,
|
|
1011
|
+
noUnselect: resolved.noUnselect,
|
|
1012
|
+
canBeInvisible: false,
|
|
1013
|
+
}),
|
|
1014
|
+
evt,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
unSelectRow(rowEntity: GridRecord, evt?: Event | null): void {
|
|
1019
|
+
const row = this.findRowById(this.resolveRowId(rowEntity));
|
|
1020
|
+
if (!row || !row.isSelected) return;
|
|
1021
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1022
|
+
this.applySelectionChange(
|
|
1023
|
+
() =>
|
|
1024
|
+
coreToggleGridRowSelection(this.selectionState, this.allGridRows(), row, {
|
|
1025
|
+
multiSelect: resolved.multiSelect,
|
|
1026
|
+
noUnselect: resolved.noUnselect,
|
|
1027
|
+
}),
|
|
1028
|
+
evt,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
unSelectRowByVisibleIndex(rowNum: number, evt?: Event | null): void {
|
|
1033
|
+
const row = this.pipeline.visibleRows[rowNum];
|
|
1034
|
+
if (!row || !row.isSelected) return;
|
|
1035
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1036
|
+
this.applySelectionChange(
|
|
1037
|
+
() =>
|
|
1038
|
+
coreToggleGridRowSelection(this.selectionState, this.allGridRows(), row, {
|
|
1039
|
+
multiSelect: resolved.multiSelect,
|
|
1040
|
+
noUnselect: resolved.noUnselect,
|
|
1041
|
+
canBeInvisible: false,
|
|
1042
|
+
}),
|
|
1043
|
+
evt,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
unSelectRowByKey(
|
|
1048
|
+
isInEntity: boolean,
|
|
1049
|
+
key: string,
|
|
1050
|
+
comparator: unknown,
|
|
1051
|
+
evt?: Event | null,
|
|
1052
|
+
lookInRows?: readonly GridRow[],
|
|
1053
|
+
): void {
|
|
1054
|
+
const rows = lookInRows ?? this.allGridRows();
|
|
1055
|
+
const row = coreFindGridRowByKey(rows, isInEntity, key, comparator);
|
|
1056
|
+
if (!row || !row.isSelected) return;
|
|
1057
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1058
|
+
this.applySelectionChange(
|
|
1059
|
+
() =>
|
|
1060
|
+
coreToggleGridRowSelection(this.selectionState, this.allGridRows(), row, {
|
|
1061
|
+
multiSelect: resolved.multiSelect,
|
|
1062
|
+
noUnselect: resolved.noUnselect,
|
|
1063
|
+
canBeInvisible: false,
|
|
1064
|
+
}),
|
|
1065
|
+
evt,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
selectAllRows(evt?: Event | null): void {
|
|
1070
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1071
|
+
this.applySelectionChange(
|
|
1072
|
+
() =>
|
|
1073
|
+
coreSelectAllGridRows(this.selectionState, this.allGridRows(), {
|
|
1074
|
+
multiSelect: resolved.multiSelect,
|
|
1075
|
+
isRowSelectable: resolved.isRowSelectable,
|
|
1076
|
+
}),
|
|
1077
|
+
evt,
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
selectAllVisibleRows(evt?: Event | null): void {
|
|
1082
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1083
|
+
this.applySelectionChange(
|
|
1084
|
+
() =>
|
|
1085
|
+
coreSelectAllVisibleGridRows(this.selectionState, this.allGridRows(), {
|
|
1086
|
+
multiSelect: resolved.multiSelect,
|
|
1087
|
+
isRowSelectable: resolved.isRowSelectable,
|
|
1088
|
+
}),
|
|
1089
|
+
evt,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
clearSelectedRows(evt?: Event | null): void {
|
|
1094
|
+
this.applySelectionChange(
|
|
1095
|
+
() => coreClearAllGridSelection(this.selectionState, this.allGridRows()),
|
|
1096
|
+
evt,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
shiftSelectRow(rowEntity: GridRecord, evt?: Event | null): void {
|
|
1101
|
+
const row = this.findRowById(this.resolveRowId(rowEntity));
|
|
1102
|
+
if (!row) return;
|
|
1103
|
+
const resolved = resolveGridSelectionOptions(this.options);
|
|
1104
|
+
this.applySelectionChange(
|
|
1105
|
+
() =>
|
|
1106
|
+
coreShiftGridRowSelection(this.selectionState, this.pipeline.visibleRows, row, {
|
|
1107
|
+
multiSelect: resolved.multiSelect,
|
|
1108
|
+
}),
|
|
1109
|
+
evt,
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
getSelectedRows(): GridRecord[] {
|
|
1114
|
+
return coreMapSelectedRowsToEntities(this.getSelectedGridRows());
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
getUnSelectedRows(): GridRecord[] {
|
|
1118
|
+
return coreMapSelectedRowsToEntities(this.getUnSelectedGridRows());
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
getSelectedGridRows(): GridRow[] {
|
|
1122
|
+
return this.allGridRows().filter((row) => row.isSelected);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
getUnSelectedGridRows(): GridRow[] {
|
|
1126
|
+
return this.allGridRows().filter((row) => !row.isSelected);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
setMultiSelect(multiSelect: boolean): void {
|
|
1130
|
+
this.options = { ...this.options, multiSelect };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
setModifierKeysToMultiSelect(value: boolean): void {
|
|
1134
|
+
this.options = { ...this.options, modifierKeysToMultiSelect: value };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Focus a row by id — the old module's `row.setFocused(true)` with all
|
|
1138
|
+
* the book-keeping. Raises rowFocusChanged and refreshes. */
|
|
1139
|
+
setRowFocused(rowId: string, focused: boolean, evt?: Event | null): void {
|
|
1140
|
+
const row = this.findRowById(rowId);
|
|
1141
|
+
if (!row) return;
|
|
1142
|
+
const previousFocusedId = this.selectionState.focusedRowId;
|
|
1143
|
+
if (focused) {
|
|
1144
|
+
if (previousFocusedId && previousFocusedId !== rowId) {
|
|
1145
|
+
const previous = this.findRowById(previousFocusedId);
|
|
1146
|
+
previous?.setFocused(false);
|
|
1147
|
+
}
|
|
1148
|
+
row.setFocused(true);
|
|
1149
|
+
this.selectionState.focusedRowId = rowId;
|
|
1150
|
+
} else {
|
|
1151
|
+
row.setFocused(false);
|
|
1152
|
+
if (previousFocusedId === rowId) this.selectionState.focusedRowId = null;
|
|
1153
|
+
}
|
|
1154
|
+
this.gridApi.selection.raise.rowFocusChanged(row, evt);
|
|
1155
|
+
this.refresh();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
getRowSelectionState(): Readonly<GridSelectionState> {
|
|
1159
|
+
return this.selectionState;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Resolved/defaulted selection options — exposed for the element layer
|
|
1163
|
+
* so it can decide whether to wire pointer handlers without duplicating
|
|
1164
|
+
* default logic. */
|
|
1165
|
+
getResolvedSelectionOptions(): ReturnType<typeof resolveGridSelectionOptions> {
|
|
1166
|
+
return resolveGridSelectionOptions(this.options);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Public analogue of findRowById — the element uses this during drag
|
|
1170
|
+
* painting to resolve a row-id back to its entity without calling the
|
|
1171
|
+
* selection API methods that would double-raise events. */
|
|
1172
|
+
findRowByIdPublic(rowId: string): GridRow | null {
|
|
1173
|
+
return this.findRowById(rowId);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ---- End row selection ---------------------------------------------
|
|
1177
|
+
|
|
1178
|
+
// ---- cellNav -------------------------------------------------------
|
|
1179
|
+
// State + accessors for the cellNav public API. Element owns the DOM
|
|
1180
|
+
// focus; controller owns the logical "last focused cell" and the
|
|
1181
|
+
// focused-cells history. Element calls setCellNavFocus() whenever the
|
|
1182
|
+
// active cell changes.
|
|
1183
|
+
|
|
1184
|
+
/** Record a navigation event. Pushes to the focused-cells history,
|
|
1185
|
+
* updates lastRowCol, and raises cellNav.navigate on the public API. */
|
|
1186
|
+
setCellNavFocus(
|
|
1187
|
+
rowId: string | null,
|
|
1188
|
+
columnName: string | null,
|
|
1189
|
+
opts: { appendToSelection?: boolean } = {},
|
|
1190
|
+
): void {
|
|
1191
|
+
const previous = this.cellNavLastRowCol;
|
|
1192
|
+
const previousRowCol = this.resolveCellNavRowCol(previous);
|
|
1193
|
+
if (rowId && columnName) {
|
|
1194
|
+
this.cellNavLastRowCol = { rowId, columnName };
|
|
1195
|
+
if (opts.appendToSelection) {
|
|
1196
|
+
this.cellNavFocusedCells = [
|
|
1197
|
+
...this.cellNavFocusedCells.filter(
|
|
1198
|
+
(entry) => !(entry.rowId === rowId && entry.columnName === columnName),
|
|
1199
|
+
),
|
|
1200
|
+
{ rowId, columnName },
|
|
1201
|
+
];
|
|
1202
|
+
} else {
|
|
1203
|
+
this.cellNavFocusedCells = [{ rowId, columnName }];
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
this.cellNavLastRowCol = null;
|
|
1207
|
+
this.cellNavFocusedCells = [];
|
|
1208
|
+
}
|
|
1209
|
+
const nextRowCol = this.resolveCellNavRowCol(this.cellNavLastRowCol);
|
|
1210
|
+
this.gridApi.cellNav.raise.navigate(nextRowCol, previousRowCol);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** Raise viewPortKeyDown / viewPortKeyPress on behalf of the element.
|
|
1214
|
+
* Element gates this with `options.keyDownOverrides`. */
|
|
1215
|
+
raiseCellNavKeyEvent(
|
|
1216
|
+
type: 'keydown' | 'keypress',
|
|
1217
|
+
event: KeyboardEvent,
|
|
1218
|
+
): void {
|
|
1219
|
+
const rowCol = this.resolveCellNavRowCol(this.cellNavLastRowCol);
|
|
1220
|
+
if (type === 'keydown') this.gridApi.cellNav.raise.viewPortKeyDown(event, rowCol);
|
|
1221
|
+
else this.gridApi.cellNav.raise.viewPortKeyPress(event, rowCol);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
private cellNavGetFocusedCell(): GridRowColumn | null {
|
|
1225
|
+
return this.resolveCellNavRowCol(this.cellNavLastRowCol);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
private cellNavGetCurrentSelection(): GridRowColumn[] {
|
|
1229
|
+
const out: GridRowColumn[] = [];
|
|
1230
|
+
for (const entry of this.cellNavFocusedCells) {
|
|
1231
|
+
const rowCol = this.resolveCellNavRowCol(entry);
|
|
1232
|
+
if (rowCol) out.push(rowCol);
|
|
1233
|
+
}
|
|
1234
|
+
return out;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private cellNavRowColSelectIndex(rowCol: GridRowColumn): number {
|
|
1238
|
+
for (let i = 0; i < this.cellNavFocusedCells.length; i++) {
|
|
1239
|
+
const entry = this.cellNavFocusedCells[i]!;
|
|
1240
|
+
if (entry.rowId === rowCol.row.id && entry.columnName === rowCol.col.name) {
|
|
1241
|
+
return i;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return -1;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private resolveCellNavRowCol(
|
|
1248
|
+
position: { rowId: string; columnName: string } | null,
|
|
1249
|
+
): GridRowColumn | null {
|
|
1250
|
+
if (!position) return null;
|
|
1251
|
+
const row = this.findRowById(position.rowId);
|
|
1252
|
+
const col = this.findColumnByName(position.columnName);
|
|
1253
|
+
if (!row || !col) return null;
|
|
1254
|
+
return { row, col };
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/** cellNav.scrollToFocus — resolves rowEntity + colDef into a logical
|
|
1258
|
+
* focus and delegates to the element (if attached) for actual scrolling.
|
|
1259
|
+
* When the element isn't wired (e.g. bare controller in tests), the
|
|
1260
|
+
* state still updates and the promise resolves immediately. */
|
|
1261
|
+
private async cellNavScrollToFocus(
|
|
1262
|
+
rowEntity: GridRecord | null,
|
|
1263
|
+
colDef: GridColumnDef | null,
|
|
1264
|
+
): Promise<void> {
|
|
1265
|
+
const rowId = rowEntity ? this.resolveRowId(rowEntity) : null;
|
|
1266
|
+
const columnName = colDef?.name ?? null;
|
|
1267
|
+
this.setCellNavFocus(rowId, columnName);
|
|
1268
|
+
this.cellNavScrollRequest?.(rowId, columnName);
|
|
1269
|
+
return Promise.resolve();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** Element registers its scroll-and-focus handler here so the API
|
|
1273
|
+
* binding above can delegate DOM work. Controller stays DOM-free. */
|
|
1274
|
+
private cellNavScrollRequest: ((rowId: string | null, columnName: string | null) => void) | null = null;
|
|
1275
|
+
setCellNavScrollHandler(handler: ((rowId: string | null, columnName: string | null) => void) | null): void {
|
|
1276
|
+
this.cellNavScrollRequest = handler;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** Read-only accessor for the current options. Used by the element to
|
|
1280
|
+
* inspect keyDownOverrides / modifierKeysToMultiSelectCells without
|
|
1281
|
+
* coupling it to a private field. */
|
|
1282
|
+
getOptions(): GridOptions {
|
|
1283
|
+
return this.options;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// ---- End cellNav ---------------------------------------------------
|
|
1287
|
+
|
|
1288
|
+
// ---- Infinite scroll ----------------------------------------------
|
|
1289
|
+
// Thin wrappers over grid.core.infinite-scroll's pure helpers. The
|
|
1290
|
+
// element calls `evaluateInfiniteScroll` from its scroll-frame callback;
|
|
1291
|
+
// these helpers raise needLoadMoreData / needLoadMoreDataTop when the
|
|
1292
|
+
// user approaches the end/top of the dataset.
|
|
1293
|
+
|
|
1294
|
+
/** Evaluate whether the current scroll position should request more
|
|
1295
|
+
* data at the top or bottom. Called from the element's scroll frame. */
|
|
1296
|
+
evaluateInfiniteScroll(startIndex: number, visibleRows: number, viewportRows: number): void {
|
|
1297
|
+
if (this.options.enableInfiniteScroll === false) return;
|
|
1298
|
+
const threshold = this.options.infiniteScrollRowsFromEnd ?? 20;
|
|
1299
|
+
const { request, nextState } = maybeRequestInfiniteScrollData({
|
|
1300
|
+
state: this.infiniteScrollState,
|
|
1301
|
+
startIndex,
|
|
1302
|
+
visibleRows,
|
|
1303
|
+
viewportRows,
|
|
1304
|
+
threshold,
|
|
1305
|
+
});
|
|
1306
|
+
if (request === null) return;
|
|
1307
|
+
this.infiniteScrollState = nextState;
|
|
1308
|
+
if (request === 'top') this.gridApi.infiniteScroll.raise.needLoadMoreDataTop();
|
|
1309
|
+
else this.gridApi.infiniteScroll.raise.needLoadMoreData();
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private infiniteScrollDataLoaded(scrollUp?: boolean, scrollDown?: boolean): Promise<void> {
|
|
1313
|
+
this.infiniteScrollState = completeInfiniteScrollDataLoad(
|
|
1314
|
+
this.infiniteScrollState,
|
|
1315
|
+
scrollUp ?? this.infiniteScrollState.scrollUp,
|
|
1316
|
+
scrollDown ?? this.infiniteScrollState.scrollDown,
|
|
1317
|
+
);
|
|
1318
|
+
this.refresh();
|
|
1319
|
+
return Promise.resolve();
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private infiniteScrollResetScroll(scrollUp?: boolean, scrollDown?: boolean): void {
|
|
1323
|
+
this.infiniteScrollState = resetInfiniteScrollState(
|
|
1324
|
+
scrollUp ?? false,
|
|
1325
|
+
scrollDown ?? true,
|
|
1326
|
+
);
|
|
1327
|
+
this.infiniteScrollScrollToTopRequest?.();
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private infiniteScrollSavePercentage(): void {
|
|
1331
|
+
this.infiniteScrollState = saveInfiniteScrollPercentage(
|
|
1332
|
+
this.infiniteScrollState,
|
|
1333
|
+
this.pipeline.visibleRows.length,
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private infiniteScrollDataRemovedTop(scrollUp?: boolean, scrollDown?: boolean): void {
|
|
1338
|
+
this.infiniteScrollState = setInfiniteScrollDirectionsState(
|
|
1339
|
+
this.infiniteScrollState,
|
|
1340
|
+
scrollUp ?? this.infiniteScrollState.scrollUp,
|
|
1341
|
+
scrollDown ?? this.infiniteScrollState.scrollDown,
|
|
1342
|
+
);
|
|
1343
|
+
this.refresh();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
private infiniteScrollDataRemovedBottom(scrollUp?: boolean, scrollDown?: boolean): void {
|
|
1347
|
+
this.infiniteScrollState = setInfiniteScrollDirectionsState(
|
|
1348
|
+
this.infiniteScrollState,
|
|
1349
|
+
scrollUp ?? this.infiniteScrollState.scrollUp,
|
|
1350
|
+
scrollDown ?? this.infiniteScrollState.scrollDown,
|
|
1351
|
+
);
|
|
1352
|
+
this.refresh();
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private infiniteScrollSetDirections(scrollUp: boolean, scrollDown: boolean): void {
|
|
1356
|
+
this.infiniteScrollState = setInfiniteScrollDirectionsState(
|
|
1357
|
+
this.infiniteScrollState,
|
|
1358
|
+
scrollUp,
|
|
1359
|
+
scrollDown,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** Element registers a scroll-to-top handler here so resetScroll can
|
|
1364
|
+
* bring the viewport back to 0. Keeps the controller DOM-free. */
|
|
1365
|
+
private infiniteScrollScrollToTopRequest: (() => void) | null = null;
|
|
1366
|
+
setInfiniteScrollResetHandler(handler: (() => void) | null): void {
|
|
1367
|
+
this.infiniteScrollScrollToTopRequest = handler;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
getInfiniteScrollState(): Readonly<GridInfiniteScrollState> {
|
|
1371
|
+
return this.infiniteScrollState;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// ---- End infinite scroll ------------------------------------------
|
|
1375
|
+
|
|
594
1376
|
canResizeColumns(): boolean {
|
|
595
1377
|
return this.options.enableColumnResizing !== false;
|
|
596
1378
|
}
|
|
@@ -678,6 +1460,21 @@ export class VanillaGridController {
|
|
|
678
1460
|
return isColumnPinnable(this.options, column);
|
|
679
1461
|
}
|
|
680
1462
|
|
|
1463
|
+
/** True when the given column is editable in principle — does NOT evaluate
|
|
1464
|
+
* a per-row `cellEditableCondition`. Use isCellEditable for that. */
|
|
1465
|
+
isColumnEditable(column: GridColumnDef): boolean {
|
|
1466
|
+
return column.enableCellEdit ?? this.options.enableCellEdit ?? false;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/** True when the (row, column) cell is editable, respecting both the
|
|
1470
|
+
* column's `enableCellEdit` flag and any `cellEditableCondition`. */
|
|
1471
|
+
isCellEditable(row: GridRow, column: GridColumnDef): boolean {
|
|
1472
|
+
if (!this.isColumnEditable(column)) return false;
|
|
1473
|
+
const condition = column.cellEditableCondition ?? this.options.cellEditableCondition ?? true;
|
|
1474
|
+
if (typeof condition === 'boolean') return condition;
|
|
1475
|
+
return condition({ row: row.entity, column, rowIndex: row.index });
|
|
1476
|
+
}
|
|
1477
|
+
|
|
681
1478
|
isPinned(column: GridColumnDef): boolean {
|
|
682
1479
|
return this.pinnedColumns[column.name] !== undefined;
|
|
683
1480
|
}
|
|
@@ -740,27 +1537,121 @@ export class VanillaGridController {
|
|
|
740
1537
|
return this.editingCell?.rowId === rowId && this.editingCell?.columnName === columnName;
|
|
741
1538
|
}
|
|
742
1539
|
|
|
1540
|
+
/**
|
|
1541
|
+
* Serialise the current grid state. Mirrors `ui.grid.saveState.save`:
|
|
1542
|
+
* every field is gated by an `options.save*` flag with the old grid's
|
|
1543
|
+
* defaults. A missing flag keeps the matching field out of the result.
|
|
1544
|
+
*/
|
|
743
1545
|
getState(): GridSaveState {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1546
|
+
const opts = this.options;
|
|
1547
|
+
const saveWidths = opts.saveWidths !== false;
|
|
1548
|
+
const saveOrder = opts.saveOrder !== false;
|
|
1549
|
+
const saveVisible = opts.saveVisible !== false;
|
|
1550
|
+
const saveSort = opts.saveSort !== false;
|
|
1551
|
+
const saveFilter = opts.saveFilter !== false;
|
|
1552
|
+
const saveSelection = opts.saveSelection !== false;
|
|
1553
|
+
const saveGrouping = opts.saveGrouping !== false;
|
|
1554
|
+
const saveGroupingExpandedStates = opts.saveGroupingExpandedStates === true;
|
|
1555
|
+
const savePinning = opts.savePinning !== false;
|
|
1556
|
+
const saveTreeView = opts.saveTreeView !== false;
|
|
1557
|
+
const savePagination = opts.savePagination !== false;
|
|
1558
|
+
const saveScroll = opts.saveScroll === true;
|
|
1559
|
+
const saveFocus = saveScroll ? false : opts.saveFocus !== false;
|
|
1560
|
+
void saveVisible; // Visible column state is derived from columnOrder + defs;
|
|
1561
|
+
// kept as an opt-flag for forward compat.
|
|
1562
|
+
|
|
1563
|
+
const state: GridSaveState = {};
|
|
1564
|
+
if (saveSort) state.sortState = { ...this.sortState };
|
|
1565
|
+
if (saveFilter) state.activeFilters = { ...this.activeFilters };
|
|
1566
|
+
if (saveGrouping) state.groupByColumns = [...this.groupByColumns];
|
|
1567
|
+
if (saveGrouping && saveGroupingExpandedStates) {
|
|
1568
|
+
state.collapsedGroups = { ...this.collapsedGroups };
|
|
1569
|
+
}
|
|
1570
|
+
if (savePinning) state.pinnedColumns = { ...this.pinnedColumns };
|
|
1571
|
+
if (saveOrder) state.columnOrder = [...this.columnOrder];
|
|
1572
|
+
if (saveWidths) state.columnWidthOverrides = { ...this.columnWidthOverrides };
|
|
1573
|
+
if (savePagination) {
|
|
1574
|
+
state.currentPage = this.currentPage;
|
|
1575
|
+
state.pageSize = this.pageSize;
|
|
1576
|
+
}
|
|
1577
|
+
if (saveSelection) {
|
|
1578
|
+
state.selectedRowIds = [...this.selectionState.selectedRowIds];
|
|
1579
|
+
}
|
|
1580
|
+
if (saveTreeView) {
|
|
1581
|
+
state.expandedRows = { ...this.expandedRows };
|
|
1582
|
+
state.expandedTreeRows = { ...this.expandedTreeRows };
|
|
1583
|
+
}
|
|
1584
|
+
if (saveFocus) {
|
|
1585
|
+
state.focusedCell = this.cellNavLastRowCol
|
|
1586
|
+
? {
|
|
1587
|
+
rowId: this.cellNavLastRowCol.rowId,
|
|
1588
|
+
columnName: this.cellNavLastRowCol.columnName,
|
|
1589
|
+
}
|
|
1590
|
+
: null;
|
|
1591
|
+
}
|
|
1592
|
+
if (saveScroll && this.saveStateScrollSnapshot) {
|
|
1593
|
+
const snap = this.saveStateScrollSnapshot();
|
|
1594
|
+
state.scrollTop = snap.scrollTop;
|
|
1595
|
+
state.scrollLeft = snap.scrollLeft;
|
|
1596
|
+
}
|
|
1597
|
+
return state;
|
|
753
1598
|
}
|
|
754
1599
|
|
|
1600
|
+
/**
|
|
1601
|
+
* Apply a previously saved state. All fields are optional — missing
|
|
1602
|
+
* fields are left at whatever the controller currently has. Matches the
|
|
1603
|
+
* old grid's `ui.grid.saveState.restore`.
|
|
1604
|
+
*/
|
|
755
1605
|
setState(state: Partial<GridSaveState>): void {
|
|
756
1606
|
if (state.sortState !== undefined) this.sortState = { ...state.sortState };
|
|
757
1607
|
if (state.activeFilters !== undefined) this.activeFilters = { ...state.activeFilters };
|
|
758
1608
|
if (state.groupByColumns !== undefined) this.groupByColumns = [...state.groupByColumns];
|
|
1609
|
+
if (state.collapsedGroups !== undefined) this.collapsedGroups = { ...state.collapsedGroups };
|
|
759
1610
|
if (state.pinnedColumns !== undefined) this.pinnedColumns = { ...state.pinnedColumns };
|
|
760
1611
|
if (state.columnOrder !== undefined) this.columnOrder = [...state.columnOrder];
|
|
1612
|
+
if (state.columnWidthOverrides !== undefined) this.columnWidthOverrides = { ...state.columnWidthOverrides };
|
|
761
1613
|
if (state.currentPage !== undefined) this.currentPage = state.currentPage;
|
|
762
1614
|
if (state.pageSize !== undefined) this.pageSize = state.pageSize;
|
|
1615
|
+
if (state.expandedRows !== undefined) this.expandedRows = { ...state.expandedRows };
|
|
1616
|
+
if (state.expandedTreeRows !== undefined) this.expandedTreeRows = { ...state.expandedTreeRows };
|
|
1617
|
+
if (state.selectedRowIds !== undefined) {
|
|
1618
|
+
// Rebuild the selection Set in place — selectedRowIds is readonly on
|
|
1619
|
+
// GridSelectionState so we mutate the existing collection rather
|
|
1620
|
+
// than replacing the reference.
|
|
1621
|
+
this.selectionState.selectedRowIds.clear();
|
|
1622
|
+
for (const id of state.selectedRowIds) this.selectionState.selectedRowIds.add(id);
|
|
1623
|
+
this.selectionState.selectAll = false;
|
|
1624
|
+
}
|
|
1625
|
+
if (state.focusedCell !== undefined) {
|
|
1626
|
+
if (state.focusedCell) {
|
|
1627
|
+
this.cellNavLastRowCol = {
|
|
1628
|
+
rowId: state.focusedCell.rowId,
|
|
1629
|
+
columnName: state.focusedCell.columnName,
|
|
1630
|
+
};
|
|
1631
|
+
this.cellNavFocusedCells = [{ ...this.cellNavLastRowCol }];
|
|
1632
|
+
} else {
|
|
1633
|
+
this.cellNavLastRowCol = null;
|
|
1634
|
+
this.cellNavFocusedCells = [];
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
763
1637
|
this.refresh();
|
|
1638
|
+
// Scroll restoration needs to run after render so the scroll container
|
|
1639
|
+
// exists and its scrollHeight is final.
|
|
1640
|
+
if (state.scrollTop !== undefined || state.scrollLeft !== undefined) {
|
|
1641
|
+
this.saveStateScrollRestore?.(state.scrollTop ?? 0, state.scrollLeft ?? 0);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/** Element injects its scroll accessor so save() can capture scrollTop/
|
|
1646
|
+
* scrollLeft. Controller stays DOM-free. */
|
|
1647
|
+
private saveStateScrollSnapshot: (() => { scrollTop: number; scrollLeft: number }) | null = null;
|
|
1648
|
+
private saveStateScrollRestore: ((scrollTop: number, scrollLeft: number) => void) | null = null;
|
|
1649
|
+
setSaveStateScrollHandlers(
|
|
1650
|
+
snapshot: (() => { scrollTop: number; scrollLeft: number }) | null,
|
|
1651
|
+
restore: ((scrollTop: number, scrollLeft: number) => void) | null,
|
|
1652
|
+
): void {
|
|
1653
|
+
this.saveStateScrollSnapshot = snapshot;
|
|
1654
|
+
this.saveStateScrollRestore = restore;
|
|
764
1655
|
}
|
|
765
1656
|
|
|
766
1657
|
private refresh(): void {
|
|
@@ -790,6 +1681,29 @@ export class VanillaGridController {
|
|
|
790
1681
|
this.visibleColumns = applyWidthOverrides([...pinnedLeft, ...middleColumns, ...pinnedRight]);
|
|
791
1682
|
}
|
|
792
1683
|
|
|
1684
|
+
// Selection row-header column. Injected in front of everything else
|
|
1685
|
+
// (including left-pinned columns) so it always sits at the far left,
|
|
1686
|
+
// mirroring the old grid's addRowHeaderColumn(def, 0). The width is
|
|
1687
|
+
// configurable via selectionRowHeaderWidth.
|
|
1688
|
+
const resolvedSelection = resolveGridSelectionOptions(this.options);
|
|
1689
|
+
if (resolvedSelection.enableRowSelection && resolvedSelection.enableRowHeaderSelection) {
|
|
1690
|
+
const selectionCol: GridColumnDef = {
|
|
1691
|
+
name: 'selectionRowHeaderCol',
|
|
1692
|
+
displayName: '',
|
|
1693
|
+
width: `${resolvedSelection.selectionRowHeaderWidth}px`,
|
|
1694
|
+
enableSorting: false,
|
|
1695
|
+
enableFiltering: false,
|
|
1696
|
+
enableGrouping: false,
|
|
1697
|
+
enableCellEdit: false,
|
|
1698
|
+
enablePinning: false,
|
|
1699
|
+
align: 'center',
|
|
1700
|
+
};
|
|
1701
|
+
// Dedupe if already present (setOptions could be re-entering).
|
|
1702
|
+
if (!this.visibleColumns.some((c) => c.name === 'selectionRowHeaderCol')) {
|
|
1703
|
+
this.visibleColumns = [selectionCol, ...this.visibleColumns];
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
793
1707
|
this.pipeline = defaultGridEngine.buildPipeline({
|
|
794
1708
|
options: this.options,
|
|
795
1709
|
columns: this.visibleColumns,
|
|
@@ -805,6 +1719,16 @@ export class VanillaGridController {
|
|
|
805
1719
|
rowSize: this.getRowSize(),
|
|
806
1720
|
});
|
|
807
1721
|
|
|
1722
|
+
// Re-apply the persisted selection/focus flags to the fresh row instances
|
|
1723
|
+
// produced by the pipeline. Without this, every refresh would wipe the
|
|
1724
|
+
// selected-row indicators because new GridRow objects default to
|
|
1725
|
+
// isSelected=false. Also evaluates isRowSelectable per row.
|
|
1726
|
+
coreReconcileGridSelection(
|
|
1727
|
+
this.selectionState,
|
|
1728
|
+
this.pipeline.visibleRows,
|
|
1729
|
+
this.options.isRowSelectable ?? null,
|
|
1730
|
+
);
|
|
1731
|
+
|
|
808
1732
|
if (!this.apiRegistered) {
|
|
809
1733
|
this.options.onRegisterApi?.(this.gridApi);
|
|
810
1734
|
this.apiRegistered = true;
|
|
@@ -821,6 +1745,12 @@ export class VanillaGridController {
|
|
|
821
1745
|
}
|
|
822
1746
|
|
|
823
1747
|
private findRowById(rowId: string): GridRow | null {
|
|
1748
|
+
// Prefer the pipeline's current row instances — they carry the live
|
|
1749
|
+
// isSelected / isFocused / enableSelection flags that callers mutate.
|
|
1750
|
+
// Fall back to a fresh build when the pipeline is still empty (before
|
|
1751
|
+
// the first refresh).
|
|
1752
|
+
const fromPipeline = coreFindGridRowById(this.pipeline.visibleRows, rowId);
|
|
1753
|
+
if (fromPipeline) return fromPipeline;
|
|
824
1754
|
return coreFindGridRowById(this.buildRowsFromData(this.options.data), rowId);
|
|
825
1755
|
}
|
|
826
1756
|
|
|
@@ -950,6 +1880,460 @@ export class VanillaGridController {
|
|
|
950
1880
|
this.gridApi.core.raise.benchmarkComplete(result);
|
|
951
1881
|
return result;
|
|
952
1882
|
}
|
|
1883
|
+
|
|
1884
|
+
/** Returns the rows that match the requested exporter row-type. Mirrors
|
|
1885
|
+
* the old grid's row selection logic: `all` uses the full data set (or
|
|
1886
|
+
* `exporterAllDataFn` when provided), `visible` uses the current pipeline
|
|
1887
|
+
* output, `selected` uses the selection state. */
|
|
1888
|
+
private exporterRowsFor(rowType: GridExporterRowType): readonly GridRow[] {
|
|
1889
|
+
if (rowType === 'visible') return this.pipeline.visibleRows;
|
|
1890
|
+
if (rowType === 'selected') return this.getSelectedGridRows();
|
|
1891
|
+
// 'all' — fall back to the full grid rows, including filtered-out ones.
|
|
1892
|
+
// The `exporterAllDataFn` escape hatch is left to the caller; we only
|
|
1893
|
+
// bundle the in-memory data here.
|
|
1894
|
+
return this.buildRowsFromData(this.options.data);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
private resolveExporterOptions(): GridExporterOptions {
|
|
1898
|
+
return { ...resolveGridExporterOptions(this.options), ...this.exporterOverrides };
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
private buildCsv(
|
|
1902
|
+
rowType: GridExporterRowType = 'visible',
|
|
1903
|
+
colType: GridExporterColumnType = 'visible',
|
|
1904
|
+
): string {
|
|
1905
|
+
const exporterOptions = this.resolveExporterOptions();
|
|
1906
|
+
const columns = colType === 'all' ? this.options.columnDefs : this.visibleColumns;
|
|
1907
|
+
return buildGridCsv(columns, this.exporterRowsFor(rowType), exporterOptions, colType);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
private exportCsv(
|
|
1911
|
+
rowType: GridExporterRowType = 'visible',
|
|
1912
|
+
colType: GridExporterColumnType = 'visible',
|
|
1913
|
+
): void {
|
|
1914
|
+
const csv = this.buildCsv(rowType, colType);
|
|
1915
|
+
const resolved = this.resolveExporterOptions();
|
|
1916
|
+
const filename = sanitizeDownloadFilename(
|
|
1917
|
+
resolveExporterFilename(resolved.csvFilename, 'download.csv', rowType, colType),
|
|
1918
|
+
);
|
|
1919
|
+
downloadGridCsvFile(csv, filename);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
private buildPdfDocDefinition(
|
|
1923
|
+
rowType: GridExporterRowType = 'visible',
|
|
1924
|
+
colType: GridExporterColumnType = 'visible',
|
|
1925
|
+
): GridExporterPdfDocDefinition {
|
|
1926
|
+
const exporterOptions = this.resolveExporterOptions();
|
|
1927
|
+
const pdfOptions = resolveGridExporterPdfOptions(this.options);
|
|
1928
|
+
const columns = colType === 'all' ? this.options.columnDefs : this.visibleColumns;
|
|
1929
|
+
return buildGridPdfDocDefinition(
|
|
1930
|
+
columns,
|
|
1931
|
+
this.exporterRowsFor(rowType),
|
|
1932
|
+
pdfOptions,
|
|
1933
|
+
exporterOptions,
|
|
1934
|
+
colType,
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
private exportPdf(
|
|
1939
|
+
rowType: GridExporterRowType = 'visible',
|
|
1940
|
+
colType: GridExporterColumnType = 'visible',
|
|
1941
|
+
): GridExporterPdfDocDefinition {
|
|
1942
|
+
const doc = this.buildPdfDocDefinition(rowType, colType);
|
|
1943
|
+
// pdfMake is an optional global (consumers load it separately). When
|
|
1944
|
+
// present, open the generated PDF the same way the old module did;
|
|
1945
|
+
// when missing, just return the doc definition so the caller can
|
|
1946
|
+
// render it themselves. This mirrors `uiGridExporterService.pdfExport`.
|
|
1947
|
+
const win = typeof window !== 'undefined' ? (window as typeof window & {
|
|
1948
|
+
pdfMake?: { createPdf: (doc: GridExporterPdfDocDefinition) => { open: () => void; download: (filename: string) => void } };
|
|
1949
|
+
}) : undefined;
|
|
1950
|
+
if (win?.pdfMake) {
|
|
1951
|
+
const resolved = this.resolveExporterOptions();
|
|
1952
|
+
const pdfOpts = resolveGridExporterPdfOptions(this.options);
|
|
1953
|
+
const filename = resolveExporterFilename(
|
|
1954
|
+
pdfOpts.filename,
|
|
1955
|
+
'download.pdf',
|
|
1956
|
+
rowType,
|
|
1957
|
+
colType,
|
|
1958
|
+
);
|
|
1959
|
+
const pdf = win.pdfMake.createPdf(doc);
|
|
1960
|
+
// Prefer download when the consumer supplied a filename; otherwise
|
|
1961
|
+
// fall back to .open() which pops the pdf in a new browser tab.
|
|
1962
|
+
if (resolved.csvFilename || pdfOpts.filename) {
|
|
1963
|
+
pdf.download(filename);
|
|
1964
|
+
} else {
|
|
1965
|
+
pdf.open();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return doc;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
private buildExcelSheetData(
|
|
1972
|
+
rowType: GridExporterRowType = 'visible',
|
|
1973
|
+
colType: GridExporterColumnType = 'visible',
|
|
1974
|
+
): GridExporterExcelSheetData {
|
|
1975
|
+
const exporterOptions = this.resolveExporterOptions();
|
|
1976
|
+
const columns = colType === 'all' ? this.options.columnDefs : this.visibleColumns;
|
|
1977
|
+
return buildGridExcelSheetData(columns, this.exporterRowsFor(rowType), exporterOptions, colType);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
private exportExcel(
|
|
1981
|
+
rowType: GridExporterRowType = 'visible',
|
|
1982
|
+
colType: GridExporterColumnType = 'visible',
|
|
1983
|
+
): GridExporterExcelSheetData {
|
|
1984
|
+
const sheetData = this.buildExcelSheetData(rowType, colType);
|
|
1985
|
+
// ExcelBuilder is an optional global (consumers load it separately).
|
|
1986
|
+
// When present, produce + download the xlsx the same way the old module
|
|
1987
|
+
// did; when missing, just return the raw sheet data.
|
|
1988
|
+
const win = typeof window !== 'undefined' ? (window as typeof window & {
|
|
1989
|
+
ExcelBuilder?: {
|
|
1990
|
+
Worksheet: new (config: { name: string }) => { setData: (data: unknown) => void };
|
|
1991
|
+
Workbook: new () => { addWorksheet: (sheet: unknown) => void };
|
|
1992
|
+
Builder: { createFile: (workbook: unknown, options: { type: 'blob' }) => Promise<Blob> };
|
|
1993
|
+
};
|
|
1994
|
+
}) : undefined;
|
|
1995
|
+
const ExcelBuilder = win?.ExcelBuilder;
|
|
1996
|
+
if (!ExcelBuilder) {
|
|
1997
|
+
return sheetData;
|
|
1998
|
+
}
|
|
1999
|
+
const excelOptions = resolveGridExporterExcelOptions(this.options);
|
|
2000
|
+
const sheetName = typeof excelOptions.sheetName === 'function'
|
|
2001
|
+
? excelOptions.sheetName(rowType, colType)
|
|
2002
|
+
: (excelOptions.sheetName ?? 'Sheet1');
|
|
2003
|
+
const filename = sanitizeDownloadFilename(
|
|
2004
|
+
resolveExporterFilename(excelOptions.filename, 'download.xlsx', rowType, colType),
|
|
2005
|
+
);
|
|
2006
|
+
const sheet = new ExcelBuilder.Worksheet({ name: sheetName });
|
|
2007
|
+
const workbook = new ExcelBuilder.Workbook();
|
|
2008
|
+
workbook.addWorksheet(sheet);
|
|
2009
|
+
sheet.setData(sheetData);
|
|
2010
|
+
void ExcelBuilder.Builder.createFile(workbook, { type: 'blob' }).then((result) => {
|
|
2011
|
+
if (typeof URL === 'undefined' || typeof document === 'undefined') return;
|
|
2012
|
+
const url = URL.createObjectURL(result);
|
|
2013
|
+
const link = document.createElement('a');
|
|
2014
|
+
link.href = url;
|
|
2015
|
+
link.download = filename;
|
|
2016
|
+
link.click();
|
|
2017
|
+
URL.revokeObjectURL(url);
|
|
2018
|
+
});
|
|
2019
|
+
return sheetData;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// ---- Validate — ports ui.grid.validate ----------------------------------
|
|
2023
|
+
|
|
2024
|
+
/** Runs the validators declared on `colDef.validators` against the new
|
|
2025
|
+
* value and flips `$$invalid<col>` / `$$errors<col>` on the entity. On
|
|
2026
|
+
* failure raises `validate.validationFailed`. Async validators are
|
|
2027
|
+
* awaited so the caller can `await` for complete results. */
|
|
2028
|
+
private async validateRunValidators(
|
|
2029
|
+
rowEntity: GridRecord,
|
|
2030
|
+
colDef: GridColumnDef,
|
|
2031
|
+
newValue: unknown,
|
|
2032
|
+
oldValue: unknown,
|
|
2033
|
+
): Promise<string[]> {
|
|
2034
|
+
return runGridCellValidators(
|
|
2035
|
+
rowEntity,
|
|
2036
|
+
colDef,
|
|
2037
|
+
newValue,
|
|
2038
|
+
oldValue,
|
|
2039
|
+
this.validatorRegistry,
|
|
2040
|
+
(entity, def, newVal, oldVal) => {
|
|
2041
|
+
this.gridApi.validate.raise.validationFailed(entity, def, newVal, oldVal);
|
|
2042
|
+
},
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private validateGetFormattedErrors(rowEntity: GridRecord, colDef: GridColumnDef): string {
|
|
2047
|
+
const errors = getGridCellErrorMessages(rowEntity, colDef, this.validatorRegistry);
|
|
2048
|
+
if (errors.length === 0) return '';
|
|
2049
|
+
return `<p><b>${this.labels.validateError}</b></p>${errors.map((msg) => `${msg}<br/>`).join('')}`;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
private validateGetTitleFormattedErrors(rowEntity: GridRecord, colDef: GridColumnDef): string {
|
|
2053
|
+
const errors = getGridCellErrorMessages(rowEntity, colDef, this.validatorRegistry);
|
|
2054
|
+
if (errors.length === 0) return '';
|
|
2055
|
+
return [this.labels.validateError, ...errors].join('\n');
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
private async validateGetInvalidRows(): Promise<GridRecord[]> {
|
|
2059
|
+
return validateAllGridRows(
|
|
2060
|
+
this.options.data,
|
|
2061
|
+
this.options.columnDefs,
|
|
2062
|
+
this.validatorRegistry,
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/** Test hook — exposes the validator registry so consumers can inspect or
|
|
2067
|
+
* extend it outside of the gridApi surface (e.g. from a controller-level
|
|
2068
|
+
* plugin). */
|
|
2069
|
+
getValidatorRegistry(): GridValidatorRegistry {
|
|
2070
|
+
return this.validatorRegistry;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// ---- Importer — ports ui.grid.importer ---------------------------------
|
|
2074
|
+
|
|
2075
|
+
/** Set by the element so `importAFile()` can trigger its file-picker flow.
|
|
2076
|
+
* Controller stays DOM-free: it just invokes whatever handler was wired. */
|
|
2077
|
+
private importerFilePickerRequest: (() => void) | null = null;
|
|
2078
|
+
setImporterFilePickerHandler(handler: (() => void) | null): void {
|
|
2079
|
+
this.importerFilePickerRequest = handler;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
private resolveImporterOptions(): GridImporterOptions {
|
|
2083
|
+
return resolveGridImporterOptions(this.options);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
private importerRequestFile(): void {
|
|
2087
|
+
this.importerFilePickerRequest?.();
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
private importerDispatchObjects(objects: readonly GridRecord[]): void {
|
|
2091
|
+
if (objects.length === 0) return;
|
|
2092
|
+
const opts = this.resolveImporterOptions();
|
|
2093
|
+
if (opts.dataAddCallback) {
|
|
2094
|
+
opts.dataAddCallback(objects);
|
|
2095
|
+
} else {
|
|
2096
|
+
// No callback configured — append to the in-memory data array so the
|
|
2097
|
+
// grid reflects the imported rows immediately. Matches the
|
|
2098
|
+
// "importerDataAddCallback is required" hint from the old module
|
|
2099
|
+
// but without breaking the common case of a simple local data source.
|
|
2100
|
+
this.options = { ...this.options, data: [...this.options.data, ...objects] };
|
|
2101
|
+
this.refresh();
|
|
2102
|
+
}
|
|
2103
|
+
// If rowEdit is active, flip every freshly-added row dirty so the
|
|
2104
|
+
// consumer's saveRow hook sees them. Mirrors
|
|
2105
|
+
// `service.addObjects → rowEdit.setRowsDirty`.
|
|
2106
|
+
if (this.rowEditState.dirtyRowIds.size >= 0 && this.options.rowEditWaitInterval !== undefined) {
|
|
2107
|
+
// rowEdit is configured — queue the dirty marking on the next
|
|
2108
|
+
// refresh so the new rows are in the pipeline before we flip them.
|
|
2109
|
+
queueMicrotask(() => this.rowEditSetRowsDirty(objects));
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
private importerImportText(text: string, type?: 'json' | 'csv'): void {
|
|
2114
|
+
const opts = this.resolveImporterOptions();
|
|
2115
|
+
// Type inference: explicit `type` wins. Otherwise try JSON first; if
|
|
2116
|
+
// JSON.parse returns null (meaning invalidJson already reported) AND
|
|
2117
|
+
// the caller didn't force JSON, fall through to CSV. The old module
|
|
2118
|
+
// decided purely based on MIME type; `importText` is a newer entry
|
|
2119
|
+
// point we provide for drag/drop flows.
|
|
2120
|
+
const asJson = type === 'csv' ? null : parseGridImporterJson(text, { errorCallback: type === 'json' ? opts.errorCallback : undefined });
|
|
2121
|
+
if (asJson && asJson.length > 0) {
|
|
2122
|
+
const objects = buildGridImporterObjectsFromJson(asJson, opts);
|
|
2123
|
+
this.importerDispatchObjects(objects);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (type === 'json') return;
|
|
2127
|
+
|
|
2128
|
+
const csv = parseGridImporterCsv(text);
|
|
2129
|
+
if (!csv) {
|
|
2130
|
+
opts.errorCallback?.(
|
|
2131
|
+
'importer.invalidCsv',
|
|
2132
|
+
'File could not be processed, is it valid csv? Content was: ',
|
|
2133
|
+
text,
|
|
2134
|
+
);
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const objects = buildGridImporterObjectsFromCsv(csv, this.options.columnDefs, opts);
|
|
2138
|
+
if (!objects || objects.length === 0) {
|
|
2139
|
+
opts.errorCallback?.(
|
|
2140
|
+
'importer.noObjects',
|
|
2141
|
+
'Objects were not able to be derived, content was: ',
|
|
2142
|
+
text,
|
|
2143
|
+
);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
this.importerDispatchObjects(objects);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
private importerImportThisFile(file: File): Promise<void> {
|
|
2150
|
+
return new Promise((resolve) => {
|
|
2151
|
+
if (typeof FileReader === 'undefined') {
|
|
2152
|
+
resolve();
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
const reader = new FileReader();
|
|
2156
|
+
reader.onload = () => {
|
|
2157
|
+
const text = typeof reader.result === 'string' ? reader.result : '';
|
|
2158
|
+
// MIME sniff: old module switched on file.type, with `application/json`
|
|
2159
|
+
// as JSON and everything else as CSV. Preserve that default while
|
|
2160
|
+
// still letting the auto-detect path in `importerImportText` recover
|
|
2161
|
+
// when a consumer stores JSON with the wrong MIME.
|
|
2162
|
+
const type: 'json' | 'csv' = file.type === 'application/json' ? 'json' : 'csv';
|
|
2163
|
+
this.importerImportText(text, type);
|
|
2164
|
+
resolve();
|
|
2165
|
+
};
|
|
2166
|
+
reader.onerror = () => resolve();
|
|
2167
|
+
reader.readAsText(file);
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
private buildExporterMenuItems(): GridExporterMenuItem[] {
|
|
2172
|
+
// Pass the full resolved label bundle — the menu module pulls only
|
|
2173
|
+
// the `exporter*` keys it needs via `Partial<GridLabels>`.
|
|
2174
|
+
return buildGridExporterMenuItems(
|
|
2175
|
+
this.options,
|
|
2176
|
+
this.labels,
|
|
2177
|
+
{
|
|
2178
|
+
csvExport: (rowType, colType) => this.exportCsv(rowType, colType),
|
|
2179
|
+
pdfExport: (rowType, colType) => {
|
|
2180
|
+
this.exportPdf(rowType, colType);
|
|
2181
|
+
},
|
|
2182
|
+
excelExport: (rowType, colType) => {
|
|
2183
|
+
this.exportExcel(rowType, colType);
|
|
2184
|
+
},
|
|
2185
|
+
},
|
|
2186
|
+
() => this.selectionState.selectedRowIds.size > 0,
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/** Menu items the importer contributes. Just the single "Import"
|
|
2191
|
+
* entry when the feature is enabled; returns an empty array otherwise. */
|
|
2192
|
+
private buildImporterMenuItems(): GridMenuItem[] {
|
|
2193
|
+
return buildGridImporterMenuItems(
|
|
2194
|
+
this.options,
|
|
2195
|
+
this.labels,
|
|
2196
|
+
{ importAFile: () => this.importerRequestFile() },
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/** Menu items row-edit contributes: "Save changes" + "Retry errored
|
|
2201
|
+
* rows". Each entry is gated by both a gridOption flag and a runtime
|
|
2202
|
+
* predicate (`hasDirtyRows` / `hasErrorRows`) so the menu only surfaces
|
|
2203
|
+
* actions that have something to do. */
|
|
2204
|
+
private buildRowEditMenuItems(): GridMenuItem[] {
|
|
2205
|
+
return buildGridRowEditMenuItems(
|
|
2206
|
+
this.options,
|
|
2207
|
+
this.labels,
|
|
2208
|
+
{
|
|
2209
|
+
flushDirtyRows: () => this.rowEditFlushDirtyRows(),
|
|
2210
|
+
retryErroredRows: () => this.rowEditRetryErroredRows(),
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
hasDirtyRows: () => this.rowEditState.dirtyRowIds.size > 0,
|
|
2214
|
+
hasErrorRows: () => this.rowEditState.errorRowIds.size > 0,
|
|
2215
|
+
},
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// ---- Row-edit — ports ui.grid.rowEdit -----------------------------------
|
|
2220
|
+
|
|
2221
|
+
private rowEditSetSavePromise(rowEntity: GridRecord, savePromise: Promise<void>): void {
|
|
2222
|
+
const rowId = this.resolveRowId(rowEntity);
|
|
2223
|
+
this.rowEditSavePromiseOverrides.set(rowId, savePromise);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
private rowEditGetDirtyRows(): GridRow[] {
|
|
2227
|
+
return this.pipeline.visibleRows.filter((row) =>
|
|
2228
|
+
this.rowEditState.dirtyRowIds.has(row.id),
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
private rowEditGetErrorRows(): GridRow[] {
|
|
2233
|
+
return this.pipeline.visibleRows.filter((row) =>
|
|
2234
|
+
this.rowEditState.errorRowIds.has(row.id),
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
private rowEditFlushDirtyRows(): Promise<void> {
|
|
2239
|
+
const dirty = [...this.rowEditState.dirtyRowIds]
|
|
2240
|
+
.map((id) => this.findRowById(id))
|
|
2241
|
+
.filter((row): row is GridRow => row !== undefined);
|
|
2242
|
+
const promises = dirty.map((row) => this.runRowEditSave(row));
|
|
2243
|
+
return Promise.all(promises).then(() => undefined);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/** Re-fires `saveRow` for every row currently in the error state. The
|
|
2247
|
+
* "Retry errored rows" menu entry funnels through here. Returns a
|
|
2248
|
+
* promise that resolves once every retry promise has settled. */
|
|
2249
|
+
private rowEditRetryErroredRows(): Promise<void> {
|
|
2250
|
+
const errored = [...this.rowEditState.errorRowIds]
|
|
2251
|
+
.map((id) => this.findRowById(id))
|
|
2252
|
+
.filter((row): row is GridRow => row !== undefined);
|
|
2253
|
+
const promises = errored.map((row) => this.runRowEditSave(row));
|
|
2254
|
+
return Promise.all(promises).then(() => undefined);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
private rowEditSetRowsDirty(rowEntities: readonly GridRecord[]): void {
|
|
2258
|
+
for (const entity of rowEntities) {
|
|
2259
|
+
const rowId = this.resolveRowId(entity);
|
|
2260
|
+
const row = this.findRowById(rowId);
|
|
2261
|
+
if (!row) continue;
|
|
2262
|
+
markGridRowDirty(this.rowEditState, row);
|
|
2263
|
+
this.considerSetRowEditTimer(row);
|
|
2264
|
+
}
|
|
2265
|
+
this.emit();
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
private rowEditSetRowsClean(rowEntities: readonly GridRecord[]): void {
|
|
2269
|
+
for (const entity of rowEntities) {
|
|
2270
|
+
const rowId = this.resolveRowId(entity);
|
|
2271
|
+
const row = this.findRowById(rowId);
|
|
2272
|
+
if (!row) continue;
|
|
2273
|
+
markGridRowClean(this.rowEditState, row);
|
|
2274
|
+
this.cancelRowEditTimer(row);
|
|
2275
|
+
}
|
|
2276
|
+
this.emit();
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
private considerSetRowEditTimer(row: GridRow): void {
|
|
2280
|
+
this.cancelRowEditTimer(row);
|
|
2281
|
+
if (!row.isDirty || row.isSaving) return;
|
|
2282
|
+
const waitInterval = this.options.rowEditWaitInterval;
|
|
2283
|
+
if (!isGridRowEditTimerEnabled(waitInterval)) return;
|
|
2284
|
+
const effective = resolveGridRowEditWaitInterval(waitInterval);
|
|
2285
|
+
const timer = setTimeout(() => {
|
|
2286
|
+
this.rowEditTimers.delete(row.id);
|
|
2287
|
+
void this.runRowEditSave(row);
|
|
2288
|
+
}, effective);
|
|
2289
|
+
this.rowEditTimers.set(row.id, timer);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
private cancelRowEditTimer(row: GridRow): void {
|
|
2293
|
+
const timer = this.rowEditTimers.get(row.id);
|
|
2294
|
+
if (timer !== undefined) {
|
|
2295
|
+
clearTimeout(timer);
|
|
2296
|
+
this.rowEditTimers.delete(row.id);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
private runRowEditSave(row: GridRow): Promise<void> {
|
|
2301
|
+
if (row.isSaving) {
|
|
2302
|
+
// Re-entrance — return the in-flight promise so `flushDirtyRows`
|
|
2303
|
+
// still awaits the existing save. Matches the old module's
|
|
2304
|
+
// `rowEditSavePromise` short-circuit.
|
|
2305
|
+
return this.rowEditState.savePromises.get(row.id) ?? Promise.resolve();
|
|
2306
|
+
}
|
|
2307
|
+
markGridRowSaving(this.rowEditState, row);
|
|
2308
|
+
this.rowEditSavePromiseOverrides.delete(row.id);
|
|
2309
|
+
this.gridApi.rowEdit.raise.saveRow(row.entity);
|
|
2310
|
+
const savePromise =
|
|
2311
|
+
this.rowEditSavePromiseOverrides.get(row.id) ?? Promise.resolve();
|
|
2312
|
+
this.rowEditState.savePromises.set(row.id, savePromise);
|
|
2313
|
+
this.emit();
|
|
2314
|
+
return savePromise
|
|
2315
|
+
.then(() => {
|
|
2316
|
+
markGridRowClean(this.rowEditState, row);
|
|
2317
|
+
this.emit();
|
|
2318
|
+
})
|
|
2319
|
+
.catch(() => {
|
|
2320
|
+
markGridRowError(this.rowEditState, row);
|
|
2321
|
+
this.emit();
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
/** Test hook — reports the current row-edit dirty / error / saving state. */
|
|
2326
|
+
getRowEditState(): {
|
|
2327
|
+
dirtyRowIds: string[];
|
|
2328
|
+
errorRowIds: string[];
|
|
2329
|
+
savingRowIds: string[];
|
|
2330
|
+
} {
|
|
2331
|
+
return {
|
|
2332
|
+
dirtyRowIds: [...this.rowEditState.dirtyRowIds],
|
|
2333
|
+
errorRowIds: [...this.rowEditState.errorRowIds],
|
|
2334
|
+
savingRowIds: [...this.rowEditState.savingRowIds],
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
953
2337
|
}
|
|
954
2338
|
|
|
955
2339
|
export function createVanillaGridController(options: GridOptions): VanillaGridController {
|