@alaarab/ogrid-angular 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/esm/components/base-column-chooser.component.js +77 -0
  2. package/dist/esm/components/base-column-header-filter.component.js +267 -0
  3. package/dist/esm/components/base-column-header-menu.component.js +80 -0
  4. package/dist/esm/components/base-datagrid-table.component.js +768 -0
  5. package/dist/esm/components/base-inline-cell-editor.component.js +380 -0
  6. package/dist/esm/components/base-ogrid.component.js +36 -0
  7. package/dist/esm/components/base-pagination-controls.component.js +68 -0
  8. package/dist/esm/components/base-popover-cell-editor.component.js +122 -0
  9. package/dist/esm/components/empty-state.component.js +68 -0
  10. package/dist/esm/components/formula-bar.component.js +99 -0
  11. package/dist/esm/components/formula-ref-overlay.component.js +115 -0
  12. package/dist/esm/components/grid-context-menu.component.js +197 -0
  13. package/dist/esm/components/inline-cell-editor-template.js +134 -0
  14. package/dist/esm/components/marching-ants-overlay.component.js +177 -0
  15. package/dist/esm/components/ogrid-layout.component.js +302 -0
  16. package/dist/esm/components/sheet-tabs.component.js +83 -0
  17. package/dist/esm/components/sidebar.component.js +431 -0
  18. package/dist/esm/components/status-bar.component.js +92 -0
  19. package/dist/esm/index.js +39 -819
  20. package/dist/esm/services/column-reorder.service.js +176 -0
  21. package/dist/esm/services/datagrid-editing.service.js +59 -0
  22. package/dist/esm/services/datagrid-interaction.service.js +744 -0
  23. package/dist/esm/services/datagrid-layout.service.js +157 -0
  24. package/dist/esm/services/datagrid-state.service.js +636 -0
  25. package/dist/esm/services/formula-engine.service.js +223 -0
  26. package/dist/esm/services/ogrid.service.js +1094 -0
  27. package/dist/esm/services/virtual-scroll.service.js +114 -0
  28. package/dist/esm/styles/ogrid-theme-vars.js +112 -0
  29. package/dist/esm/types/columnTypes.js +1 -0
  30. package/dist/esm/types/dataGridTypes.js +1 -0
  31. package/dist/esm/types/index.js +1 -0
  32. package/dist/esm/utils/dataGridViewModel.js +6 -0
  33. package/dist/esm/utils/debounce.js +68 -0
  34. package/dist/esm/utils/index.js +8 -0
  35. package/dist/esm/utils/latestRef.js +41 -0
  36. package/dist/types/components/base-column-chooser.component.d.ts +3 -0
  37. package/dist/types/components/base-column-header-filter.component.d.ts +3 -0
  38. package/dist/types/components/base-column-header-menu.component.d.ts +3 -0
  39. package/dist/types/components/base-datagrid-table.component.d.ts +4 -19
  40. package/dist/types/components/base-inline-cell-editor.component.d.ts +7 -0
  41. package/dist/types/components/base-ogrid.component.d.ts +3 -0
  42. package/dist/types/components/base-pagination-controls.component.d.ts +3 -0
  43. package/dist/types/components/base-popover-cell-editor.component.d.ts +3 -0
  44. package/dist/types/components/empty-state.component.d.ts +3 -0
  45. package/dist/types/components/formula-bar.component.d.ts +3 -8
  46. package/dist/types/components/formula-ref-overlay.component.d.ts +3 -6
  47. package/dist/types/components/grid-context-menu.component.d.ts +3 -0
  48. package/dist/types/components/inline-cell-editor-template.d.ts +2 -2
  49. package/dist/types/components/marching-ants-overlay.component.d.ts +3 -0
  50. package/dist/types/components/ogrid-layout.component.d.ts +3 -0
  51. package/dist/types/components/sheet-tabs.component.d.ts +3 -8
  52. package/dist/types/components/sidebar.component.d.ts +3 -0
  53. package/dist/types/components/status-bar.component.d.ts +3 -0
  54. package/dist/types/index.d.ts +0 -2
  55. package/dist/types/services/column-reorder.service.d.ts +3 -0
  56. package/dist/types/services/datagrid-interaction.service.d.ts +1 -0
  57. package/dist/types/services/datagrid-state.service.d.ts +5 -0
  58. package/dist/types/services/formula-engine.service.d.ts +3 -9
  59. package/dist/types/services/ogrid.service.d.ts +8 -11
  60. package/dist/types/services/virtual-scroll.service.d.ts +3 -0
  61. package/dist/types/types/dataGridTypes.d.ts +0 -4
  62. package/package.json +4 -3
