@ornery/ui-grid-vanilla 0.1.9 → 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/index.html +3 -3
- package/demo/main.ts +1 -1
- package/dist/index.d.mts +584 -55
- package/dist/index.d.ts +584 -55
- package/dist/index.js +4887 -1404
- package/dist/index.mjs +5056 -1526
- 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 +1431 -20
- 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 +751 -1032
- 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;
|
|
@@ -134,10 +219,31 @@ export class VanillaGridController {
|
|
|
134
219
|
private expandedTreeRows: Record<string, boolean> = {};
|
|
135
220
|
private pinnedColumns: PinnedColumnState = {};
|
|
136
221
|
private columnOrder: string[] = [];
|
|
222
|
+
private columnWidthOverrides: Record<string, string> = {};
|
|
137
223
|
private currentPage = 1;
|
|
138
224
|
private pageSize = 0;
|
|
139
225
|
private editingCell: GridCellPosition | null = null;
|
|
140
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
|
+
};
|
|
141
247
|
private pipeline: PipelineResult;
|
|
142
248
|
private labels: GridLabels;
|
|
143
249
|
private visibleColumns: GridColumnDef[] = [];
|
|
@@ -150,7 +256,18 @@ export class VanillaGridController {
|
|
|
150
256
|
constructor(options: GridOptions) {
|
|
151
257
|
this.options = options;
|
|
152
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);
|
|
153
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
|
+
);
|
|
154
271
|
|
|
155
272
|
this.pipeline = {
|
|
156
273
|
visibleRows: [],
|
|
@@ -180,8 +297,17 @@ export class VanillaGridController {
|
|
|
180
297
|
toggleGrouping: (columnName) => this.toggleGrouping(columnName),
|
|
181
298
|
clearGrouping: () => this.clearGrouping(),
|
|
182
299
|
benchmark: (iterations) => this.benchmark(iterations),
|
|
183
|
-
exportCsv: () =>
|
|
184
|
-
|
|
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 };
|
|
185
311
|
},
|
|
186
312
|
paginationGetPage: () => this.getCurrentPage(),
|
|
187
313
|
paginationGetTotalPages: () => this.getTotalPages(),
|
|
@@ -224,14 +350,163 @@ export class VanillaGridController {
|
|
|
224
350
|
this.cancelCellEdit();
|
|
225
351
|
},
|
|
226
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();
|
|
227
481
|
});
|
|
228
482
|
|
|
229
483
|
this.refresh();
|
|
230
484
|
}
|
|
231
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
|
+
|
|
232
496
|
setOptions(options: GridOptions): void {
|
|
233
497
|
this.options = options;
|
|
234
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
|
+
}
|
|
235
510
|
if (this.columnOrder.length === 0) {
|
|
236
511
|
this.columnOrder = options.columnDefs.map((column) => column.name);
|
|
237
512
|
} else {
|
|
@@ -244,6 +519,37 @@ export class VanillaGridController {
|
|
|
244
519
|
}
|
|
245
520
|
}
|
|
246
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
|
+
|
|
247
553
|
this.apiRegistered = false;
|
|
248
554
|
this.refresh();
|
|
249
555
|
}
|
|
@@ -300,6 +606,10 @@ export class VanillaGridController {
|
|
|
300
606
|
pageSize: this.getEffectivePageSize(),
|
|
301
607
|
editingCell: this.editingCell,
|
|
302
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,
|
|
303
613
|
};
|
|
304
614
|
}
|
|
305
615
|
|
|
@@ -519,6 +829,7 @@ export class VanillaGridController {
|
|
|
519
829
|
row: GridRow | GridRecord | string,
|
|
520
830
|
columnName: string,
|
|
521
831
|
triggerEvent?: Event | KeyboardEvent | null,
|
|
832
|
+
initialValue?: string,
|
|
522
833
|
): void {
|
|
523
834
|
const rowId = this.resolveRowId(row);
|
|
524
835
|
const gridRow = this.findRowById(rowId);
|
|
@@ -544,6 +855,7 @@ export class VanillaGridController {
|
|
|
544
855
|
column,
|
|
545
856
|
getCellValue(gridRow.entity, column),
|
|
546
857
|
triggerEvent,
|
|
858
|
+
initialValue,
|
|
547
859
|
);
|
|
548
860
|
|
|
549
861
|
this.refresh();
|
|
@@ -555,6 +867,15 @@ export class VanillaGridController {
|
|
|
555
867
|
}
|
|
556
868
|
|
|
557
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
|
+
|
|
558
879
|
commitGridCellEditCommand(this.gridApi, {
|
|
559
880
|
getEditingCell: () => this.editingCell,
|
|
560
881
|
getEditingValue: () => this.editingValue,
|
|
@@ -575,6 +896,10 @@ export class VanillaGridController {
|
|
|
575
896
|
}
|
|
576
897
|
|
|
577
898
|
cancelCellEdit(): void {
|
|
899
|
+
if (this.editingCell === null) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
578
903
|
cancelGridCellEditCommand(this.gridApi, {
|
|
579
904
|
getEditingCell: () => this.editingCell,
|
|
580
905
|
setEditingCell: (next) => {
|
|
@@ -590,6 +915,485 @@ export class VanillaGridController {
|
|
|
590
915
|
this.refresh();
|
|
591
916
|
}
|
|
592
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
|
+
|
|
1376
|
+
canResizeColumns(): boolean {
|
|
1377
|
+
return this.options.enableColumnResizing !== false;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
setColumnWidthOverride(columnName: string, widthPx: number): void {
|
|
1381
|
+
const nextWidth = `${Math.max(88, Math.round(widthPx))}px`;
|
|
1382
|
+
this.columnWidthOverrides = { ...this.columnWidthOverrides, [columnName]: nextWidth };
|
|
1383
|
+
this.refresh();
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** Returns the grid-template-columns string as if `columnName` had the given width,
|
|
1387
|
+
* without triggering a full refresh. Used for smooth drag-resize DOM updates. */
|
|
1388
|
+
buildTemplateColumnsWithOverride(columnName: string, widthPx: number): string {
|
|
1389
|
+
const widthStr = `${Math.max(88, Math.round(widthPx))}px`;
|
|
1390
|
+
return buildGridTemplateColumns(
|
|
1391
|
+
this.visibleColumns.map((c) =>
|
|
1392
|
+
c.name === columnName ? { ...c, width: widthStr } : c,
|
|
1393
|
+
),
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
593
1397
|
isGroupingEnabled(): boolean {
|
|
594
1398
|
return isGridGroupingEnabled(this.options);
|
|
595
1399
|
}
|
|
@@ -656,6 +1460,21 @@ export class VanillaGridController {
|
|
|
656
1460
|
return isColumnPinnable(this.options, column);
|
|
657
1461
|
}
|
|
658
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
|
+
|
|
659
1478
|
isPinned(column: GridColumnDef): boolean {
|
|
660
1479
|
return this.pinnedColumns[column.name] !== undefined;
|
|
661
1480
|
}
|
|
@@ -718,35 +1537,134 @@ export class VanillaGridController {
|
|
|
718
1537
|
return this.editingCell?.rowId === rowId && this.editingCell?.columnName === columnName;
|
|
719
1538
|
}
|
|
720
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
|
+
*/
|
|
721
1545
|
getState(): GridSaveState {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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;
|
|
731
1598
|
}
|
|
732
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
|
+
*/
|
|
733
1605
|
setState(state: Partial<GridSaveState>): void {
|
|
734
1606
|
if (state.sortState !== undefined) this.sortState = { ...state.sortState };
|
|
735
1607
|
if (state.activeFilters !== undefined) this.activeFilters = { ...state.activeFilters };
|
|
736
1608
|
if (state.groupByColumns !== undefined) this.groupByColumns = [...state.groupByColumns];
|
|
1609
|
+
if (state.collapsedGroups !== undefined) this.collapsedGroups = { ...state.collapsedGroups };
|
|
737
1610
|
if (state.pinnedColumns !== undefined) this.pinnedColumns = { ...state.pinnedColumns };
|
|
738
1611
|
if (state.columnOrder !== undefined) this.columnOrder = [...state.columnOrder];
|
|
1612
|
+
if (state.columnWidthOverrides !== undefined) this.columnWidthOverrides = { ...state.columnWidthOverrides };
|
|
739
1613
|
if (state.currentPage !== undefined) this.currentPage = state.currentPage;
|
|
740
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
|
+
}
|
|
741
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;
|
|
742
1655
|
}
|
|
743
1656
|
|
|
744
1657
|
private refresh(): void {
|
|
745
1658
|
const orderedColumns = orderVisibleColumns(this.options.columnDefs, this.columnOrder);
|
|
1659
|
+
const applyWidthOverrides = (columns: GridColumnDef[]): GridColumnDef[] =>
|
|
1660
|
+
columns.map((col) => {
|
|
1661
|
+
const override = this.columnWidthOverrides[col.name];
|
|
1662
|
+
return override == null ? col : { ...col, width: override };
|
|
1663
|
+
});
|
|
746
1664
|
const pinnedEntries = Object.entries(this.pinnedColumns);
|
|
747
1665
|
|
|
748
1666
|
if (pinnedEntries.length === 0) {
|
|
749
|
-
this.visibleColumns = orderedColumns;
|
|
1667
|
+
this.visibleColumns = applyWidthOverrides(orderedColumns);
|
|
750
1668
|
} else {
|
|
751
1669
|
const byName = new Map(orderedColumns.map((column) => [column.name, column]));
|
|
752
1670
|
const pinnedLeft = pinnedEntries
|
|
@@ -760,7 +1678,30 @@ export class VanillaGridController {
|
|
|
760
1678
|
const middleColumns = orderedColumns.filter(
|
|
761
1679
|
(column) => this.pinnedColumns[column.name] === undefined,
|
|
762
1680
|
);
|
|
763
|
-
this.visibleColumns = [...pinnedLeft, ...middleColumns, ...pinnedRight];
|
|
1681
|
+
this.visibleColumns = applyWidthOverrides([...pinnedLeft, ...middleColumns, ...pinnedRight]);
|
|
1682
|
+
}
|
|
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
|
+
}
|
|
764
1705
|
}
|
|
765
1706
|
|
|
766
1707
|
this.pipeline = defaultGridEngine.buildPipeline({
|
|
@@ -778,6 +1719,16 @@ export class VanillaGridController {
|
|
|
778
1719
|
rowSize: this.getRowSize(),
|
|
779
1720
|
});
|
|
780
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
|
+
|
|
781
1732
|
if (!this.apiRegistered) {
|
|
782
1733
|
this.options.onRegisterApi?.(this.gridApi);
|
|
783
1734
|
this.apiRegistered = true;
|
|
@@ -794,6 +1745,12 @@ export class VanillaGridController {
|
|
|
794
1745
|
}
|
|
795
1746
|
|
|
796
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;
|
|
797
1754
|
return coreFindGridRowById(this.buildRowsFromData(this.options.data), rowId);
|
|
798
1755
|
}
|
|
799
1756
|
|
|
@@ -923,6 +1880,460 @@ export class VanillaGridController {
|
|
|
923
1880
|
this.gridApi.core.raise.benchmarkComplete(result);
|
|
924
1881
|
return result;
|
|
925
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
|
+
}
|
|
926
2337
|
}
|
|
927
2338
|
|
|
928
2339
|
export function createVanillaGridController(options: GridOptions): VanillaGridController {
|