@alaarab/ogrid-angular 2.1.15 → 2.3.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/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, validateRowIds, computeNextSortState, mergeFilter, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, parseValue, UndoRedoStack, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, getCellValue, computeTabNavigation, applyFillValues, computeAggregations, getDataGridStatusBarConfig, computeVisibleRange, computeTotalHeight, getScrollTopForRow, validateVirtualScrollConfig, GRID_BORDER_RADIUS, getStatusBarParts, GRID_CONTEXT_MENU_ITEMS, formatShortcut, injectGlobalStyles, buildHeaderRows, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, measureColumnContentWidth, getPaginationViewModel, ROW_NUMBER_COLUMN_WIDTH, reorderColumnArray, findCtrlArrowTarget, measureRange } from '@alaarab/ogrid-core';
1
+ import { FormulaEngine, getCellValue, flattenColumns, getMultiSelectFilterFields, deriveFilterOptionsFromData, processClientSideData, validateColumns, processClientSideDataAsync, validateRowIds, computeNextSortState, mergeFilter, CHECKBOX_COLUMN_WIDTH, DEFAULT_MIN_COLUMN_WIDTH, CELL_PADDING, parseValue, UndoRedoStack, rangesEqual, normalizeSelectionRange, formatSelectionAsTsv, parseTsvClipboard, applyCellDeletion, getScrollTopForRow, computeTabNavigation, applyFillValues, computeAggregations, getDataGridStatusBarConfig, computeVisibleRange, computeTotalHeight, computeVisibleColumnRange, validateVirtualScrollConfig, GRID_BORDER_RADIUS, getStatusBarParts, GRID_CONTEXT_MENU_ITEMS, formatShortcut, injectGlobalStyles, partitionColumnsForVirtualization, buildHeaderRows, getHeaderFilterConfig, getCellRenderDescriptor, resolveCellDisplayContent, resolveCellStyle, buildPopoverEditorProps, measureColumnContentWidth, getPaginationViewModel, ROW_NUMBER_COLUMN_WIDTH, reorderColumnArray, findCtrlArrowTarget, measureRange } from '@alaarab/ogrid-core';
2
2
  export * from '@alaarab/ogrid-core';
3
3
  export { CELL_PADDING, CHECKBOX_COLUMN_WIDTH, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, GRID_BORDER_RADIUS, PEOPLE_SEARCH_DEBOUNCE_MS, ROW_NUMBER_COLUMN_WIDTH, SIDEBAR_TRANSITION_MS, Z_INDEX, debounce, getCellRenderDescriptor, getHeaderFilterConfig, isInSelectionRange, normalizeSelectionRange, resolveCellDisplayContent, resolveCellStyle, toUserLike } from '@alaarab/ogrid-core';
4
4
  import { Injectable, Input, Component, ChangeDetectionStrategy, ViewEncapsulation, Output, ViewChild, inject, DestroyRef, signal, computed, effect, NgZone, EventEmitter, Injector, EnvironmentInjector, createComponent } from '@angular/core';
@@ -14,6 +14,213 @@ var __decorateClass = (decorators, target, key, kind) => {
14
14
  if (kind && result) __defProp(target, key, result);
15
15
  return result;
16
16
  };