@@ -0,0 +1,1094 @@
1
+ import { Injectable, signal, computed, effect, DestroyRef, inject } from '@angular/core';
2
+ import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, processClientSideDataAsync, computeNextSortState, shouldUseWorkerSort, validateColumns, validateRowIds, columnLetterToIndex, getCellValue, } from '@alaarab/ogrid-core';
3
+ import { extractFormulaReferences, deriveFormulaBarText } from '@alaarab/ogrid-core/formula';
4
+ import { FormulaEngineService } from './formula-engine.service';
5
+ import * as i0 from "@angular/core";
6
+ const DEFAULT_PAGE_SIZE = 25;
7
+ const EMPTY_LOADING_OPTIONS = {};
8
+ const DEFAULT_PANELS = ['columns', 'filters'];
9
+ /**
10
+ * Top-level orchestration service for OGrid: manages pagination, sorting, filtering,
11
+ * column visibility, sidebar, and server-side data fetching via Angular signals.
12
+ *
13
+ * Port of React's useOGrid hook.
14
+ */
15
+ export class OGridService {
16
+ constructor() {
17
+ this.destroyRef = inject(DestroyRef);
18
+ // --- Input signals (set by the component consuming this service) ---
19
+ this.columnsProp = signal([], ...(ngDevMode ? [{ debugName: "columnsProp" }] : []));
20
+ this.getRowId = signal((item) => item['id'], ...(ngDevMode ? [{ debugName: "getRowId" }] : []));
21
+ this.data = signal(undefined, ...(ngDevMode ? [{ debugName: "data" }] : []));
22
+ this.dataSource = signal(undefined, ...(ngDevMode ? [{ debugName: "dataSource" }] : []));
23
+ this.controlledPage = signal(undefined, ...(ngDevMode ? [{ debugName: "controlledPage" }] : []));
24
+ this.controlledPageSize = signal(undefined, ...(ngDevMode ? [{ debugName: "controlledPageSize" }] : []));
25
+ this.controlledSort = signal(undefined, ...(ngDevMode ? [{ debugName: "controlledSort" }] : []));
26
+ this.controlledFilters = signal(undefined, ...(ngDevMode ? [{ debugName: "controlledFilters" }] : []));
27
+ this.controlledVisibleColumns = signal(undefined, ...(ngDevMode ? [{ debugName: "controlledVisibleColumns" }] : []));
28
+ this.controlledLoading = signal(undefined, ...(ngDevMode ? [{ debugName: "controlledLoading" }] : []));
29
+ this.onPageChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onPageChange" }] : []));
30
+ this.onPageSizeChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onPageSizeChange" }] : []));
31
+ this.onSortChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onSortChange" }] : []));
32
+ this.onFiltersChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onFiltersChange" }] : []));
33
+ this.onVisibleColumnsChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onVisibleColumnsChange" }] : []));
34
+ this.columnOrder = signal(undefined, ...(ngDevMode ? [{ debugName: "columnOrder" }] : []));
35
+ this.internalColumnOrder = signal(undefined, ...(ngDevMode ? [{ debugName: "internalColumnOrder" }] : []));
36
+ this.onColumnOrderChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onColumnOrderChange" }] : []));
37
+ this.onColumnResized = signal(undefined, ...(ngDevMode ? [{ debugName: "onColumnResized" }] : []));
38
+ this.onAutosizeColumn = signal(undefined, ...(ngDevMode ? [{ debugName: "onAutosizeColumn" }] : []));
39
+ this.onColumnPinned = signal(undefined, ...(ngDevMode ? [{ debugName: "onColumnPinned" }] : []));
40
+ this.defaultPageSize = signal(DEFAULT_PAGE_SIZE, ...(ngDevMode ? [{ debugName: "defaultPageSize" }] : []));
41
+ this.defaultSortBy = signal(undefined, ...(ngDevMode ? [{ debugName: "defaultSortBy" }] : []));
42
+ this.defaultSortDirection = signal('asc', ...(ngDevMode ? [{ debugName: "defaultSortDirection" }] : []));
43
+ this.toolbar = signal(undefined, ...(ngDevMode ? [{ debugName: "toolbar" }] : []));
44
+ this.toolbarBelow = signal(undefined, ...(ngDevMode ? [{ debugName: "toolbarBelow" }] : []));
45
+ this.emptyState = signal(undefined, ...(ngDevMode ? [{ debugName: "emptyState" }] : []));
46
+ this.entityLabelPlural = signal('items', ...(ngDevMode ? [{ debugName: "entityLabelPlural" }] : []));
47
+ this.className = signal(undefined, ...(ngDevMode ? [{ debugName: "className" }] : []));
48
+ this.layoutMode = signal('fill', ...(ngDevMode ? [{ debugName: "layoutMode" }] : []));
49
+ this.suppressHorizontalScroll = signal(undefined, ...(ngDevMode ? [{ debugName: "suppressHorizontalScroll" }] : []));
50
+ this.stickyHeader = signal(true, ...(ngDevMode ? [{ debugName: "stickyHeader" }] : []));
51
+ this.fullScreen = signal(false, ...(ngDevMode ? [{ debugName: "fullScreen" }] : []));
52
+ this.editable = signal(undefined, ...(ngDevMode ? [{ debugName: "editable" }] : []));
53
+ this.cellSelection = signal(undefined, ...(ngDevMode ? [{ debugName: "cellSelection" }] : []));
54
+ this.density = signal('normal', ...(ngDevMode ? [{ debugName: "density" }] : []));
55
+ this.rowHeight = signal(undefined, ...(ngDevMode ? [{ debugName: "rowHeight" }] : []));
56
+ this.onCellValueChanged = signal(undefined, ...(ngDevMode ? [{ debugName: "onCellValueChanged" }] : []));
57
+ this.onUndo = signal(undefined, ...(ngDevMode ? [{ debugName: "onUndo" }] : []));
58
+ this.onRedo = signal(undefined, ...(ngDevMode ? [{ debugName: "onRedo" }] : []));
59
+ this.canUndo = signal(undefined, ...(ngDevMode ? [{ debugName: "canUndo" }] : []));
60
+ this.canRedo = signal(undefined, ...(ngDevMode ? [{ debugName: "canRedo" }] : []));
61
+ this.rowSelection = signal('none', ...(ngDevMode ? [{ debugName: "rowSelection" }] : []));
62
+ this.selectedRows = signal(undefined, ...(ngDevMode ? [{ debugName: "selectedRows" }] : []));
63
+ this.onSelectionChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onSelectionChange" }] : []));
64
+ this.statusBar = signal(undefined, ...(ngDevMode ? [{ debugName: "statusBar" }] : []));
65
+ this.pageSizeOptions = signal(undefined, ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : []));
66
+ this.sideBarConfig = signal(undefined, ...(ngDevMode ? [{ debugName: "sideBarConfig" }] : []));
67
+ this.onFirstDataRendered = signal(undefined, ...(ngDevMode ? [{ debugName: "onFirstDataRendered" }] : []));
68
+ this.onError = signal(undefined, ...(ngDevMode ? [{ debugName: "onError" }] : []));
69
+ this.columnChooserProp = signal(undefined, ...(ngDevMode ? [{ debugName: "columnChooserProp" }] : []));
70
+ this.columnReorder = signal(undefined, ...(ngDevMode ? [{ debugName: "columnReorder" }] : []));
71
+ this.responsiveColumns = signal(undefined, ...(ngDevMode ? [{ debugName: "responsiveColumns" }] : []));
72
+ this.virtualScroll = signal(undefined, ...(ngDevMode ? [{ debugName: "virtualScroll" }] : []));
73
+ this.ariaLabel = signal(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
74
+ this.ariaLabelledBy = signal(undefined, ...(ngDevMode ? [{ debugName: "ariaLabelledBy" }] : []));
75
+ this.workerSort = signal(false, ...(ngDevMode ? [{ debugName: "workerSort" }] : []));
76
+ this.showRowNumbers = signal(false, ...(ngDevMode ? [{ debugName: "showRowNumbers" }] : []));
77
+ this.cellReferences = signal(false, ...(ngDevMode ? [{ debugName: "cellReferences" }] : []));
78
+ this.formulasEnabled = signal(false, ...(ngDevMode ? [{ debugName: "formulasEnabled" }] : []));
79
+ this.initialFormulas = signal(undefined, ...(ngDevMode ? [{ debugName: "initialFormulas" }] : []));
80
+ this.onFormulaRecalc = signal(undefined, ...(ngDevMode ? [{ debugName: "onFormulaRecalc" }] : []));
81
+ this.formulaFunctions = signal(undefined, ...(ngDevMode ? [{ debugName: "formulaFunctions" }] : []));
82
+ this.namedRanges = signal(undefined, ...(ngDevMode ? [{ debugName: "namedRanges" }] : []));
83
+ this.sheets = signal(undefined, ...(ngDevMode ? [{ debugName: "sheets" }] : []));
84
+ this.sheetDefs = signal(undefined, ...(ngDevMode ? [{ debugName: "sheetDefs" }] : []));
85
+ this.activeSheet = signal(undefined, ...(ngDevMode ? [{ debugName: "activeSheet" }] : []));
86
+ this.onSheetChange = signal(undefined, ...(ngDevMode ? [{ debugName: "onSheetChange" }] : []));
87
+ this.onSheetAdd = signal(undefined, ...(ngDevMode ? [{ debugName: "onSheetAdd" }] : []));
88
+ /** Active cell reference string (e.g. 'A1') updated by DataGridTable when cellReferences is enabled. */
89
+ this.activeCellRef = signal(null, ...(ngDevMode ? [{ debugName: "activeCellRef" }] : []));
90
+ /** Active cell coordinates (0-based col/row). */
91
+ this.activeCellCoords = signal(null, ...(ngDevMode ? [{ debugName: "activeCellCoords" }] : []));
92
+ /** Stable callback passed to DataGridTable to update activeCellRef + coords. */
93
+ this.handleActiveCellChange = (ref) => {
94
+ this.activeCellRef.set(ref);
95
+ if (ref) {
96
+ const m = ref.match(/^([A-Z]+)(\d+)$/);
97
+ if (m) {
98
+ this.activeCellCoords.set({ col: columnLetterToIndex(m[1]), row: parseInt(m[2], 10) - 1 });
99
+ }
100
+ else {
101
+ this.activeCellCoords.set(null);
102
+ }
103
+ }
104
+ else {
105
+ this.activeCellCoords.set(null);
106
+ }
107
+ };
108
+ // --- Formula bar state ---
109
+ this.formulaBarEditing = signal(false, ...(ngDevMode ? [{ debugName: "formulaBarEditing" }] : []));
110
+ this.formulaBarEditText = signal('', ...(ngDevMode ? [{ debugName: "formulaBarEditText" }] : []));
111
+ // --- Formula engine ---
112
+ this.formulaService = new FormulaEngineService();
113
+ /** Monotonic counter incremented on formula recalculation - drives cache invalidation. */
114
+ this.formulaVersion = signal(0, ...(ngDevMode ? [{ debugName: "formulaVersion" }] : []));
115
+ // Stable formula method references for dataGridProps (avoid per-recompute arrow functions)
116
+ this.getFormulaValueFn = (col, row) => this.formulaService.getValue(col, row);
117
+ this.hasFormulaFn = (col, row) => this.formulaService.hasFormula(col, row);
118
+ this.getFormulaFn = (col, row) => this.formulaService.getFormula(col, row);
119
+ this.setFormulaFn = (col, row, formula) => this.formulaService.setFormula(col, row, formula ?? '');
120
+ this.onFormulaCellChangedFn = (col, row) => this.formulaService.onCellChanged(col, row);
121
+ this.getPrecedentsFn = (col, row) => this.formulaService.getPrecedents(col, row);
122
+ this.getDependentsFn = (col, row) => this.formulaService.getDependents(col, row);
123
+ this.getAuditTrailFn = (col, row) => this.formulaService.getAuditTrail(col, row);
124
+ // --- Internal state signals ---
125
+ this.internalData = signal([], ...(ngDevMode ? [{ debugName: "internalData" }] : []));
126
+ this.internalLoading = signal(false, ...(ngDevMode ? [{ debugName: "internalLoading" }] : []));
127
+ this.internalPage = signal(1, ...(ngDevMode ? [{ debugName: "internalPage" }] : []));
128
+ this.internalPageSizeOverride = signal(null, ...(ngDevMode ? [{ debugName: "internalPageSizeOverride" }] : []));
129
+ this.internalSortOverride = signal(null, ...(ngDevMode ? [{ debugName: "internalSortOverride" }] : []));
130
+ this.internalFilters = signal({}, ...(ngDevMode ? [{ debugName: "internalFilters" }] : []));
131
+ this.internalVisibleColumnsOverride = signal(null, ...(ngDevMode ? [{ debugName: "internalVisibleColumnsOverride" }] : []));
132
+ this.internalSelectedRows = signal(new Set(), ...(ngDevMode ? [{ debugName: "internalSelectedRows" }] : []));
133
+ this.columnWidthOverrides = signal({}, ...(ngDevMode ? [{ debugName: "columnWidthOverrides" }] : []));
134
+ this.pinnedOverrides = signal({}, ...(ngDevMode ? [{ debugName: "pinnedOverrides" }] : []));
135
+ // Server-side state
136
+ this.serverItems = signal([], ...(ngDevMode ? [{ debugName: "serverItems" }] : []));
137
+ this.serverTotalCount = signal(0, ...(ngDevMode ? [{ debugName: "serverTotalCount" }] : []));
138
+ this.serverLoading = signal(true, ...(ngDevMode ? [{ debugName: "serverLoading" }] : []));
139
+ this.fetchAbortController = null;
140
+ this.filterAbortController = null;
141
+ this.refreshCounter = signal(0, ...(ngDevMode ? [{ debugName: "refreshCounter" }] : []));
142
+ this.firstDataRendered = signal(false, ...(ngDevMode ? [{ debugName: "firstDataRendered" }] : []));
143
+ // Worker sort async state
144
+ this.asyncClientItems = signal(null, ...(ngDevMode ? [{ debugName: "asyncClientItems" }] : []));
145
+ this.workerSortAbortId = 0;
146
+ // Stable sorted order (index-based, same approach as React/Vue).
147
+ // sortedIndices stores indices into displayData() from the last explicit sort/filter pass.
148
+ // When only displayData changes (cell edit), we reuse these indices to look up updated row
149
+ // objects - preserving order without re-sorting. Matches Excel behavior.
150
+ this.sortedIndices = null;
151
+ this.sortedPrevSortVersion = -1;
152
+ this.sortedPrevFilters = null;
153
+ this.sortedPrevColumns = null;
154
+ this.sortedPrevDataLength = -1;
155
+ this.sortSnapshotVersion = signal(0, ...(ngDevMode ? [{ debugName: "sortSnapshotVersion" }] : []));
156
+ // Side bar state
157
+ this.sideBarActivePanel = signal(null, ...(ngDevMode ? [{ debugName: "sideBarActivePanel" }] : []));
158
+ // Filter options state
159
+ this.serverFilterOptions = signal({}, ...(ngDevMode ? [{ debugName: "serverFilterOptions" }] : []));
160
+ this.loadingFilterOptions = signal({}, ...(ngDevMode ? [{ debugName: "loadingFilterOptions" }] : []));
161
+ // --- Derived computed signals ---
162
+ this.columns = computed(() => flattenColumns(this.columnsProp()), ...(ngDevMode ? [{ debugName: "columns" }] : []));
163
+ this.isServerSide = computed(() => this.dataSource() != null, ...(ngDevMode ? [{ debugName: "isServerSide" }] : []));
164
+ this.isClientSide = computed(() => !this.isServerSide(), ...(ngDevMode ? [{ debugName: "isClientSide" }] : []));
165
+ this.displayData = computed(() => this.data() ?? this.internalData(), ...(ngDevMode ? [{ debugName: "displayData" }] : []));
166
+ this.displayLoading = computed(() => this.controlledLoading() ?? this.internalLoading(), ...(ngDevMode ? [{ debugName: "displayLoading" }] : []));
167
+ this.defaultSortField = computed(() => this.defaultSortBy() ?? this.columns()[0]?.columnId ?? '', ...(ngDevMode ? [{ debugName: "defaultSortField" }] : []));
168
+ this.page = computed(() => this.controlledPage() ?? this.internalPage(), ...(ngDevMode ? [{ debugName: "page" }] : []));
169
+ this.pageSize = computed(() => this.controlledPageSize() ?? this.internalPageSizeOverride() ?? this.defaultPageSize(), ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
170
+ this.sort = computed(() => this.controlledSort() ?? this.internalSortOverride() ?? {
171
+ field: this.defaultSortField(),
172
+ direction: this.defaultSortDirection(),
173
+ }, ...(ngDevMode ? [{ debugName: "sort" }] : []));
174
+ this.filters = computed(() => this.controlledFilters() ?? this.internalFilters(), ...(ngDevMode ? [{ debugName: "filters" }] : []));
175
+ this.visibleColumns = computed(() => {
176
+ const controlled = this.controlledVisibleColumns();
177
+ if (controlled)
178
+ return controlled;
179
+ const override = this.internalVisibleColumnsOverride();
180
+ if (override)
181
+ return override;
182
+ const cols = this.columns();
183
+ if (cols.length === 0)
184
+ return new Set();
185
+ const visible = cols.filter((c) => c.defaultVisible !== false).map((c) => c.columnId);
186
+ return new Set(visible.length > 0 ? visible : cols.map((c) => c.columnId));
187
+ }, ...(ngDevMode ? [{ debugName: "visibleColumns" }] : []));
188
+ this.effectiveColumnOrder = computed(() => this.columnOrder() ?? this.internalColumnOrder(), ...(ngDevMode ? [{ debugName: "effectiveColumnOrder" }] : []));
189
+ this.effectiveSelectedRows = computed(() => this.selectedRows() ?? this.internalSelectedRows(), ...(ngDevMode ? [{ debugName: "effectiveSelectedRows" }] : []));
190
+ this.columnChooserPlacement = computed(() => {
191
+ const prop = this.columnChooserProp();
192
+ return prop === false ? 'none' : prop === 'sidebar' ? 'sidebar' : 'toolbar';
193
+ }, ...(ngDevMode ? [{ debugName: "columnChooserPlacement" }] : []));
194
+ this.multiSelectFilterFields = computed(() => getMultiSelectFilterFields(this.columns()), ...(ngDevMode ? [{ debugName: "multiSelectFilterFields" }] : []));
195
+ this.hasServerFilterOptions = computed(() => this.dataSource()?.fetchFilterOptions != null, ...(ngDevMode ? [{ debugName: "hasServerFilterOptions" }] : []));
196
+ this.clientFilterOptions = computed(() => {
197
+ if (this.hasServerFilterOptions())
198
+ return this.serverFilterOptions();
199
+ return deriveFilterOptionsFromData(this.displayData(), this.columns());
200
+ }, ...(ngDevMode ? [{ debugName: "clientFilterOptions" }] : []));
201
+ this.workerSortEnabled = computed(() => shouldUseWorkerSort(this.workerSort(), this.displayData().length, {
202
+ columns: this.columns(),
203
+ filters: this.filters(),
204
+ sortBy: this.sort().field,
205
+ }), ...(ngDevMode ? [{ debugName: "workerSortEnabled" }] : []));
206
+ /** Sync path: used when workerSort is off. */
207
+ this.clientItemsAndTotal = computed(() => {
208
+ if (!this.isClientSide() || this.workerSortEnabled())
209
+ return null;
210
+ const data = this.displayData();
211
+ const cols = this.columns();
212
+ const f = this.filters();
213
+ const sv = this.sortSnapshotVersion(); // reactive: increments on explicit sort
214
+ const sortField = this.sort().field;
215
+ const sortDir = this.sort().direction;
216
+ const needsResort = sv !== this.sortedPrevSortVersion ||
217
+ f !== this.sortedPrevFilters ||
218
+ cols !== this.sortedPrevColumns ||
219
+ data.length !== this.sortedPrevDataLength;
220
+ let orderedRows;
221
+ if (needsResort || this.sortedIndices === null) {
222
+ this.sortedPrevSortVersion = sv;
223
+ this.sortedPrevFilters = f;
224
+ this.sortedPrevColumns = cols;
225
+ this.sortedPrevDataLength = data.length;
226
+ const sorted = processClientSideData(data, cols, f, sortField, sortDir);
227
+ const indexMap = new Map();
228
+ for (let i = 0; i < data.length; i++)
229
+ indexMap.set(data[i], i);
230
+ this.sortedIndices = sorted.map((row) => {
231
+ const idx = indexMap.get(row);
232
+ return idx !== undefined ? idx : -1;
233
+ }).filter((idx) => idx !== -1);
234
+ orderedRows = sorted;
235
+ }
236
+ else {
237
+ // Cell edit: preserve existing order, look up updated row objects by index.
238
+ orderedRows = this.sortedIndices.map((idx) => data[idx]).filter((r) => r !== undefined);
239
+ }
240
+ const total = orderedRows.length;
241
+ const start = (this.page() - 1) * this.pageSize();
242
+ const paged = orderedRows.slice(start, start + this.pageSize());
243
+ return { items: paged, totalCount: total };
244
+ }, ...(ngDevMode ? [{ debugName: "clientItemsAndTotal" }] : []));
245
+ /** Resolved client items - sync or async depending on workerSort. */
246
+ this.resolvedClientItems = computed(() => {
247
+ // Sync path
248
+ const syncResult = this.clientItemsAndTotal();
249
+ if (syncResult)
250
+ return syncResult;
251
+ // Async path
252
+ return this.asyncClientItems();
253
+ }, ...(ngDevMode ? [{ debugName: "resolvedClientItems" }] : []));
254
+ this.displayItems = computed(() => {
255
+ const cit = this.resolvedClientItems();
256
+ return this.isClientSide() && cit ? cit.items : this.serverItems();
257
+ }, ...(ngDevMode ? [{ debugName: "displayItems" }] : []));
258
+ this.displayTotalCount = computed(() => {
259
+ const cit = this.resolvedClientItems();
260
+ return this.isClientSide() && cit ? cit.totalCount : this.serverTotalCount();
261
+ }, ...(ngDevMode ? [{ debugName: "displayTotalCount" }] : []));
262
+ this.hasActiveFilters = computed(() => {
263
+ return Object.values(this.filters()).some((v) => v !== undefined);
264
+ }, ...(ngDevMode ? [{ debugName: "hasActiveFilters" }] : []));
265
+ this.columnChooserColumns = computed(() => this.columns().map((c) => ({
266
+ columnId: c.columnId,
267
+ name: c.name,
268
+ required: c.required === true,
269
+ })), ...(ngDevMode ? [{ debugName: "columnChooserColumns" }] : []));
270
+ this.statusBarConfig = computed(() => {
271
+ const sb = this.statusBar();
272
+ if (!sb)
273
+ return undefined;
274
+ if (typeof sb === 'object')
275
+ return sb;
276
+ const totalData = this.isClientSide() ? (this.data()?.length ?? 0) : this.serverTotalCount();
277
+ const filteredData = this.displayTotalCount();
278
+ return {
279
+ totalCount: totalData,
280
+ filteredCount: this.hasActiveFilters() ? filteredData : undefined,
281
+ selectedCount: this.effectiveSelectedRows().size,
282
+ suppressRowCount: true,
283
+ };
284
+ }, ...(ngDevMode ? [{ debugName: "statusBarConfig" }] : []));
285
+ this.isLoadingResolved = computed(() => {
286
+ return (this.isServerSide() && this.serverLoading()) || this.displayLoading();
287
+ }, ...(ngDevMode ? [{ debugName: "isLoadingResolved" }] : []));
288
+ // Side bar
289
+ this.sideBarEnabled = computed(() => {
290
+ const config = this.sideBarConfig();
291
+ return config != null && config !== false;
292
+ }, ...(ngDevMode ? [{ debugName: "sideBarEnabled" }] : []));
293
+ this.sideBarParsed = computed(() => {
294
+ const config = this.sideBarConfig();
295
+ if (!this.sideBarEnabled() || config === true) {
296
+ return { panels: DEFAULT_PANELS, position: 'right', defaultPanel: null };
297
+ }
298
+ const def = config;
299
+ return {
300
+ panels: def.panels ?? DEFAULT_PANELS,
301
+ position: def.position ?? 'right',
302
+ defaultPanel: def.defaultPanel ?? null,
303
+ };
304
+ }, ...(ngDevMode ? [{ debugName: "sideBarParsed" }] : []));
305
+ this.filterableColumns = computed(() => this.columns()
306
+ .filter((c) => c.filterable && c.filterable.type)
307
+ .map((c) => ({
308
+ columnId: c.columnId,
309
+ name: c.name,
310
+ filterField: c.filterable?.filterField ?? c.columnId,
311
+ filterType: c.filterable?.type,
312
+ })), ...(ngDevMode ? [{ debugName: "filterableColumns" }] : []));
313
+ this.sideBarState = computed(() => ({
314
+ isEnabled: this.sideBarEnabled(),
315
+ activePanel: this.sideBarActivePanel(),
316
+ setActivePanel: (panel) => this.sideBarActivePanel.set(panel),
317
+ panels: this.sideBarParsed().panels,
318
+ position: this.sideBarParsed().position,
319
+ isOpen: this.sideBarActivePanel() !== null,
320
+ toggle: (panel) => this.sideBarActivePanel.update((p) => p === panel ? null : panel),
321
+ close: () => this.sideBarActivePanel.set(null),
322
+ }), ...(ngDevMode ? [{ debugName: "sideBarState" }] : []));
323
+ // --- Formula bar derived state ---
324
+ /** Display text derived from active cell (formula string or raw value). */
325
+ this.formulaBarDisplayText = computed(() => {
326
+ const coords = this.activeCellCoords();
327
+ if (!coords)
328
+ return '';
329
+ const getFormula = this.formulaService.enabled()
330
+ ? (c, r) => this.formulaService.getFormula(c, r)
331
+ : undefined;
332
+ const items = this.displayItems();
333
+ const cols = this.columns();
334
+ const getRawValue = (c, r) => {
335
+ if (r < 0 || r >= items.length || c < 0 || c >= cols.length)
336
+ return undefined;
337
+ return getCellValue(items[r], cols[c]);
338
+ };
339
+ return deriveFormulaBarText(coords.col, coords.row, getFormula, getRawValue);
340
+ }, ...(ngDevMode ? [{ debugName: "formulaBarDisplayText" }] : []));
341
+ /** Formula text shown in the bar: edit text when editing, display text otherwise. */
342
+ this.formulaBarText = computed(() => this.formulaBarEditing() ? this.formulaBarEditText() : this.formulaBarDisplayText(), ...(ngDevMode ? [{ debugName: "formulaBarText" }] : []));
343
+ /** References extracted from the current formula text (for highlighting). */
344
+ this.formulaBarReferences = computed(() => extractFormulaReferences(this.formulaBarText()), ...(ngDevMode ? [{ debugName: "formulaBarReferences" }] : []));
345
+ // Stable formula bar callbacks (avoid new closures per computed)
346
+ this.formulaBarOnInputChangeFn = (text) => { this.formulaBarEditText.set(text); };
347
+ this.formulaBarOnCommitFn = () => { this.commitFormulaBar(); };
348
+ this.formulaBarOnCancelFn = () => { this.cancelFormulaBar(); };
349
+ this.formulaBarStartEditingFn = () => { this.startFormulaBarEditing(); };
350
+ /** Aggregate formula bar state for template consumption. */
351
+ this.formulaBarState = computed(() => ({
352
+ cellRef: this.activeCellRef(),
353
+ formulaText: this.formulaBarText(),
354
+ isEditing: this.formulaBarEditing(),
355
+ onInputChange: this.formulaBarOnInputChangeFn,
356
+ onCommit: this.formulaBarOnCommitFn,
357
+ onCancel: this.formulaBarOnCancelFn,
358
+ startEditing: this.formulaBarStartEditingFn,
359
+ referencedCells: this.formulaBarReferences(),
360
+ }), ...(ngDevMode ? [{ debugName: "formulaBarState" }] : []));
361
+ // --- Pre-computed stable callback references for dataGridProps ---
362
+ // These avoid recreating arrow functions on every dataGridProps recomputation.
363
+ this.handleSortFn = (columnKey, direction) => this.handleSort(columnKey, direction);
364
+ this.handleColumnResizedFn = (columnId, width) => this.handleColumnResized(columnId, width);
365
+ this.handleColumnPinnedFn = (columnId, pinned) => this.handleColumnPinned(columnId, pinned);
366
+ this.handleSelectionChangeFn = (event) => this.handleSelectionChange(event);
367
+ this.handleFilterChangeFn = (key, value) => this.handleFilterChange(key, value);
368
+ this.clearAllFiltersFn = () => this.setFilters({});
369
+ this.setPageFn = (p) => this.setPage(p);
370
+ this.setPageSizeFn = (size) => this.setPageSize(size);
371
+ this.handleVisibilityChangeFn = (columnKey, isVisible) => this.handleVisibilityChange(columnKey, isVisible);
372
+ // --- Data grid props computed ---
373
+ this.dataGridProps = computed(() => ({
374
+ items: this.displayItems(),
375
+ columns: this.columnsProp(),
376
+ getRowId: this.getRowId(),
377
+ sortBy: this.sort().field,
378
+ sortDirection: this.sort().direction,
379
+ onColumnSort: this.handleSortFn,
380
+ visibleColumns: this.visibleColumns(),
381
+ columnOrder: this.effectiveColumnOrder(),
382
+ onColumnOrderChange: this.onColumnOrderChange(),
383
+ onColumnResized: this.handleColumnResizedFn,
384
+ onAutosizeColumn: this.onAutosizeColumn(),
385
+ onColumnPinned: this.handleColumnPinnedFn,
386
+ pinnedColumns: this.pinnedOverrides(),
387
+ initialColumnWidths: this.columnWidthOverrides(),
388
+ editable: this.editable(),
389
+ cellSelection: this.cellSelection(),
390
+ density: this.density(),
391
+ rowHeight: this.rowHeight(),
392
+ onCellValueChanged: this.onCellValueChanged(),
393
+ onUndo: this.onUndo(),
394
+ onRedo: this.onRedo(),
395
+ canUndo: this.canUndo(),
396
+ canRedo: this.canRedo(),
397
+ rowSelection: this.rowSelection(),
398
+ selectedRows: this.effectiveSelectedRows(),
399
+ onSelectionChange: this.handleSelectionChangeFn,
400
+ showRowNumbers: this.showRowNumbers() || this.cellReferences() || this.formulasEnabled(),
401
+ showColumnLetters: !!(this.cellReferences() || this.formulasEnabled()),
402
+ showNameBox: !!(this.cellReferences() && !this.formulasEnabled()),
403
+ onActiveCellChange: (this.cellReferences() || this.formulasEnabled()) ? this.handleActiveCellChange : undefined,
404
+ currentPage: this.page(),
405
+ pageSize: this.pageSize(),
406
+ statusBar: this.statusBarConfig(),
407
+ isLoading: this.isLoadingResolved(),
408
+ filters: this.filters(),
409
+ onFilterChange: this.handleFilterChangeFn,
410
+ filterOptions: this.clientFilterOptions(),
411
+ loadingFilterOptions: this.dataSource()?.fetchFilterOptions ? this.loadingFilterOptions() : EMPTY_LOADING_OPTIONS,
412
+ peopleSearch: this.dataSource()?.searchPeople?.bind(this.dataSource()),
413
+ getUserByEmail: this.dataSource()?.getUserByEmail?.bind(this.dataSource()),
414
+ layoutMode: this.layoutMode(),
415
+ suppressHorizontalScroll: this.suppressHorizontalScroll(),
416
+ stickyHeader: this.stickyHeader(),
417
+ columnReorder: this.columnReorder(),
418
+ responsiveColumns: this.responsiveColumns(),
419
+ virtualScroll: this.virtualScroll(),
420
+ 'aria-label': this.ariaLabel(),
421
+ 'aria-labelledby': this.ariaLabelledBy(),
422
+ emptyState: {
423
+ hasActiveFilters: this.hasActiveFilters(),
424
+ onClearAll: this.clearAllFiltersFn,
425
+ message: this.emptyState()?.message,
426
+ render: this.emptyState()?.render,
427
+ },
428
+ formulas: this.formulasEnabled(),
429
+ formulaVersion: this.formulaVersion(),
430
+ formulaReferences: this.formulaBarReferences().length > 0 ? this.formulaBarReferences() : undefined,
431
+ ...(this.formulaService.enabled() ? {
432
+ getFormulaValue: this.getFormulaValueFn,
433
+ hasFormula: this.hasFormulaFn,
434
+ getFormula: this.getFormulaFn,
435
+ setFormula: this.setFormulaFn,
436
+ onFormulaCellChanged: this.onFormulaCellChangedFn,
437
+ getPrecedents: this.getPrecedentsFn,
438
+ getDependents: this.getDependentsFn,
439
+ getAuditTrail: this.getAuditTrailFn,
440
+ } : {}),
441
+ }), ...(ngDevMode ? [{ debugName: "dataGridProps" }] : []));
442
+ this.pagination = computed(() => ({
443
+ page: this.page(),
444
+ pageSize: this.pageSize(),
445
+ displayTotalCount: this.displayTotalCount(),
446
+ setPage: this.setPageFn,
447
+ setPageSize: this.setPageSizeFn,
448
+ pageSizeOptions: this.pageSizeOptions(),
449
+ entityLabelPlural: this.entityLabelPlural(),
450
+ }), ...(ngDevMode ? [{ debugName: "pagination" }] : []));
451
+ this.columnChooser = computed(() => ({
452
+ columns: this.columnChooserColumns(),
453
+ visibleColumns: this.visibleColumns(),
454
+ onVisibilityChange: this.handleVisibilityChangeFn,
455
+ placement: this.columnChooserPlacement(),
456
+ }), ...(ngDevMode ? [{ debugName: "columnChooser" }] : []));
457
+ this.filtersResult = computed(() => ({
458
+ hasActiveFilters: this.hasActiveFilters(),
459
+ setFilters: (f) => this.setFilters(f),
460
+ }), ...(ngDevMode ? [{ debugName: "filtersResult" }] : []));
461
+ this.sideBarProps = computed(() => {
462
+ const state = this.sideBarState();
463
+ if (!state.isEnabled)
464
+ return null;
465
+ return {
466
+ activePanel: state.activePanel,
467
+ onPanelChange: state.setActivePanel,
468
+ panels: state.panels,
469
+ position: state.position,
470
+ columns: this.columnChooserColumns(),
471
+ visibleColumns: this.visibleColumns(),
472
+ onVisibilityChange: (columnKey, visible) => this.handleVisibilityChange(columnKey, visible),
473
+ onSetVisibleColumns: (cols) => this.setVisibleColumns(cols),
474
+ filterableColumns: this.filterableColumns(),
475
+ filters: this.filters(),
476
+ onFilterChange: (key, value) => this.handleFilterChange(key, value),
477
+ filterOptions: this.clientFilterOptions(),
478
+ };
479
+ }, ...(ngDevMode ? [{ debugName: "sideBarProps" }] : []));
480
+ // Validate columns once (on first non-empty columns signal)
481
+ let columnsValidated = false;
482
+ effect(() => {
483
+ const cols = this.columns();
484
+ if (!columnsValidated && cols.length > 0) {
485
+ columnsValidated = true;
486
+ validateColumns(cols);
487
+ }
488
+ });
489
+ // Worker sort async effect - runs processClientSideDataAsync when workerSort is on
490
+ // Uses the same index-based snapshot approach as the sync path.
491
+ let asyncSortedIndices = null;
492
+ let asyncPrevSortVersion = -1;
493
+ let asyncPrevFilters = null;
494
+ let asyncPrevColumns = null;
495
+ let asyncPrevDataLength = -1;
496
+ effect((onCleanup) => {
497
+ if (!this.isClientSide() || !this.workerSortEnabled())
498
+ return;
499
+ const data = this.displayData();
500
+ const cols = this.columns();
501
+ const filters = this.filters();
502
+ const sv = this.sortSnapshotVersion(); // reactive trigger for explicit sort changes
503
+ const sortField = this.sort().field;
504
+ const sortDir = this.sort().direction;
505
+ const page = this.page();
506
+ const ps = this.pageSize();
507
+ const needsResortAsync = sv !== asyncPrevSortVersion ||
508
+ filters !== asyncPrevFilters ||
509
+ cols !== asyncPrevColumns ||
510
+ data.length !== asyncPrevDataLength;
511
+ const abortId = ++this.workerSortAbortId;
512
+ if (needsResortAsync || asyncSortedIndices === null) {
513
+ asyncPrevSortVersion = sv;
514
+ asyncPrevFilters = filters;
515
+ asyncPrevColumns = cols;
516
+ asyncPrevDataLength = data.length;
517
+ asyncSortedIndices = null;
518
+ processClientSideDataAsync(data, cols, filters, sortField, sortDir)
519
+ .then((rows) => {
520
+ if (abortId !== this.workerSortAbortId)
521
+ return; // stale
522
+ const indexMap = new Map();
523
+ for (let i = 0; i < data.length; i++)
524
+ indexMap.set(data[i], i);
525
+ asyncSortedIndices = rows.map((row) => {
526
+ const idx = indexMap.get(row);
527
+ return idx !== undefined ? idx : -1;
528
+ }).filter((idx) => idx !== -1);
529
+ const total = rows.length;
530
+ const start = (page - 1) * ps;
531
+ const paged = rows.slice(start, start + ps);
532
+ this.asyncClientItems.set({ items: paged, totalCount: total });
533
+ })
534
+ .catch(() => {
535
+ if (abortId !== this.workerSortAbortId)
536
+ return;
537
+ // Fallback: use sync
538
+ const rows = processClientSideData(data, cols, filters, sortField, sortDir);
539
+ const indexMap = new Map();
540
+ for (let i = 0; i < data.length; i++)
541
+ indexMap.set(data[i], i);
542
+ asyncSortedIndices = rows.map((row) => {
543
+ const idx = indexMap.get(row);
544
+ return idx !== undefined ? idx : -1;
545
+ }).filter((idx) => idx !== -1);
546
+ const total = rows.length;
547
+ const start = (page - 1) * ps;
548
+ const paged = rows.slice(start, start + ps);
549
+ this.asyncClientItems.set({ items: paged, totalCount: total });
550
+ });
551
+ }
552
+ else {
553
+ // Preserve order: look up updated rows by stored indices.
554
+ const orderedRows = asyncSortedIndices.map((idx) => data[idx]).filter((r) => r !== undefined);
555
+ const total = orderedRows.length;
556
+ const start = (page - 1) * ps;
557
+ const paged = orderedRows.slice(start, start + ps);
558
+ this.asyncClientItems.set({ items: paged, totalCount: total });
559
+ }
560
+ onCleanup(() => {
561
+ this.workerSortAbortId++;
562
+ });
563
+ });
564
+ // Server-side data fetching effect
565
+ effect((onCleanup) => {
566
+ const ds = this.dataSource();
567
+ if (!this.isServerSide() || !ds) {
568
+ if (!this.isServerSide())
569
+ this.serverLoading.set(false);
570
+ return;
571
+ }
572
+ const page = this.page();
573
+ const pageSize = this.pageSize();
574
+ const sort = this.sort();
575
+ const filters = this.filters();
576
+ // Read refreshCounter to trigger re-fetches
577
+ this.refreshCounter();
578
+ const controller = new AbortController();
579
+ this.fetchAbortController = controller;
580
+ this.serverLoading.set(true);
581
+ ds.fetchPage({
582
+ page,
583
+ pageSize,
584
+ sort: { field: sort.field, direction: sort.direction },
585
+ filters,
586
+ signal: controller.signal,
587
+ })
588
+ .then((res) => {
589
+ if (controller.signal.aborted)
590
+ return;
591
+ this.serverItems.set(res.items);
592
+ this.serverTotalCount.set(res.totalCount);
593
+ })
594
+ .catch((err) => {
595
+ if (controller.signal.aborted)
596
+ return;
597
+ this.onError()?.(err);
598
+ this.serverItems.set([]);
599
+ this.serverTotalCount.set(0);
600
+ })
601
+ .finally(() => {
602
+ if (!controller.signal.aborted)
603
+ this.serverLoading.set(false);
604
+ });
605
+ onCleanup(() => {
606
+ controller.abort();
607
+ });
608
+ });
609
+ // Fire onFirstDataRendered once; also validate row IDs on first data
610
+ let rowIdsValidated = false;
611
+ effect(() => {
612
+ const items = this.displayItems();
613
+ if (!this.firstDataRendered() && items.length > 0) {
614
+ this.firstDataRendered.set(true);
615
+ this.onFirstDataRendered()?.();
616
+ }
617
+ if (!rowIdsValidated && items.length > 0) {
618
+ rowIdsValidated = true;
619
+ validateRowIds(items, this.getRowId());
620
+ }
621
+ });
622
+ // Load server filter options
623
+ effect((onCleanup) => {
624
+ const ds = this.dataSource();
625
+ const fields = this.multiSelectFilterFields();
626
+ const fetcher = ds && 'fetchFilterOptions' in ds && typeof ds.fetchFilterOptions === 'function'
627
+ ? ds.fetchFilterOptions.bind(ds)
628
+ : undefined;
629
+ if (!fetcher || fields.length === 0) {
630
+ this.serverFilterOptions.set({});
631
+ this.loadingFilterOptions.set({});
632
+ return;
633
+ }
634
+ const controller = new AbortController();
635
+ this.filterAbortController = controller;
636
+ const loading = {};
637
+ fields.forEach((f) => { loading[f] = true; });
638
+ this.loadingFilterOptions.set(loading);
639
+ const results = {};
640
+ Promise.all(fields.map(async (field) => {
641
+ try {
642
+ results[field] = await fetcher(field);
643
+ }
644
+ catch {
645
+ results[field] = [];
646
+ }
647
+ })).then(() => {
648
+ if (controller.signal.aborted)
649
+ return;
650
+ this.serverFilterOptions.set(results);
651
+ this.loadingFilterOptions.set({});
652
+ });
653
+ onCleanup(() => {
654
+ controller.abort();
655
+ });
656
+ });
657
+ // Initialize sidebar default panel
658
+ effect(() => {
659
+ const parsed = this.sideBarParsed();
660
+ if (parsed.defaultPanel) {
661
+ this.sideBarActivePanel.set(parsed.defaultPanel);
662
+ }
663
+ });
664
+ // Reset formula bar editing when active cell changes
665
+ effect(() => {
666
+ // Read active cell coords to trigger on cell change
667
+ this.activeCellCoords();
668
+ this.formulaBarEditing.set(false);
669
+ });
670
+ // Formula engine: configure when formula signals change
671
+ effect(() => {
672
+ const userRecalcCb = this.onFormulaRecalc();
673
+ this.formulaService.configure({
674
+ formulas: this.formulasEnabled(),
675
+ initialFormulas: this.initialFormulas(),
676
+ formulaFunctions: this.formulaFunctions(),
677
+ onFormulaRecalc: (result) => {
678
+ this.formulaVersion.update((v) => v + 1);
679
+ userRecalcCb?.(result);
680
+ },
681
+ namedRanges: this.namedRanges(),
682
+ sheets: this.sheets(),
683
+ });
684
+ });
685
+ // Formula engine: keep data in sync with display items + columns
686
+ effect(() => {
687
+ const items = this.displayItems();
688
+ const cols = this.columns();
689
+ this.formulaService.setData(items, cols);
690
+ });
691
+ // Cleanup on destroy - abort in-flight requests and reset callback signals
692
+ this.destroyRef.onDestroy(() => {
693
+ this.fetchAbortController?.abort();
694
+ this.filterAbortController?.abort();
695
+ this.fetchAbortController = null;
696
+ this.filterAbortController = null;
697
+ this.onPageChange.set(undefined);
698
+ this.onPageSizeChange.set(undefined);
699
+ this.onSortChange.set(undefined);
700
+ this.onFiltersChange.set(undefined);
701
+ this.onVisibleColumnsChange.set(undefined);
702
+ this.onColumnOrderChange.set(undefined);
703
+ this.onColumnResized.set(undefined);
704
+ this.onAutosizeColumn.set(undefined);
705
+ this.onColumnPinned.set(undefined);
706
+ this.onCellValueChanged.set(undefined);
707
+ this.onSelectionChange.set(undefined);
708
+ this.onFirstDataRendered.set(undefined);
709
+ this.onError.set(undefined);
710
+ this.onUndo.set(undefined);
711
+ this.onRedo.set(undefined);
712
+ });
713
+ }
714
+ // --- Setters ---
715
+ setPage(p) {
716
+ if (this.controlledPage() === undefined)
717
+ this.internalPage.set(p);
718
+ this.onPageChange()?.(p);
719
+ }
720
+ setPageSize(size) {
721
+ if (this.controlledPageSize() === undefined)
722
+ this.internalPageSizeOverride.set(size);
723
+ this.onPageSizeChange()?.(size);
724
+ this.setPage(1);
725
+ }
726
+ setSort(s) {
727
+ if (this.controlledSort() === undefined)
728
+ this.internalSortOverride.set(s);
729
+ this.onSortChange()?.(s);
730
+ this.setPage(1);
731
+ // Invalidate sorted indices so clientItemsAndTotal re-sorts with the new field/direction.
732
+ this.sortedIndices = null;
733
+ this.sortSnapshotVersion.update((v) => v + 1);
734
+ }
735
+ setFilters(f) {
736
+ if (this.controlledFilters() === undefined)
737
+ this.internalFilters.set(f);
738
+ this.onFiltersChange()?.(f);
739
+ this.setPage(1);
740
+ }
741
+ setVisibleColumns(cols) {
742
+ if (this.controlledVisibleColumns() === undefined)
743
+ this.internalVisibleColumnsOverride.set(cols);
744
+ this.onVisibleColumnsChange()?.(cols);
745
+ }
746
+ handleSort(columnKey, direction) {
747
+ this.setSort(computeNextSortState(this.sort(), columnKey, direction));
748
+ }
749
+ handleFilterChange(key, value) {
750
+ this.setFilters(mergeFilter(this.filters(), key, value));
751
+ }
752
+ handleVisibilityChange(columnKey, isVisible) {
753
+ const next = new Set(this.visibleColumns());
754
+ if (isVisible)
755
+ next.add(columnKey);
756
+ else
757
+ next.delete(columnKey);
758
+ this.setVisibleColumns(next);
759
+ }
760
+ handleSelectionChange(event) {
761
+ if (this.selectedRows() === undefined) {
762
+ this.internalSelectedRows.set(new Set(event.selectedRowIds));
763
+ }
764
+ this.onSelectionChange()?.(event);
765
+ }
766
+ handleColumnResized(columnId, width) {
767
+ this.columnWidthOverrides.update((prev) => ({ ...prev, [columnId]: width }));
768
+ this.onColumnResized()?.(columnId, width);
769
+ }
770
+ handleColumnPinned(columnId, pinned) {
771
+ this.pinnedOverrides.update((prev) => {
772
+ if (pinned === null) {
773
+ const { [columnId]: _removed, ...rest } = prev;
774
+ return rest;
775
+ }
776
+ return { ...prev, [columnId]: pinned };
777
+ });
778
+ this.onColumnPinned()?.(columnId, pinned);
779
+ }
780
+ // --- Formula bar methods ---
781
+ startFormulaBarEditing() {
782
+ this.formulaBarEditText.set(this.formulaBarDisplayText());
783
+ this.formulaBarEditing.set(true);
784
+ }
785
+ commitFormulaBar() {
786
+ const coords = this.activeCellCoords();
787
+ if (!coords)
788
+ return;
789
+ const text = this.formulaBarEditText().trim();
790
+ if (text.startsWith('=')) {
791
+ this.formulaService.setFormula(coords.col, coords.row, text);
792
+ this.formulaVersion.update((v) => v + 1);
793
+ }
794
+ else {
795
+ this.formulaService.setFormula(coords.col, coords.row, null);
796
+ this.onCellValueChanged()?.({
797
+ rowIndex: coords.row,
798
+ columnId: this.columns()[coords.col]?.columnId ?? '',
799
+ oldValue: undefined,
800
+ newValue: text,
801
+ });
802
+ }
803
+ this.formulaBarEditing.set(false);
804
+ }
805
+ cancelFormulaBar() {
806
+ this.formulaBarEditing.set(false);
807
+ this.formulaBarEditText.set('');
808
+ }
809
+ // --- Configure from props ---
810
+ configure(props) {
811
+ this.columnsProp.set(props.columns);
812
+ // Initialize pinned overrides from column definitions on first configure
813
+ if (Object.keys(this.pinnedOverrides()).length === 0) {
814
+ const initial = {};
815
+ for (const col of flattenColumns(props.columns)) {
816
+ if (col.pinned)
817
+ initial[col.columnId] = col.pinned;
818
+ }
819
+ if (Object.keys(initial).length > 0)
820
+ this.pinnedOverrides.set(initial);
821
+ }
822
+ this.getRowId.set(props.getRowId);
823
+ if ('data' in props && props.data !== undefined)
824
+ this.data.set(props.data);
825
+ if ('dataSource' in props && props.dataSource !== undefined)
826
+ this.dataSource.set(props.dataSource);
827
+ if (props.page !== undefined)
828
+ this.controlledPage.set(props.page);
829
+ if (props.pageSize !== undefined)
830
+ this.controlledPageSize.set(props.pageSize);
831
+ if (props.sort !== undefined)
832
+ this.controlledSort.set(props.sort);
833
+ if (props.filters !== undefined)
834
+ this.controlledFilters.set(props.filters);
835
+ if (props.visibleColumns !== undefined)
836
+ this.controlledVisibleColumns.set(props.visibleColumns);
837
+ if (props.isLoading !== undefined)
838
+ this.controlledLoading.set(props.isLoading);
839
+ if (props.onPageChange)
840
+ this.onPageChange.set(props.onPageChange);
841
+ if (props.onPageSizeChange)
842
+ this.onPageSizeChange.set(props.onPageSizeChange);
843
+ if (props.onSortChange)
844
+ this.onSortChange.set(props.onSortChange);
845
+ if (props.onFiltersChange)
846
+ this.onFiltersChange.set(props.onFiltersChange);
847
+ if (props.onVisibleColumnsChange)
848
+ this.onVisibleColumnsChange.set(props.onVisibleColumnsChange);
849
+ if (props.columnOrder !== undefined)
850
+ this.columnOrder.set(props.columnOrder);
851
+ if (props.onColumnOrderChange)
852
+ this.onColumnOrderChange.set(props.onColumnOrderChange);
853
+ if (props.onColumnResized)
854
+ this.onColumnResized.set(props.onColumnResized);
855
+ if (props.onAutosizeColumn)
856
+ this.onAutosizeColumn.set(props.onAutosizeColumn);
857
+ if (props.onColumnPinned)
858
+ this.onColumnPinned.set(props.onColumnPinned);
859
+ if (props.defaultPageSize !== undefined)
860
+ this.defaultPageSize.set(props.defaultPageSize);
861
+ if (props.defaultSortBy !== undefined)
862
+ this.defaultSortBy.set(props.defaultSortBy);
863
+ if (props.defaultSortDirection !== undefined)
864
+ this.defaultSortDirection.set(props.defaultSortDirection);
865
+ if (props.editable !== undefined)
866
+ this.editable.set(props.editable);
867
+ if (props.cellSelection !== undefined)
868
+ this.cellSelection.set(props.cellSelection);
869
+ if (props.density !== undefined)
870
+ this.density.set(props.density);
871
+ if (props.rowHeight !== undefined)
872
+ this.rowHeight.set(props.rowHeight);
873
+ if (props.onCellValueChanged)
874
+ this.onCellValueChanged.set(props.onCellValueChanged);
875
+ if (props.onUndo)
876
+ this.onUndo.set(props.onUndo);
877
+ if (props.onRedo)
878
+ this.onRedo.set(props.onRedo);
879
+ if (props.canUndo !== undefined)
880
+ this.canUndo.set(props.canUndo);
881
+ if (props.canRedo !== undefined)
882
+ this.canRedo.set(props.canRedo);
883
+ if (props.rowSelection !== undefined)
884
+ this.rowSelection.set(props.rowSelection);
885
+ if (props.selectedRows !== undefined)
886
+ this.selectedRows.set(props.selectedRows);
887
+ if (props.onSelectionChange)
888
+ this.onSelectionChange.set(props.onSelectionChange);
889
+ if (props.statusBar !== undefined)
890
+ this.statusBar.set(props.statusBar);
891
+ if (props.pageSizeOptions !== undefined)
892
+ this.pageSizeOptions.set(props.pageSizeOptions);
893
+ if (props.sideBar !== undefined)
894
+ this.sideBarConfig.set(props.sideBar);
895
+ if (props.onFirstDataRendered)
896
+ this.onFirstDataRendered.set(props.onFirstDataRendered);
897
+ if (props.onError)
898
+ this.onError.set(props.onError);
899
+ if (props.columnChooser !== undefined)
900
+ this.columnChooserProp.set(props.columnChooser);
901
+ if (props.columnReorder !== undefined)
902
+ this.columnReorder.set(props.columnReorder);
903
+ if (props.responsiveColumns !== undefined)
904
+ this.responsiveColumns.set(props.responsiveColumns);
905
+ if (props.virtualScroll !== undefined)
906
+ this.virtualScroll.set(props.virtualScroll);
907
+ if (props.workerSort !== undefined)
908
+ this.workerSort.set(props.workerSort);
909
+ if (props.showRowNumbers !== undefined)
910
+ this.showRowNumbers.set(props.showRowNumbers);
911
+ if (props.cellReferences !== undefined)
912
+ this.cellReferences.set(props.cellReferences);
913
+ if (props.formulas !== undefined)
914
+ this.formulasEnabled.set(props.formulas);
915
+ if (props.initialFormulas !== undefined)
916
+ this.initialFormulas.set(props.initialFormulas);
917
+ if (props.onFormulaRecalc)
918
+ this.onFormulaRecalc.set(props.onFormulaRecalc);
919
+ if (props.formulaFunctions !== undefined)
920
+ this.formulaFunctions.set(props.formulaFunctions);
921
+ if (props.namedRanges !== undefined)
922
+ this.namedRanges.set(props.namedRanges);
923
+ if (props.sheets !== undefined)
924
+ this.sheets.set(props.sheets);
925
+ if (props.sheetDefs !== undefined)
926
+ this.sheetDefs.set(props.sheetDefs);
927
+ if (props.activeSheet !== undefined)
928
+ this.activeSheet.set(props.activeSheet);
929
+ if (props.onSheetChange)
930
+ this.onSheetChange.set(props.onSheetChange);
931
+ if (props.onSheetAdd)
932
+ this.onSheetAdd.set(props.onSheetAdd);
933
+ if (props.entityLabelPlural !== undefined)
934
+ this.entityLabelPlural.set(props.entityLabelPlural);
935
+ if (props.className !== undefined)
936
+ this.className.set(props.className);
937
+ if (props.layoutMode !== undefined)
938
+ this.layoutMode.set(props.layoutMode);
939
+ if (props.suppressHorizontalScroll !== undefined)
940
+ this.suppressHorizontalScroll.set(props.suppressHorizontalScroll);
941
+ if (props.stickyHeader !== undefined)
942
+ this.stickyHeader.set(props.stickyHeader);
943
+ if (props.fullScreen !== undefined)
944
+ this.fullScreen.set(props.fullScreen);
945
+ if (props['aria-label'] !== undefined)
946
+ this.ariaLabel.set(props['aria-label']);
947
+ if (props['aria-labelledby'] !== undefined)
948
+ this.ariaLabelledBy.set(props['aria-labelledby']);
949
+ }
950
+ // --- API ---
951
+ // --- Column Pinning Methods ---
952
+ /**
953
+ * Pin a column to the left or right edge.
954
+ */
955
+ pinColumn(columnId, side) {
956
+ this.pinnedOverrides.update((prev) => ({ ...prev, [columnId]: side }));
957
+ this.onColumnPinned()?.(columnId, side);
958
+ }
959
+ /**
960
+ * Unpin a column (remove sticky positioning).
961
+ */
962
+ unpinColumn(columnId) {
963
+ this.pinnedOverrides.update((prev) => {
964
+ const { [columnId]: _, ...next } = prev;
965
+ return next;
966
+ });
967
+ this.onColumnPinned()?.(columnId, null);
968
+ }
969
+ /**
970
+ * Check if a column is pinned and which side.
971
+ */
972
+ isPinned(columnId) {
973
+ return this.pinnedOverrides()[columnId];
974
+ }
975
+ /**
976
+ * Compute sticky left offsets for left-pinned columns.
977
+ * Returns a map of columnId -> left offset in pixels.
978
+ */
979
+ computeLeftOffsets(visibleCols, columnWidths, defaultWidth, hasCheckboxColumn, checkboxColumnWidth) {
980
+ const offsets = {};
981
+ const pinned = this.pinnedOverrides();
982
+ let left = hasCheckboxColumn ? checkboxColumnWidth : 0;
983
+ for (const col of visibleCols) {
984
+ if (pinned[col.columnId] === 'left') {
985
+ offsets[col.columnId] = left;
986
+ left += columnWidths[col.columnId] ?? defaultWidth;
987
+ }
988
+ }
989
+ return offsets;
990
+ }
991
+ /**
992
+ * Compute sticky right offsets for right-pinned columns.
993
+ * Returns a map of columnId -> right offset in pixels.
994
+ */
995
+ computeRightOffsets(visibleCols, columnWidths, defaultWidth) {
996
+ const offsets = {};
997
+ const pinned = this.pinnedOverrides();
998
+ let right = 0;
999
+ for (let i = visibleCols.length - 1; i >= 0; i--) {
1000
+ const col = visibleCols[i];
1001
+ if (pinned[col.columnId] === 'right') {
1002
+ offsets[col.columnId] = right;
1003
+ right += columnWidths[col.columnId] ?? defaultWidth;
1004
+ }
1005
+ }
1006
+ return offsets;
1007
+ }
1008
+ getApi() {
1009
+ return {
1010
+ setRowData: (d) => {
1011
+ if (!this.isServerSide())
1012
+ this.internalData.set(d);
1013
+ },
1014
+ setLoading: (loading) => this.internalLoading.set(loading),
1015
+ getColumnState: () => ({
1016
+ visibleColumns: Array.from(this.visibleColumns()),
1017
+ sort: this.sort(),
1018
+ columnOrder: this.effectiveColumnOrder() ?? undefined,
1019
+ columnWidths: Object.keys(this.columnWidthOverrides()).length > 0 ? this.columnWidthOverrides() : undefined,
1020
+ filters: Object.keys(this.filters()).length > 0 ? this.filters() : undefined,
1021
+ pinnedColumns: Object.keys(this.pinnedOverrides()).length > 0 ? this.pinnedOverrides() : undefined,
1022
+ }),
1023
+ applyColumnState: (state) => {
1024
+ if (state.visibleColumns)
1025
+ this.setVisibleColumns(new Set(state.visibleColumns));
1026
+ if (state.sort)
1027
+ this.setSort(state.sort);
1028
+ if (state.columnOrder) {
1029
+ if (this.columnOrder() === undefined)
1030
+ this.internalColumnOrder.set(state.columnOrder);
1031
+ this.onColumnOrderChange()?.(state.columnOrder);
1032
+ }
1033
+ if (state.columnWidths)
1034
+ this.columnWidthOverrides.set(state.columnWidths);
1035
+ if (state.filters)
1036
+ this.setFilters(state.filters);
1037
+ if (state.pinnedColumns)
1038
+ this.pinnedOverrides.set(state.pinnedColumns);
1039
+ },
1040
+ setFilterModel: (filters) => this.setFilters(filters),
1041
+ getSelectedRows: () => Array.from(this.effectiveSelectedRows()),
1042
+ setSelectedRows: (rowIds) => {
1043
+ if (this.selectedRows() === undefined)
1044
+ this.internalSelectedRows.set(new Set(rowIds));
1045
+ },
1046
+ selectAll: () => {
1047
+ const allIds = new Set(this.displayItems().map((item) => this.getRowId()(item)));
1048
+ if (this.selectedRows() === undefined)
1049
+ this.internalSelectedRows.set(allIds);
1050
+ this.onSelectionChange()?.({
1051
+ selectedRowIds: Array.from(allIds),
1052
+ selectedItems: this.displayItems(),
1053
+ });
1054
+ },
1055
+ deselectAll: () => {
1056
+ if (this.selectedRows() === undefined)
1057
+ this.internalSelectedRows.set(new Set());
1058
+ this.onSelectionChange()?.({ selectedRowIds: [], selectedItems: [] });
1059
+ },
1060
+ clearFilters: () => this.setFilters({}),
1061
+ clearSort: () => this.setSort({ field: this.defaultSortField(), direction: this.defaultSortDirection() }),
1062
+ resetGridState: (options) => {
1063
+ this.setFilters({});
1064
+ this.setSort({ field: this.defaultSortField(), direction: this.defaultSortDirection() });
1065
+ if (!options?.keepSelection) {
1066
+ if (this.selectedRows() === undefined)
1067
+ this.internalSelectedRows.set(new Set());
1068
+ this.onSelectionChange()?.({ selectedRowIds: [], selectedItems: [] });
1069
+ }
1070
+ },
1071
+ getDisplayedRows: () => this.displayItems(),
1072
+ refreshData: () => {
1073
+ if (this.isServerSide()) {
1074
+ this.refreshCounter.update((c) => c + 1);
1075
+ }
1076
+ },
1077
+ getColumnOrder: () => this.effectiveColumnOrder() ?? this.columns().map((c) => c.columnId),
1078
+ setColumnOrder: (order) => {
1079
+ if (this.columnOrder() === undefined)
1080
+ this.internalColumnOrder.set(order);
1081
+ this.onColumnOrderChange()?.(order);
1082
+ },
1083
+ scrollToRow: (_index, _options) => {
1084
+ // Scrolling is handled by VirtualScrollService at the UI layer.
1085
+ // The UI component should wire this to VirtualScrollService.scrollToRow().
1086
+ },
1087
+ };
1088
+ }
1089
+ static { this.ɵfac = function OGridService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || OGridService)(); }; }
1090
+ static { this.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: OGridService, factory: OGridService.ɵfac }); }
1091
+ }
1092
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(OGridService, [{
1093
+ type: Injectable
1094
+ }], () => [], null); })();