17
+ var FormulaEngineService = class {
18
+ constructor() {
19
+ this.destroyRef = inject(DestroyRef);
20
+ // --- Internal state ---
21
+ this.engine = null;
22
+ this.initialLoaded = false;
23
+ // --- Data references (updated via configure or setData) ---
24
+ this.items = [];
25
+ this.flatColumns = [];
26
+ // --- Signals ---
27
+ /** Whether formula support is currently enabled. */
28
+ this.enabled = signal(false);
29
+ /** Last recalculation result, for UI to react to formula changes. */
30
+ this.lastRecalcResult = signal(null);
31
+ /** Number of formulas currently registered. */
32
+ this.formulaCount = computed(() => {
33
+ this.lastRecalcResult();
34
+ return this.engine?.getAllFormulas().length ?? 0;
35
+ });
36
+ this.destroyRef.onDestroy(() => {
37
+ this.engine?.clear();
38
+ this.engine = null;
39
+ });
40
+ }
41
+ /**
42
+ * Configure the formula engine. Call this when the grid component initializes.
43
+ *
44
+ * Lazily creates the FormulaEngine only when `formulas: true`.
45
+ */
46
+ configure(options) {
47
+ const { formulas, initialFormulas, formulaFunctions, onFormulaRecalc, namedRanges, sheets } = options;
48
+ this.onFormulaRecalcFn = onFormulaRecalc;
49
+ if (formulas && !this.engine) {
50
+ this.engine = new FormulaEngine({
51
+ customFunctions: formulaFunctions,
52
+ namedRanges
53
+ });
54
+ if (sheets) {
55
+ for (const [name, accessor] of Object.entries(sheets)) {
56
+ this.engine.registerSheet(name, accessor);
57
+ }
58
+ }
59
+ this.enabled.set(true);
60
+ } else if (!formulas && this.engine) {
61
+ this.engine.clear();
62
+ this.engine = null;
63
+ this.enabled.set(false);
64
+ this.lastRecalcResult.set(null);
65
+ this.initialLoaded = false;
66
+ }
67
+ if (formulas && this.engine && initialFormulas && !this.initialLoaded) {
68
+ this.initialLoaded = true;
69
+ const accessor = this.createAccessor();
70
+ const result = this.engine.loadFormulas(initialFormulas, accessor);
71
+ if (result.updatedCells.length > 0) {
72
+ this.lastRecalcResult.set(result);
73
+ this.onFormulaRecalcFn?.(result);
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * Update the data references used by the accessor bridge.
79
+ * Call this whenever the grid's items or columns change.
80
+ */
81
+ setData(items, flatColumns) {
82
+ this.items = items;
83
+ this.flatColumns = flatColumns;
84
+ }
85
+ /**
86
+ * Set or clear a formula for a cell. Triggers recalculation of dependents.
87
+ */
88
+ setFormula(col, row, formula, accessor) {
89
+ if (!this.engine) return;
90
+ const acc = accessor ?? this.createAccessor();
91
+ const result = this.engine.setFormula(col, row, formula, acc);
92
+ if (result.updatedCells.length > 0) {
93
+ this.lastRecalcResult.set(result);
94
+ this.onFormulaRecalcFn?.(result);
95
+ }
96
+ }
97
+ /**
98
+ * Notify the engine that a non-formula cell's value changed.
99
+ * Triggers recalculation of any formulas that depend on this cell.
100
+ */
101
+ onCellChanged(col, row, accessor) {
102
+ if (!this.engine) return;
103
+ const acc = accessor ?? this.createAccessor();
104
+ const result = this.engine.onCellChanged(col, row, acc);
105
+ if (result.updatedCells.length > 0) {
106
+ this.lastRecalcResult.set(result);
107
+ this.onFormulaRecalcFn?.(result);
108
+ }
109
+ }
110
+ /**
111
+ * Get the formula engine's computed value for a cell coordinate.
112
+ */
113
+ getValue(col, row) {
114
+ return this.engine?.getValue(col, row);
115
+ }
116
+ /**
117
+ * Check if a cell has a formula.
118
+ */
119
+ hasFormula(col, row) {
120
+ return this.engine?.hasFormula(col, row) ?? false;
121
+ }
122
+ /**
123
+ * Get the formula string for a cell.
124
+ */
125
+ getFormula(col, row) {
126
+ return this.engine?.getFormula(col, row);
127
+ }
128
+ /**
129
+ * Trigger a full recalculation of all formulas.
130
+ */
131
+ recalcAll(accessor) {
132
+ if (!this.engine) return;
133
+ const acc = accessor ?? this.createAccessor();
134
+ const result = this.engine.recalcAll(acc);
135
+ if (result.updatedCells.length > 0) {
136
+ this.lastRecalcResult.set(result);
137
+ this.onFormulaRecalcFn?.(result);
138
+ }
139
+ }
140
+ /**
141
+ * Get all formulas for serialization.
142
+ */
143
+ getAllFormulas() {
144
+ return this.engine?.getAllFormulas() ?? [];
145
+ }
146
+ /**
147
+ * Register a custom function at runtime.
148
+ */
149
+ registerFunction(name, fn) {
150
+ this.engine?.registerFunction(name, fn);
151
+ }
152
+ /**
153
+ * Clear all formulas and cached values.
154
+ */
155
+ clear() {
156
+ this.engine?.clear();
157
+ this.lastRecalcResult.set(null);
158
+ }
159
+ /**
160
+ * Define a named range.
161
+ */
162
+ defineNamedRange(name, ref) {
163
+ this.engine?.defineNamedRange(name, ref);
164
+ }
165
+ /**
166
+ * Remove a named range.
167
+ */
168
+ removeNamedRange(name) {
169
+ this.engine?.removeNamedRange(name);
170
+ }
171
+ /**
172
+ * Register a sheet accessor for cross-sheet references.
173
+ */
174
+ registerSheet(name, accessor) {
175
+ this.engine?.registerSheet(name, accessor);
176
+ }
177
+ /**
178
+ * Unregister a sheet accessor.
179
+ */
180
+ unregisterSheet(name) {
181
+ this.engine?.unregisterSheet(name);
182
+ }
183
+ /**
184
+ * Get all cells that a cell depends on (deep, transitive).
185
+ */
186
+ getPrecedents(col, row) {
187
+ return this.engine?.getPrecedents(col, row) ?? [];
188
+ }
189
+ /**
190
+ * Get all cells that depend on a cell (deep, transitive).
191
+ */
192
+ getDependents(col, row) {
193
+ return this.engine?.getDependents(col, row) ?? [];
194
+ }
195
+ /**
196
+ * Get full audit trail for a cell.
197
+ */
198
+ getAuditTrail(col, row) {
199
+ return this.engine?.getAuditTrail(col, row) ?? null;
200
+ }
201
+ // --- Private helpers ---
202
+ /**
203
+ * Create a data accessor that bridges grid data to formula coordinates.
204
+ */
205
+ createAccessor() {
206
+ const items = this.items;
207
+ const cols = this.flatColumns;
208
+ return {
209
+ getCellValue: (col, row) => {
210
+ if (row < 0 || row >= items.length) return null;
211
+ if (col < 0 || col >= cols.length) return null;
212
+ return getCellValue(items[row], cols[col]);
213
+ },
214
+ getRowCount: () => items.length,
215
+ getColumnCount: () => cols.length
216
+ };
217
+ }
218
+ };
219
+ FormulaEngineService = __decorateClass([
220
+ Injectable()
221
+ ], FormulaEngineService);
222
+
223
+ // src/services/ogrid.service.ts
17
224
  var DEFAULT_PAGE_SIZE = 25;
18
225
  var EMPTY_LOADING_OPTIONS = {};
19
226
  var DEFAULT_PANELS = ["columns", "filters"];
@@ -75,6 +282,32 @@ var OGridService = class {
75
282
  this.virtualScroll = signal(void 0);
76
283
  this.ariaLabel = signal(void 0);
77
284
  this.ariaLabelledBy = signal(void 0);
285
+ this.workerSort = signal(false);
286
+ this.showRowNumbers = signal(false);
287
+ this.cellReferences = signal(false);
288
+ this.formulasEnabled = signal(false);
289
+ this.initialFormulas = signal(void 0);
290
+ this.onFormulaRecalc = signal(void 0);
291
+ this.formulaFunctions = signal(void 0);
292
+ this.namedRanges = signal(void 0);
293
+ this.sheets = signal(void 0);
294
+ /** Active cell reference string (e.g. 'A1') updated by DataGridTable when cellReferences is enabled. */
295
+ this.activeCellRef = signal(null);
296
+ /** Stable callback passed to DataGridTable to update activeCellRef. */
297
+ this.handleActiveCellChange = (ref) => {
298
+ this.activeCellRef.set(ref);
299
+ };
300
+ // --- Formula engine ---
301
+ this.formulaService = new FormulaEngineService();
302
+ // Stable formula method references for dataGridProps (avoid per-recompute arrow functions)
303
+ this.getFormulaValueFn = (col, row) => this.formulaService.getValue(col, row);
304
+ this.hasFormulaFn = (col, row) => this.formulaService.hasFormula(col, row);
305
+ this.getFormulaFn = (col, row) => this.formulaService.getFormula(col, row);
306
+ this.setFormulaFn = (col, row, formula) => this.formulaService.setFormula(col, row, formula ?? "");
307
+ this.onFormulaCellChangedFn = (col, row) => this.formulaService.onCellChanged(col, row);
308
+ this.getPrecedentsFn = (col, row) => this.formulaService.getPrecedents(col, row);
309
+ this.getDependentsFn = (col, row) => this.formulaService.getDependents(col, row);
310
+ this.getAuditTrailFn = (col, row) => this.formulaService.getAuditTrail(col, row);
78
311
  // --- Internal state signals ---
79
312
  this.internalData = signal([]);
80
313
  this.internalLoading = signal(false);
@@ -94,6 +327,9 @@ var OGridService = class {
94
327
  this.filterAbortController = null;
95
328
  this.refreshCounter = signal(0);
96
329
  this.firstDataRendered = signal(false);
330
+ // Worker sort async state
331
+ this.asyncClientItems = signal(null);
332
+ this.workerSortAbortId = 0;
97
333
  // Side bar state
98
334
  this.sideBarActivePanel = signal(null);
99
335
  // Filter options state
@@ -134,8 +370,9 @@ var OGridService = class {
134
370
  if (this.hasServerFilterOptions()) return this.serverFilterOptions();
135
371
  return deriveFilterOptionsFromData(this.displayData(), this.columns());
136
372
  });
373
+ /** Sync path: used when workerSort is off. */
137
374
  this.clientItemsAndTotal = computed(() => {
138
- if (!this.isClientSide()) return null;
375
+ if (!this.isClientSide() || this.workerSort()) return null;
139
376
  const rows = processClientSideData(
140
377
  this.displayData(),
141
378
  this.columns(),
@@ -148,12 +385,18 @@ var OGridService = class {
148
385
  const paged = rows.slice(start, start + this.pageSize());
149
386
  return { items: paged, totalCount: total };
150
387
  });
388
+ /** Resolved client items — sync or async depending on workerSort. */
389
+ this.resolvedClientItems = computed(() => {
390
+ const syncResult = this.clientItemsAndTotal();
391
+ if (syncResult) return syncResult;
392
+ return this.asyncClientItems();
393
+ });
151
394
  this.displayItems = computed(() => {
152
- const cit = this.clientItemsAndTotal();
395
+ const cit = this.resolvedClientItems();
153
396
  return this.isClientSide() && cit ? cit.items : this.serverItems();
154
397
  });
155
398
  this.displayTotalCount = computed(() => {
156
- const cit = this.clientItemsAndTotal();
399
+ const cit = this.resolvedClientItems();
157
400
  return this.isClientSide() && cit ? cit.totalCount : this.serverTotalCount();
158
401
  });
159
402
  this.hasActiveFilters = computed(() => {
@@ -256,6 +499,12 @@ var OGridService = class {
256
499
  rowSelection: this.rowSelection(),
257
500
  selectedRows: this.effectiveSelectedRows(),
258
501
  onSelectionChange: this.handleSelectionChangeFn,
502
+ showRowNumbers: this.showRowNumbers() || this.cellReferences(),
503
+ showColumnLetters: !!this.cellReferences(),
504
+ showNameBox: !!this.cellReferences(),
505
+ onActiveCellChange: this.cellReferences() ? this.handleActiveCellChange : void 0,
506
+ currentPage: this.page(),
507
+ pageSize: this.pageSize(),
259
508
  statusBar: this.statusBarConfig(),
260
509
  isLoading: this.isLoadingResolved(),
261
510
  filters: this.filters(),
@@ -276,7 +525,18 @@ var OGridService = class {
276
525
  onClearAll: this.clearAllFiltersFn,
277
526
  message: this.emptyState()?.message,
278
527
  render: this.emptyState()?.render
279
- }
528
+ },
529
+ formulas: this.formulasEnabled(),
530
+ ...this.formulaService.enabled() ? {
531
+ getFormulaValue: this.getFormulaValueFn,
532
+ hasFormula: this.hasFormulaFn,
533
+ getFormula: this.getFormulaFn,
534
+ setFormula: this.setFormulaFn,
535
+ onFormulaCellChanged: this.onFormulaCellChangedFn,
536
+ getPrecedents: this.getPrecedentsFn,
537
+ getDependents: this.getDependentsFn,
538
+ getAuditTrail: this.getAuditTrailFn
539
+ } : {}
280
540
  }));
281
541
  this.pagination = computed(() => ({
282
542
  page: this.page(),
@@ -323,6 +583,34 @@ var OGridService = class {
323
583
  validateColumns(cols);
324
584
  }
325
585
  });
586
+ effect((onCleanup) => {
587
+ if (!this.isClientSide() || !this.workerSort()) return;
588
+ const data = this.displayData();
589
+ const cols = this.columns();
590
+ const filters = this.filters();
591
+ const sortField = this.sort().field;
592
+ const sortDir = this.sort().direction;
593
+ const page = this.page();
594
+ const ps = this.pageSize();
595
+ const abortId = ++this.workerSortAbortId;
596
+ processClientSideDataAsync(data, cols, filters, sortField, sortDir).then((rows) => {
597
+ if (abortId !== this.workerSortAbortId) return;
598
+ const total = rows.length;
599
+ const start = (page - 1) * ps;
600
+ const paged = rows.slice(start, start + ps);
601
+ this.asyncClientItems.set({ items: paged, totalCount: total });
602
+ }).catch(() => {
603
+ if (abortId !== this.workerSortAbortId) return;
604
+ const rows = processClientSideData(data, cols, filters, sortField, sortDir);
605
+ const total = rows.length;
606
+ const start = (page - 1) * ps;
607
+ const paged = rows.slice(start, start + ps);
608
+ this.asyncClientItems.set({ items: paged, totalCount: total });
609
+ });
610
+ onCleanup(() => {
611
+ this.workerSortAbortId++;
612
+ });
613
+ });
326
614
  effect((onCleanup) => {
327
615
  const ds = this.dataSource();
328
616
  if (!this.isServerSide() || !ds) {
@@ -405,6 +693,21 @@ var OGridService = class {
405
693
  this.sideBarActivePanel.set(parsed.defaultPanel);
406
694
  }
407
695
  });
696
+ effect(() => {
697
+ this.formulaService.configure({
698
+ formulas: this.formulasEnabled(),
699
+ initialFormulas: this.initialFormulas(),
700
+ formulaFunctions: this.formulaFunctions(),
701
+ onFormulaRecalc: this.onFormulaRecalc(),
702
+ namedRanges: this.namedRanges(),
703
+ sheets: this.sheets()
704
+ });
705
+ });
706
+ effect(() => {
707
+ const items = this.displayItems();
708
+ const cols = this.columns();
709
+ this.formulaService.setData(items, cols);
710
+ });
408
711
  this.destroyRef.onDestroy(() => {
409
712
  this.fetchAbortController?.abort();
410
713
  this.filterAbortController?.abort();
@@ -486,6 +789,13 @@ var OGridService = class {
486
789
  // --- Configure from props ---
487
790
  configure(props) {
488
791
  this.columnsProp.set(props.columns);
792
+ if (Object.keys(this.pinnedOverrides()).length === 0) {
793
+ const initial = {};
794
+ for (const col of flattenColumns(props.columns)) {
795
+ if (col.pinned) initial[col.columnId] = col.pinned;
796
+ }
797
+ if (Object.keys(initial).length > 0) this.pinnedOverrides.set(initial);
798
+ }
489
799
  this.getRowId.set(props.getRowId);
490
800
  if ("data" in props && props.data !== void 0) this.data.set(props.data);
491
801
  if ("dataSource" in props && props.dataSource !== void 0) this.dataSource.set(props.dataSource);
@@ -528,6 +838,15 @@ var OGridService = class {
528
838
  if (props.columnChooser !== void 0) this.columnChooserProp.set(props.columnChooser);
529
839
  if (props.columnReorder !== void 0) this.columnReorder.set(props.columnReorder);
530
840
  if (props.virtualScroll !== void 0) this.virtualScroll.set(props.virtualScroll);
841
+ if (props.workerSort !== void 0) this.workerSort.set(props.workerSort);
842
+ if (props.showRowNumbers !== void 0) this.showRowNumbers.set(props.showRowNumbers);
843
+ if (props.cellReferences !== void 0) this.cellReferences.set(props.cellReferences);
844
+ if (props.formulas !== void 0) this.formulasEnabled.set(props.formulas);
845
+ if (props.initialFormulas !== void 0) this.initialFormulas.set(props.initialFormulas);
846
+ if (props.onFormulaRecalc) this.onFormulaRecalc.set(props.onFormulaRecalc);
847
+ if (props.formulaFunctions !== void 0) this.formulaFunctions.set(props.formulaFunctions);
848
+ if (props.namedRanges !== void 0) this.namedRanges.set(props.namedRanges);
849
+ if (props.sheets !== void 0) this.sheets.set(props.sheets);
531
850
  if (props.entityLabelPlural !== void 0) this.entityLabelPlural.set(props.entityLabelPlural);
532
851
  if (props.className !== void 0) this.className.set(props.className);
533
852
  if (props.layoutMode !== void 0) this.layoutMode.set(props.layoutMode);
@@ -1065,7 +1384,7 @@ var DataGridInteractionHelper = class {
1065
1384
  const maxColIndex = visibleColumnCount - 1 + colOffset;
1066
1385
  if (items.length === 0) return;
1067
1386
  if (activeCell === null) {
1068
- if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End"].includes(e.key)) {
1387
+ if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Tab", "Enter", "Home", "End", "PageDown", "PageUp"].includes(e.key)) {
1069
1388
  this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
1070
1389
  e.preventDefault();
1071
1390
  }
@@ -1221,6 +1540,36 @@ var DataGridInteractionHelper = class {
1221
1540
  this.setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
1222
1541
  break;
1223
1542
  }
1543
+ case "PageDown":
1544
+ case "PageUp": {
1545
+ e.preventDefault();
1546
+ let pageSize = 10;
1547
+ let rowHeight = 36;
1548
+ if (wrapperEl) {
1549
+ const firstRow = wrapperEl.querySelector("tbody tr");
1550
+ if (firstRow && firstRow.offsetHeight > 0) {
1551
+ rowHeight = firstRow.offsetHeight;
1552
+ pageSize = Math.max(1, Math.floor(wrapperEl.clientHeight / rowHeight));
1553
+ }
1554
+ }
1555
+ const pgDir = e.key === "PageDown" ? 1 : -1;
1556
+ const newRowPage = Math.max(0, Math.min(rowIndex + pgDir * pageSize, maxRowIndex));
1557
+ if (shift) {
1558
+ this.setSelectionRange(normalizeSelectionRange({
1559
+ startRow: selectionRange?.startRow ?? rowIndex,
1560
+ startCol: selectionRange?.startCol ?? dataColIndex,
1561
+ endRow: newRowPage,
1562
+ endCol: selectionRange?.endCol ?? dataColIndex
1563
+ }));
1564
+ } else {
1565
+ this.setSelectionRange({ startRow: newRowPage, startCol: dataColIndex, endRow: newRowPage, endCol: dataColIndex });
1566
+ }
1567
+ this.setActiveCell({ rowIndex: newRowPage, columnIndex });
1568
+ if (wrapperEl) {
1569
+ wrapperEl.scrollTop = getScrollTopForRow(newRowPage, rowHeight, wrapperEl.clientHeight, "center");
1570
+ }
1571
+ break;
1572
+ }
1224
1573
  case "Enter":
1225
1574
  case "F2": {
1226
1575
  e.preventDefault();
@@ -1294,20 +1643,8 @@ var DataGridInteractionHelper = class {
1294
1643
  const range = selectionRange ?? (activeCell != null ? { startRow: activeCell.rowIndex, startCol: activeCell.columnIndex - colOffset, endRow: activeCell.rowIndex, endCol: activeCell.columnIndex - colOffset } : null);
1295
1644
  if (range == null) break;
1296
1645
  e.preventDefault();
1297
- const norm = normalizeSelectionRange(range);
1298
- for (let r = norm.startRow; r <= norm.endRow; r++) {
1299
- for (let c = norm.startCol; c <= norm.endCol; c++) {
1300
- if (r >= items.length || c >= visibleCols.length) continue;
1301
- const item = items[r];
1302
- const col = visibleCols[c];
1303
- const colEditable = col.editable === true || typeof col.editable === "function" && col.editable(item);
1304
- if (!colEditable) continue;
1305
- const oldValue = getCellValue(item, col);
1306
- const result = parseValue("", oldValue, item, col);
1307
- if (!result.valid) continue;
1308
- wrappedOnCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: r });
1309
- }
1310
- }
1646
+ const deleteEvents = applyCellDeletion(normalizeSelectionRange(range), items, visibleCols);
1647
+ for (const evt of deleteEvents) wrappedOnCellValueChanged(evt);
1311
1648
  break;
1312
1649
  }
1313
1650
  case "F10":
@@ -2229,6 +2566,10 @@ var VirtualScrollService = class {
2229
2566
  this.containerHeight = signal(0);
2230
2567
  // --- Internal state ---
2231
2568
  this.scrollTop = signal(0);
2569
+ this.scrollLeft = signal(0);
2570
+ // --- Column virtualization inputs ---
2571
+ this.columnWidths = signal([]);
2572
+ this.containerWidth = signal(0);
2232
2573
  // Scrollable container reference for programmatic scrolling
2233
2574
  this.containerEl = null;
2234
2575
  // --- Derived computed signals ---
@@ -2258,6 +2599,21 @@ var VirtualScrollService = class {
2258
2599
  });
2259
2600
  /** Total scrollable height in pixels. */
2260
2601
  this.totalHeight = computed(() => computeTotalHeight(this.totalRows(), this.rowHeight()));
2602
+ // --- Column virtualization ---
2603
+ this.columnsEnabled = computed(() => this.config().columns === true);
2604
+ this.columnOverscan = computed(() => this.config().columnOverscan ?? 2);
2605
+ /** The visible column range with spacer widths, or null when column virtualization is off. */
2606
+ this.columnRange = computed(() => {
2607
+ if (!this.columnsEnabled()) return null;
2608
+ const widths = this.columnWidths();
2609
+ if (widths.length === 0) return null;
2610
+ return computeVisibleColumnRange(
2611
+ this.scrollLeft(),
2612
+ widths,
2613
+ this.containerWidth(),
2614
+ this.columnOverscan()
2615
+ );
2616
+ });
2261
2617
  this.destroyRef.onDestroy(() => {
2262
2618
  this.containerEl = null;
2263
2619
  });
@@ -2271,10 +2627,12 @@ var VirtualScrollService = class {
2271
2627
  }
2272
2628
  /**
2273
2629
  * Call this from the container's scroll event handler.
2630
+ * Tracks both vertical and horizontal scroll positions.
2274
2631
  */
2275
2632
  onScroll(event) {
2276
2633
  const target = event.target;
2277
2634
  this.scrollTop.set(target.scrollTop);
2635
+ this.scrollLeft.set(target.scrollLeft);
2278
2636
  }
2279
2637
  /**
2280
2638
  * Scroll to a specific row index.
@@ -2682,6 +3040,8 @@ var OGridLayoutComponent = class {
2682
3040
  this.hasPagination = false;
2683
3041
  this.sideBar = null;
2684
3042
  this.fullScreen = false;
3043
+ this.showNameBox = false;
3044
+ this.activeCellRef = null;
2685
3045
  this.isFullScreen = false;
2686
3046
  this.borderRadius = GRID_BORDER_RADIUS;
2687
3047
  this.escListener = null;
@@ -2729,6 +3089,12 @@ __decorateClass([
2729
3089
  __decorateClass([
2730
3090
  Input()
2731
3091
  ], OGridLayoutComponent.prototype, "fullScreen", 2);
3092
+ __decorateClass([
3093
+ Input()
3094
+ ], OGridLayoutComponent.prototype, "showNameBox", 2);
3095
+ __decorateClass([
3096
+ Input()
3097
+ ], OGridLayoutComponent.prototype, "activeCellRef", 2);
2732
3098
  OGridLayoutComponent = __decorateClass([
2733
3099
  Component({
2734
3100
  selector: "ogrid-layout",
@@ -2778,18 +3144,28 @@ OGridLayoutComponent = __decorateClass([
2778
3144
  color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));
2779
3145
  }
2780
3146
  .ogrid-fullscreen-btn:hover { background: var(--ogrid-hover-bg, rgba(0, 0, 0, 0.04)); }
3147
+ .ogrid-name-box {
3148
+ display: inline-flex; align-items: center; padding: 0 8px;
3149
+ font-family: 'Consolas', 'Courier New', monospace; font-size: 12px;
3150
+ border: 1px solid var(--ogrid-border, rgba(0, 0, 0, 0.12)); border-radius: 3px;
3151
+ height: 24px; margin-right: 8px; background: var(--ogrid-bg, #fff);
3152
+ min-width: 40px; color: var(--ogrid-fg-secondary, rgba(0, 0, 0, 0.6));
3153
+ }
2781
3154
  `],
2782
3155
  template: `
2783
3156
  <div [class]="rootClass">
2784
3157
  <div class="ogrid-layout-container" [style.border-radius.px]="isFullScreen ? 0 : borderRadius">
2785
3158
  <!-- Toolbar strip -->
2786
- @if (hasToolbar || fullScreen) {
3159
+ @if (hasToolbar || fullScreen || showNameBox) {
2787
3160
  <div
2788
3161
  class="ogrid-layout-toolbar"
2789
3162
  [class.ogrid-layout-toolbar--has-below]="hasToolbarBelow"
2790
3163
  [class.ogrid-layout-toolbar--no-below]="!hasToolbarBelow"
2791
3164
  >
2792
3165
  <div class="ogrid-layout-toolbar-left">
3166
+ @if (showNameBox) {
3167
+ <div class="ogrid-name-box">{{ activeCellRef ?? '\u2014' }}</div>
3168
+ }
2793
3169
  <ng-content select="[toolbar]"></ng-content>
2794
3170
  </div>
2795
3171
  <div class="ogrid-layout-toolbar-right">
@@ -3404,6 +3780,53 @@ var BaseDataGridTableComponent = class {
3404
3780
  if (!this.vsEnabled()) return 0;
3405
3781
  return this.vsVisibleRange().startIndex;
3406
3782
  });
3783
+ // Column virtualization
3784
+ this.vsColumnsEnabled = computed(() => this.virtualScrollService.columnsEnabled());
3785
+ this.vsColumnRange = computed(() => this.virtualScrollService.columnRange());
3786
+ /** Partitioned columns for column virtualization: pinned-left, virtualized-unpinned, pinned-right, with spacer widths. */
3787
+ this.vsColumnPartition = computed(() => {
3788
+ if (!this.vsColumnsEnabled()) return null;
3789
+ const cols = this.visibleCols();
3790
+ const range = this.vsColumnRange();
3791
+ const props = this.getProps();
3792
+ const pinnedCols = props?.pinnedColumns;
3793
+ return partitionColumnsForVirtualization(cols, range, pinnedCols);
3794
+ });
3795
+ /** Column layouts filtered by column virtualization range (or all if column virt is off). */
3796
+ this.vsColumnLayouts = computed(() => {
3797
+ const allLayouts = this.columnLayouts();
3798
+ const partition = this.vsColumnPartition();
3799
+ if (!partition) return allLayouts;
3800
+ const visibleIds = /* @__PURE__ */ new Set();
3801
+ for (const col of partition.pinnedLeft) visibleIds.add(col.columnId);
3802
+ for (const col of partition.virtualizedUnpinned) visibleIds.add(col.columnId);
3803
+ for (const col of partition.pinnedRight) visibleIds.add(col.columnId);
3804
+ return allLayouts.filter((layout) => visibleIds.has(layout.col.columnId));
3805
+ });
3806
+ /** Visible columns filtered by column virtualization range (or all if column virt is off). */
3807
+ this.vsVisibleCols = computed(() => {
3808
+ const allCols = this.visibleCols();
3809
+ const partition = this.vsColumnPartition();
3810
+ if (!partition) return allCols;
3811
+ const visibleIds = /* @__PURE__ */ new Set();
3812
+ for (const col of partition.pinnedLeft) visibleIds.add(col.columnId);
3813
+ for (const col of partition.virtualizedUnpinned) visibleIds.add(col.columnId);
3814
+ for (const col of partition.pinnedRight) visibleIds.add(col.columnId);
3815
+ return allCols.filter((col) => visibleIds.has(col.columnId));
3816
+ });
3817
+ /** Left spacer width for column virtualization (in pixels). */
3818
+ this.vsLeftSpacerWidth = computed(() => this.vsColumnPartition()?.leftSpacerWidth ?? 0);
3819
+ /** Right spacer width for column virtualization (in pixels). */
3820
+ this.vsRightSpacerWidth = computed(() => this.vsColumnPartition()?.rightSpacerWidth ?? 0);
3821
+ /** Map from columnId to its global index in visibleCols. Used to maintain correct colIdx for cell descriptors during column virtualization. */
3822
+ this.globalColIndexMap = computed(() => {
3823
+ const cols = this.visibleCols();
3824
+ const map = /* @__PURE__ */ new Map();
3825
+ for (let i = 0; i < cols.length; i++) {
3826
+ map.set(cols[i].columnId, i);
3827
+ }
3828
+ return map;
3829
+ });
3407
3830
  // Popover editing
3408
3831
  this.popoverAnchorEl = computed(() => this.editingState().popoverAnchorEl);
3409
3832
  this.pendingEditorValueForPopover = computed(() => this.editingState().pendingEditorValue);
@@ -3527,6 +3950,10 @@ var BaseDataGridTableComponent = class {
3527
3950
  this.measuredColumnWidths.set(measured);
3528
3951
  }
3529
3952
  }
3953
+ /** Get global column index for a column (correct even during column virtualization). */
3954
+ getGlobalColIndex(col) {
3955
+ return this.globalColIndexMap().get(col.columnId) ?? 0;
3956
+ }
3530
3957
  /**
3531
3958
  * Initialize base wiring effects. Must be called from subclass constructor.
3532
3959
  *
@@ -3577,16 +4004,26 @@ var BaseDataGridTableComponent = class {
3577
4004
  this.virtualScrollService.updateConfig({
3578
4005
  enabled: p.virtualScroll.enabled,
3579
4006
  rowHeight: p.virtualScroll.rowHeight,
3580
- overscan: p.virtualScroll.overscan
4007
+ overscan: p.virtualScroll.overscan,
4008
+ columns: p.virtualScroll.columns,
4009
+ columnOverscan: p.virtualScroll.columnOverscan
3581
4010
  });
3582
4011
  }
3583
4012
  }
3584
4013
  });
4014
+ effect(() => {
4015
+ const layouts = this.columnLayouts();
4016
+ const props = this.getProps();
4017
+ const pinnedCols = props?.pinnedColumns ?? {};
4018
+ const unpinnedWidths = layouts.filter((l) => !l.pinnedLeft && !l.pinnedRight && !pinnedCols[l.col.columnId]).map((l) => l.width);
4019
+ this.virtualScrollService.columnWidths.set(unpinnedWidths);
4020
+ });
3585
4021
  effect(() => {
3586
4022
  const el = this.wrapperElSignal();
3587
4023
  if (el) {
3588
4024
  this.virtualScrollService.setContainer(el);
3589
4025
  this.virtualScrollService.containerHeight.set(el.clientHeight);
4026
+ this.virtualScrollService.containerWidth.set(el.clientWidth);
3590
4027
  }
3591
4028
  });
3592
4029
  }
@@ -3658,8 +4095,8 @@ var BaseDataGridTableComponent = class {
3658
4095
  return "";
3659
4096
  }
3660
4097
  }
3661
- resolveCellStyleFn(col, item) {
3662
- return resolveCellStyle(col, item);
4098
+ resolveCellStyleFn(col, item, displayValue) {
4099
+ return resolveCellStyle(col, item, displayValue);
3663
4100
  }
3664
4101
  buildPopoverEditorProps(item, col, descriptor) {
3665
4102
  return buildPopoverEditorProps(item, col, descriptor, this.pendingEditorValue(), {
@@ -4393,7 +4830,12 @@ var BaseInlineCellEditorComponent = class {
4393
4830
  const el = this.inputEl?.nativeElement;
4394
4831
  if (el) {
4395
4832
  el.focus();
4396
- if (el instanceof HTMLInputElement && el.type === "text") {
4833
+ if (el instanceof HTMLInputElement && el.type === "date") {
4834
+ try {
4835
+ el.showPicker();
4836
+ } catch {
4837
+ }
4838
+ } else if (el instanceof HTMLInputElement && el.type === "text") {
4397
4839
  el.select();
4398
4840
  }
4399
4841
  }
@@ -4515,6 +4957,7 @@ var BaseInlineCellEditorComponent = class {
4515
4957
  dropdown.style.maxHeight = `${maxH}px`;
4516
4958
  dropdown.style.zIndex = "9999";
4517
4959
  dropdown.style.right = "auto";
4960
+ dropdown.style.textAlign = "left";
4518
4961
  if (flipUp) {
4519
4962
  dropdown.style.top = "auto";
4520
4963
  dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
@@ -4599,7 +5042,7 @@ var INLINE_CELL_EDITOR_TEMPLATE = `
4599
5042
  style="width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0"
4600
5043
  />
4601
5044
  <div #richSelectDropdown role="listbox"
4602
- style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)">
5045
+ style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2);text-align:left">
4603
5046
  @for (opt of filteredOptions(); track opt; let i = $index) {
4604
5047
  <div role="option"
4605
5048
  [attr.aria-selected]="i === highlightedIndex()"
@@ -4623,7 +5066,7 @@ var INLINE_CELL_EDITOR_TEMPLATE = `
4623
5066
  <span style="margin-left:4px;font-size:10px;opacity:0.5">&#9662;</span>
4624
5067
  </div>
4625
5068
  <div #selectDropdown role="listbox"
4626
- style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)">
5069
+ style="position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2);text-align:left">
4627
5070
  @for (opt of selectOptions(); track opt; let i = $index) {
4628
5071
  <div role="option"
4629
5072
  [attr.aria-selected]="i === highlightedIndex()"
@@ -4763,4 +5206,4 @@ __decorateClass([
4763
5206
  ViewChild("editorContainer")
4764
5207
  ], BasePopoverCellEditorComponent.prototype, "editorContainerRef", 2);
4765
5208
 
4766
- export { BaseColumnChooserComponent, BaseColumnHeaderFilterComponent, BaseDataGridTableComponent, BaseInlineCellEditorComponent, BaseOGridComponent, BasePaginationControlsComponent, BasePopoverCellEditorComponent, ColumnReorderService, DataGridEditingHelper, DataGridInteractionHelper, DataGridLayoutHelper, DataGridStateService, EmptyStateComponent, GridContextMenuComponent, INLINE_CELL_EDITOR_STYLES, INLINE_CELL_EDITOR_TEMPLATE, MarchingAntsOverlayComponent, OGRID_THEME_VARS_CSS, OGridLayoutComponent, OGridService, POPOVER_CELL_EDITOR_OVERLAY_STYLES, POPOVER_CELL_EDITOR_TEMPLATE, SideBarComponent, StatusBarComponent, VirtualScrollService, createDebouncedCallback, createDebouncedSignal, createLatestCallback };
5209
+ export { BaseColumnChooserComponent, BaseColumnHeaderFilterComponent, BaseDataGridTableComponent, BaseInlineCellEditorComponent, BaseOGridComponent, BasePaginationControlsComponent, BasePopoverCellEditorComponent, ColumnReorderService, DataGridEditingHelper, DataGridInteractionHelper, DataGridLayoutHelper, DataGridStateService, EmptyStateComponent, FormulaEngineService, GridContextMenuComponent, INLINE_CELL_EDITOR_STYLES, INLINE_CELL_EDITOR_TEMPLATE, MarchingAntsOverlayComponent, OGRID_THEME_VARS_CSS, OGridLayoutComponent, OGridService, POPOVER_CELL_EDITOR_OVERLAY_STYLES, POPOVER_CELL_EDITOR_TEMPLATE, SideBarComponent, StatusBarComponent, VirtualScrollService, createDebouncedCallback, createDebouncedSignal, createLatestCallback };
@@ -134,6 +134,34 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
134
134
  readonly vsBottomSpacerHeight: import("@angular/core").Signal<number>;
135
135
  readonly vsVisibleItems: import("@angular/core").Signal<T[]>;
136
136
  readonly vsStartIndex: import("@angular/core").Signal<number>;
137
+ readonly vsColumnsEnabled: import("@angular/core").Signal<boolean>;
138
+ readonly vsColumnRange: import("@angular/core").Signal<import("@alaarab/ogrid-core").IVisibleColumnRange | null>;
139
+ /** Partitioned columns for column virtualization: pinned-left, virtualized-unpinned, pinned-right, with spacer widths. */
140
+ readonly vsColumnPartition: import("@angular/core").Signal<{
141
+ pinnedLeft: import("@alaarab/ogrid-core").IColumnDef<T>[];
142
+ virtualizedUnpinned: import("@alaarab/ogrid-core").IColumnDef<T>[];
143
+ pinnedRight: import("@alaarab/ogrid-core").IColumnDef<T>[];
144
+ leftSpacerWidth: number;
145
+ rightSpacerWidth: number;
146
+ } | null>;
147
+ /** Column layouts filtered by column virtualization range (or all if column virt is off). */
148
+ readonly vsColumnLayouts: import("@angular/core").Signal<{
149
+ col: IColumnDef<T>;
150
+ pinnedLeft: boolean;
151
+ pinnedRight: boolean;
152
+ minWidth: number;
153
+ width: number;
154
+ }[]>;
155
+ /** Visible columns filtered by column virtualization range (or all if column virt is off). */
156
+ readonly vsVisibleCols: import("@angular/core").Signal<IColumnDef<T>[]>;
157
+ /** Left spacer width for column virtualization (in pixels). */
158
+ readonly vsLeftSpacerWidth: import("@angular/core").Signal<number>;
159
+ /** Right spacer width for column virtualization (in pixels). */
160
+ readonly vsRightSpacerWidth: import("@angular/core").Signal<number>;
161
+ /** Map from columnId to its global index in visibleCols. Used to maintain correct colIdx for cell descriptors during column virtualization. */
162
+ readonly globalColIndexMap: import("@angular/core").Signal<Map<string, number>>;
163
+ /** Get global column index for a column (correct even during column virtualization). */
164
+ getGlobalColIndex(col: IColumnDef<T>): number;
137
165
  readonly popoverAnchorEl: import("@angular/core").Signal<HTMLElement | null>;
138
166
  readonly pendingEditorValueForPopover: import("@angular/core").Signal<unknown>;
139
167
  readonly allowOverflowX: import("@angular/core").Signal<boolean>;
@@ -217,7 +245,7 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
217
245
  };
218
246
  getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor;
219
247
  resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): unknown;
220
- resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
248
+ resolveCellStyleFn(col: IColumnDef<T>, item: T, displayValue?: unknown): Record<string, string> | undefined;
221
249
  buildPopoverEditorProps(item: T, col: IColumnDef<T>, descriptor: CellRenderDescriptor): unknown;
222
250
  /** Check if a specific cell is the active cell (PrimeNG inline template helper). */
223
251
  isActiveCell(rowIndex: number, colIdx: number): boolean;
@@ -2,5 +2,5 @@
2
2
  * Shared inline cell editor template used by all Angular UI packages.
3
3
  * The template is identical across Material, PrimeNG, and Radix implementations.
4
4
  */
5
- export declare const INLINE_CELL_EDITOR_TEMPLATE = "\n @switch (editorType) {\n @case ('text') {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @case ('richSelect') {\n <div #richSelectWrapper\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\">\n <input\n #richSelectInput\n type=\"text\"\n [value]=\"searchText()\"\n (input)=\"onRichSelectSearch($any($event.target).value)\"\n (keydown)=\"onRichSelectKeyDown($event)\"\n placeholder=\"Search...\"\n style=\"width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0\"\n />\n <div #richSelectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)\">\n @for (opt of filteredOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n @if (filteredOptions().length === 0) {\n <div style=\"padding:6px 8px;color:var(--ogrid-muted, #999)\">No matches</div>\n }\n </div>\n </div>\n }\n @case ('select') {\n <div #selectWrapper tabindex=\"0\"\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\"\n (keydown)=\"onCustomSelectKeyDown($event)\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font-size:13px;color:inherit\">\n <span>{{ getDisplayText(value) }}</span>\n <span style=\"margin-left:4px;font-size:10px;opacity:0.5\">&#9662;</span>\n </div>\n <div #selectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2)\">\n @for (opt of selectOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n </div>\n </div>\n }\n @case ('checkbox') {\n <div style=\"display:flex;align-items:center;justify-content:center;width:100%;height:100%\">\n <input\n type=\"checkbox\"\n [checked]=\"!!localValue()\"\n (change)=\"commitValue($any($event.target).checked)\"\n (keydown)=\"onCheckboxKeyDown($event)\"\n />\n </div>\n }\n @case ('date') {\n <input\n #inputEl\n type=\"date\"\n [value]=\"localValue()\"\n (change)=\"commitValue($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @default {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n }\n";
5
+ export declare const INLINE_CELL_EDITOR_TEMPLATE = "\n @switch (editorType) {\n @case ('text') {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @case ('richSelect') {\n <div #richSelectWrapper\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\">\n <input\n #richSelectInput\n type=\"text\"\n [value]=\"searchText()\"\n (input)=\"onRichSelectSearch($any($event.target).value)\"\n (keydown)=\"onRichSelectKeyDown($event)\"\n placeholder=\"Search...\"\n style=\"width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0\"\n />\n <div #richSelectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2);text-align:left\">\n @for (opt of filteredOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n @if (filteredOptions().length === 0) {\n <div style=\"padding:6px 8px;color:var(--ogrid-muted, #999)\">No matches</div>\n }\n </div>\n </div>\n }\n @case ('select') {\n <div #selectWrapper tabindex=\"0\"\n style=\"width:100%;height:100%;display:flex;align-items:center;padding:6px 10px;box-sizing:border-box;min-width:0;position:relative\"\n (keydown)=\"onCustomSelectKeyDown($event)\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer;font-size:13px;color:inherit\">\n <span>{{ getDisplayText(value) }}</span>\n <span style=\"margin-left:4px;font-size:10px;opacity:0.5\">&#9662;</span>\n </div>\n <div #selectDropdown role=\"listbox\"\n style=\"position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:var(--ogrid-bg, #fff);border:1px solid var(--ogrid-border, rgba(0,0,0,0.12));z-index:10;box-shadow:0 4px 16px rgba(0,0,0,0.2);text-align:left\">\n @for (opt of selectOptions(); track opt; let i = $index) {\n <div role=\"option\"\n [attr.aria-selected]=\"i === highlightedIndex()\"\n (click)=\"commitValue(opt)\"\n [style]=\"i === highlightedIndex() ? 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424);background:var(--ogrid-bg-hover, #e8f0fe)' : 'padding:6px 8px;cursor:pointer;color:var(--ogrid-fg, #242424)'\">\n {{ getDisplayText(opt) }}\n </div>\n }\n </div>\n </div>\n }\n @case ('checkbox') {\n <div style=\"display:flex;align-items:center;justify-content:center;width:100%;height:100%\">\n <input\n type=\"checkbox\"\n [checked]=\"!!localValue()\"\n (change)=\"commitValue($any($event.target).checked)\"\n (keydown)=\"onCheckboxKeyDown($event)\"\n />\n </div>\n }\n @case ('date') {\n <input\n #inputEl\n type=\"date\"\n [value]=\"localValue()\"\n (change)=\"commitValue($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n @default {\n <input\n #inputEl\n type=\"text\"\n [value]=\"localValue()\"\n (input)=\"localValue.set($any($event.target).value)\"\n (keydown)=\"onTextKeyDown($event)\"\n (blur)=\"onTextBlur()\"\n [style]=\"getInputStyle()\"\n />\n }\n }\n";
6
6
  export declare const INLINE_CELL_EDITOR_STYLES = "\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n";
@@ -6,6 +6,8 @@ export declare class OGridLayoutComponent {
6
6
  hasPagination: boolean;
7
7
  sideBar: SideBarProps | null;
8
8
  fullScreen: boolean;
9
+ showNameBox: boolean;
10
+ activeCellRef: string | null;
9
11
  isFullScreen: boolean;
10
12
  readonly borderRadius = 6;
11
13
  private escListener;
@@ -13,6 +13,8 @@ export { DataGridEditingHelper } from './services/datagrid-editing.service';
13
13
  export { DataGridInteractionHelper } from './services/datagrid-interaction.service';
14
14
  export { ColumnReorderService } from './services/column-reorder.service';
15
15
  export { VirtualScrollService } from './services/virtual-scroll.service';
16
+ export { FormulaEngineService } from './services/formula-engine.service';
17
+ export type { FormulaEngineConfig } from './services/formula-engine.service';
16
18
  export { OGridLayoutComponent } from './components/ogrid-layout.component';
17
19
  export { StatusBarComponent } from './components/status-bar.component';
18
20
  export { GridContextMenuComponent } from './components/grid-context-menu.component';
@@ -0,0 +1,133 @@
1
+ /**
2
+ * FormulaEngineService — Angular service for integrating the formula engine with the grid.
3
+ *
4
+ * Lazily creates a FormulaEngine instance when configured with `formulas: true`.
5
+ * Provides an accessor bridge between grid data and formula coordinates.
6
+ * Uses Angular signals for reactive state.
7
+ *
8
+ * Port of React's useFormulaEngine hook.
9
+ */
10
+ import type { IGridDataAccessor, IFormulaFunction, IRecalcResult, IColumnDef, IAuditEntry, IAuditTrail } from '@alaarab/ogrid-core';
11
+ export interface FormulaEngineConfig {
12
+ /** Enable formula support. */
13
+ formulas?: boolean;
14
+ /** Initial formulas to load on first configure. */
15
+ initialFormulas?: Array<{
16
+ col: number;
17
+ row: number;
18
+ formula: string;
19
+ }>;
20
+ /** Custom formula functions to register. */
21
+ formulaFunctions?: Record<string, IFormulaFunction>;
22
+ /** Called when recalculation produces cascading updates. */
23
+ onFormulaRecalc?: (result: IRecalcResult) => void;
24
+ /** Named ranges: name → cell/range reference string. */
25
+ namedRanges?: Record<string, string>;
26
+ /** Sheet accessors for cross-sheet references. */
27
+ sheets?: Record<string, IGridDataAccessor>;
28
+ }
29
+ /**
30
+ * Per-component injectable service that wraps FormulaEngine from @alaarab/ogrid-core.
31
+ *
32
+ * Not providedIn: 'root' — provide it per component so each grid instance
33
+ * gets its own formula engine.
34
+ */
35
+ export declare class FormulaEngineService<T = unknown> {
36
+ private destroyRef;
37
+ private engine;
38
+ private initialLoaded;
39
+ private onFormulaRecalcFn;
40
+ private items;
41
+ private flatColumns;
42
+ /** Whether formula support is currently enabled. */
43
+ readonly enabled: import("@angular/core").WritableSignal<boolean>;
44
+ /** Last recalculation result, for UI to react to formula changes. */
45
+ readonly lastRecalcResult: import("@angular/core").WritableSignal<IRecalcResult | null>;
46
+ /** Number of formulas currently registered. */
47
+ readonly formulaCount: import("@angular/core").Signal<number>;
48
+ constructor();
49
+ /**
50
+ * Configure the formula engine. Call this when the grid component initializes.
51
+ *
52
+ * Lazily creates the FormulaEngine only when `formulas: true`.
53
+ */
54
+ configure(options: FormulaEngineConfig): void;
55
+ /**
56
+ * Update the data references used by the accessor bridge.
57
+ * Call this whenever the grid's items or columns change.
58
+ */
59
+ setData(items: T[], flatColumns: IColumnDef<T>[]): void;
60
+ /**
61
+ * Set or clear a formula for a cell. Triggers recalculation of dependents.
62
+ */
63
+ setFormula(col: number, row: number, formula: string | null, accessor?: IGridDataAccessor): void;
64
+ /**
65
+ * Notify the engine that a non-formula cell's value changed.
66
+ * Triggers recalculation of any formulas that depend on this cell.
67
+ */
68
+ onCellChanged(col: number, row: number, accessor?: IGridDataAccessor): void;
69
+ /**
70
+ * Get the formula engine's computed value for a cell coordinate.
71
+ */
72
+ getValue(col: number, row: number): unknown | undefined;
73
+ /**
74
+ * Check if a cell has a formula.
75
+ */
76
+ hasFormula(col: number, row: number): boolean;
77
+ /**
78
+ * Get the formula string for a cell.
79
+ */
80
+ getFormula(col: number, row: number): string | undefined;
81
+ /**
82
+ * Trigger a full recalculation of all formulas.
83
+ */
84
+ recalcAll(accessor?: IGridDataAccessor): void;
85
+ /**
86
+ * Get all formulas for serialization.
87
+ */
88
+ getAllFormulas(): Array<{
89
+ col: number;
90
+ row: number;
91
+ formula: string;
92
+ }>;
93
+ /**
94
+ * Register a custom function at runtime.
95
+ */
96
+ registerFunction(name: string, fn: IFormulaFunction): void;
97
+ /**
98
+ * Clear all formulas and cached values.
99
+ */
100
+ clear(): void;
101
+ /**
102
+ * Define a named range.
103
+ */
104
+ defineNamedRange(name: string, ref: string): void;
105
+ /**
106
+ * Remove a named range.
107
+ */
108
+ removeNamedRange(name: string): void;
109
+ /**
110
+ * Register a sheet accessor for cross-sheet references.
111
+ */
112
+ registerSheet(name: string, accessor: IGridDataAccessor): void;
113
+ /**
114
+ * Unregister a sheet accessor.
115
+ */
116
+ unregisterSheet(name: string): void;
117
+ /**
118
+ * Get all cells that a cell depends on (deep, transitive).
119
+ */
120
+ getPrecedents(col: number, row: number): IAuditEntry[];
121
+ /**
122
+ * Get all cells that depend on a cell (deep, transitive).
123
+ */
124
+ getDependents(col: number, row: number): IAuditEntry[];
125
+ /**
126
+ * Get full audit trail for a cell.
127
+ */
128
+ getAuditTrail(col: number, row: number): IAuditTrail | null;
129
+ /**
130
+ * Create a data accessor that bridges grid data to formula coordinates.
131
+ */
132
+ private createAccessor;
133
+ }
@@ -1,4 +1,4 @@
1
- import type { RowId, IOGridApi, IFilters, FilterValue, IRowSelectionChangeEvent, IStatusBarProps, IColumnDefinition, IDataSource, ISideBarDef, IVirtualScrollConfig, SideBarPanelId } from '../types';
1
+ import type { RowId, IOGridApi, IFilters, FilterValue, IRowSelectionChangeEvent, IStatusBarProps, IColumnDefinition, IDataSource, ISideBarDef, IVirtualScrollConfig, SideBarPanelId, IFormulaFunction, IRecalcResult, IGridDataAccessor } from '../types';
2
2
  import type { IOGridProps, IOGridDataGridProps } from '../types';
3
3
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from '../types';
4
4
  import type { SideBarProps } from '../components/sidebar.component';
@@ -108,6 +108,32 @@ export declare class OGridService<T> {
108
108
  readonly virtualScroll: import("@angular/core").WritableSignal<IVirtualScrollConfig | undefined>;
109
109
  readonly ariaLabel: import("@angular/core").WritableSignal<string | undefined>;
110
110
  readonly ariaLabelledBy: import("@angular/core").WritableSignal<string | undefined>;
111
+ readonly workerSort: import("@angular/core").WritableSignal<boolean>;
112
+ readonly showRowNumbers: import("@angular/core").WritableSignal<boolean>;
113
+ readonly cellReferences: import("@angular/core").WritableSignal<boolean>;
114
+ readonly formulasEnabled: import("@angular/core").WritableSignal<boolean>;
115
+ readonly initialFormulas: import("@angular/core").WritableSignal<{
116
+ col: number;
117
+ row: number;
118
+ formula: string;
119
+ }[] | undefined>;
120
+ readonly onFormulaRecalc: import("@angular/core").WritableSignal<((result: IRecalcResult) => void) | undefined>;
121
+ readonly formulaFunctions: import("@angular/core").WritableSignal<Record<string, IFormulaFunction> | undefined>;
122
+ readonly namedRanges: import("@angular/core").WritableSignal<Record<string, string> | undefined>;
123
+ readonly sheets: import("@angular/core").WritableSignal<Record<string, IGridDataAccessor> | undefined>;
124
+ /** Active cell reference string (e.g. 'A1') updated by DataGridTable when cellReferences is enabled. */
125
+ readonly activeCellRef: import("@angular/core").WritableSignal<string | null>;
126
+ /** Stable callback passed to DataGridTable to update activeCellRef. */
127
+ private readonly handleActiveCellChange;
128
+ private readonly formulaService;
129
+ private readonly getFormulaValueFn;
130
+ private readonly hasFormulaFn;
131
+ private readonly getFormulaFn;
132
+ private readonly setFormulaFn;
133
+ private readonly onFormulaCellChangedFn;
134
+ private readonly getPrecedentsFn;
135
+ private readonly getDependentsFn;
136
+ private readonly getAuditTrailFn;
111
137
  private readonly internalData;
112
138
  private readonly internalLoading;
113
139
  private readonly internalPage;
@@ -125,6 +151,8 @@ export declare class OGridService<T> {
125
151
  private filterAbortController;
126
152
  private readonly refreshCounter;
127
153
  private readonly firstDataRendered;
154
+ private readonly asyncClientItems;
155
+ private workerSortAbortId;
128
156
  private readonly sideBarActivePanel;
129
157
  private readonly serverFilterOptions;
130
158
  private readonly loadingFilterOptions;
@@ -147,10 +175,16 @@ export declare class OGridService<T> {
147
175
  readonly multiSelectFilterFields: import("@angular/core").Signal<string[]>;
148
176
  readonly hasServerFilterOptions: import("@angular/core").Signal<boolean>;
149
177
  readonly clientFilterOptions: import("@angular/core").Signal<Record<string, string[]>>;
178
+ /** Sync path: used when workerSort is off. */
150
179
  readonly clientItemsAndTotal: import("@angular/core").Signal<{
151
180
  items: T[];
152
181
  totalCount: number;
153
182
  } | null>;
183
+ /** Resolved client items — sync or async depending on workerSort. */
184
+ readonly resolvedClientItems: import("@angular/core").Signal<{
185
+ items: T[];
186
+ totalCount: number;
187
+ } | null>;
154
188
  readonly displayItems: import("@angular/core").Signal<T[]>;
155
189
  readonly displayTotalCount: import("@angular/core").Signal<number>;
156
190
  readonly hasActiveFilters: import("@angular/core").Signal<boolean>;
@@ -1,4 +1,4 @@
1
- import type { IVisibleRange, IVirtualScrollConfig } from '@alaarab/ogrid-core';
1
+ import type { IVisibleRange, IVisibleColumnRange, IVirtualScrollConfig } from '@alaarab/ogrid-core';
2
2
  /**
3
3
  * Manages virtual scrolling state using Angular signals.
4
4
  * Port of React's useVirtualScroll hook.
@@ -13,6 +13,9 @@ export declare class VirtualScrollService {
13
13
  readonly config: import("@angular/core").WritableSignal<IVirtualScrollConfig>;
14
14
  readonly containerHeight: import("@angular/core").WritableSignal<number>;
15
15
  readonly scrollTop: import("@angular/core").WritableSignal<number>;
16
+ readonly scrollLeft: import("@angular/core").WritableSignal<number>;
17
+ readonly columnWidths: import("@angular/core").WritableSignal<number[]>;
18
+ readonly containerWidth: import("@angular/core").WritableSignal<number>;
16
19
  private containerEl;
17
20
  readonly rowHeight: import("@angular/core").Signal<number>;
18
21
  readonly overscan: import("@angular/core").Signal<number>;
@@ -24,6 +27,10 @@ export declare class VirtualScrollService {
24
27
  readonly visibleRange: import("@angular/core").Signal<IVisibleRange>;
25
28
  /** Total scrollable height in pixels. */
26
29
  readonly totalHeight: import("@angular/core").Signal<number>;
30
+ readonly columnsEnabled: import("@angular/core").Signal<boolean>;
31
+ readonly columnOverscan: import("@angular/core").Signal<number>;
32
+ /** The visible column range with spacer widths, or null when column virtualization is off. */
33
+ readonly columnRange: import("@angular/core").Signal<IVisibleColumnRange | null>;
27
34
  constructor();
28
35
  /**
29
36
  * Set the scrollable container element.
@@ -32,6 +39,7 @@ export declare class VirtualScrollService {
32
39
  setContainer(el: HTMLElement | null): void;
33
40
  /**
34
41
  * Call this from the container's scroll event handler.
42
+ * Tracks both vertical and horizontal scroll positions.
35
43
  */
36
44
  onScroll(event: Event): void;
37
45
  /**
@@ -1,8 +1,8 @@
1
1
  import type { TemplateRef } from '@angular/core';
2
2
  import type { IColumnDef, IColumnGroupDef, ICellValueChangedEvent } from './columnTypes';
3
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IOGridApi, } from '@alaarab/ogrid-core';
3
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IOGridApi, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, } from '@alaarab/ogrid-core';
4
4
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from '@alaarab/ogrid-core';
5
- import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig } from '@alaarab/ogrid-core';
5
+ import type { RowId, UserLike, IFilters, FilterValue, RowSelectionMode, IRowSelectionChangeEvent, IStatusBarProps, IDataSource, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail } from '@alaarab/ogrid-core';
6
6
  /** Base props shared by both client-side and server-side OGrid modes. */
7
7
  interface IOGridBaseProps<T> {
8
8
  columns: (IColumnDef<T> | IColumnGroupDef<T>)[];
@@ -63,6 +63,8 @@ interface IOGridBaseProps<T> {
63
63
  sideBar?: boolean | ISideBarDef;
64
64
  columnReorder?: boolean;
65
65
  virtualScroll?: IVirtualScrollConfig;
66
+ /** Offload sort/filter to a Web Worker for large datasets. Falls back to sync when sort column has a custom compare. */
67
+ workerSort?: boolean;
66
68
  /** Fixed row height in pixels. Overrides default row height (36px). */
67
69
  rowHeight?: number;
68
70
  pageSizeOptions?: number[];
@@ -70,6 +72,24 @@ interface IOGridBaseProps<T> {
70
72
  onError?: (error: unknown) => void;
71
73
  onCellError?: (error: Error, info: unknown) => void;
72
74
  showRowNumbers?: boolean;
75
+ /** Enable Excel-style cell references: column letter headers, row numbers, and name box. Implies showRowNumbers. */
76
+ cellReferences?: boolean;
77
+ /** Enable Excel-like formula support. When true, cells starting with '=' are treated as formulas. Default: false. */
78
+ formulas?: boolean;
79
+ /** Initial formulas to load when the formula engine initializes. */
80
+ initialFormulas?: Array<{
81
+ col: number;
82
+ row: number;
83
+ formula: string;
84
+ }>;
85
+ /** Called when formula recalculation produces updated cell values (e.g. cascade from an edited cell). */
86
+ onFormulaRecalc?: (result: IRecalcResult) => void;
87
+ /** Custom formula functions to register with the formula engine (e.g. { MYFUNC: { minArgs: 1, maxArgs: 1, evaluate: ... } }). */
88
+ formulaFunctions?: Record<string, IFormulaFunction>;
89
+ /** Named ranges for the formula engine: name → cell/range ref string (e.g. { Revenue: 'A1:A10' }). */
90
+ namedRanges?: Record<string, string>;
91
+ /** Sheet accessors for cross-sheet formula references (e.g. { Sheet2: accessor }). */
92
+ sheets?: Record<string, IGridDataAccessor>;
73
93
  'aria-label'?: string;
74
94
  'aria-labelledby'?: string;
75
95
  }
@@ -121,6 +141,9 @@ export interface IOGridDataGridProps<T> {
121
141
  selectedRows?: Set<RowId>;
122
142
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
123
143
  showRowNumbers?: boolean;
144
+ showColumnLetters?: boolean;
145
+ showNameBox?: boolean;
146
+ onActiveCellChange?: (ref: string | null) => void;
124
147
  currentPage?: number;
125
148
  pageSize?: number;
126
149
  statusBar?: IStatusBarProps;
@@ -145,4 +168,22 @@ export interface IOGridDataGridProps<T> {
145
168
  'aria-labelledby'?: string;
146
169
  /** Custom keydown handler. Called before grid's built-in handling. Call event.preventDefault() to suppress grid default. */
147
170
  onKeyDown?: (event: KeyboardEvent) => void;
171
+ /** Enable formula support. When true, cell values starting with '=' are treated as formulas. */
172
+ formulas?: boolean;
173
+ /** Get the formula engine's computed value for a cell, or undefined if no formula. */
174
+ getFormulaValue?: (col: number, row: number) => unknown;
175
+ /** Check if a cell has a formula. */
176
+ hasFormula?: (col: number, row: number) => boolean;
177
+ /** Get the formula string for a cell. */
178
+ getFormula?: (col: number, row: number) => string | undefined;
179
+ /** Set a formula for a cell (called from edit commit when value starts with '='). */
180
+ setFormula?: (col: number, row: number, formula: string | null) => void;
181
+ /** Notify the formula engine that a non-formula cell changed. */
182
+ onFormulaCellChanged?: (col: number, row: number) => void;
183
+ /** Get all cells that a cell depends on (deep, transitive). */
184
+ getPrecedents?: (col: number, row: number) => IAuditEntry[];
185
+ /** Get all cells that depend on a cell (deep, transitive). */
186
+ getDependents?: (col: number, row: number) => IAuditEntry[];
187
+ /** Get full audit trail for a cell. */
188
+ getAuditTrail?: (col: number, row: number) => IAuditTrail | null;
148
189
  }
@@ -1,3 +1,3 @@
1
1
  export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnGroupDef, IColumnDefinition, ICellValueChangedEvent, ICellEditorProps, CellEditorParams, IValueParserParams, IDateFilterValue, HeaderCell, HeaderRow, } from './columnTypes';
2
- export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, } from './dataGridTypes';
2
+ export type { RowId, UserLike, UserLikeInput, FilterValue, IFilters, IFetchParams, IPageResult, IDataSource, IGridColumnState, IOGridApi, IOGridProps, IOGridClientProps, IOGridServerProps, IOGridDataGridProps, RowSelectionMode, IRowSelectionChangeEvent, StatusBarPanel, IStatusBarProps, IActiveCell, ISelectionRange, SideBarPanelId, ISideBarDef, IVirtualScrollConfig, IFormulaFunction, IRecalcResult, IGridDataAccessor, IAuditEntry, IAuditTrail, } from './dataGridTypes';
3
3
  export { toUserLike, isInSelectionRange, normalizeSelectionRange } from './dataGridTypes';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alaarab/ogrid-angular",
3
- "version": "2.1.15",
3
+ "version": "2.3.0",
4
4
  "description": "OGrid Angular – Angular services, signals, and headless components for OGrid data grids.",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -35,7 +35,7 @@
35
35
  "node": ">=18"
36
36
  },
37
37
  "dependencies": {
38
- "@alaarab/ogrid-core": "2.1.15"
38
+ "@alaarab/ogrid-core": "2.3.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@angular/core": "^21.0.0",