@alaarab/ogrid-angular 2.2.0 → 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, processClientSideDataAsync, 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, computeVisibleColumnRange, getScrollTopForRow, 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';
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"];
@@ -76,6 +283,31 @@ var OGridService = class {
76
283
  this.ariaLabel = signal(void 0);
77
284
  this.ariaLabelledBy = signal(void 0);
78
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);
79
311
  // --- Internal state signals ---
80
312
  this.internalData = signal([]);
81
313
  this.internalLoading = signal(false);
@@ -267,6 +499,12 @@ var OGridService = class {
267
499
  rowSelection: this.rowSelection(),
268
500
  selectedRows: this.effectiveSelectedRows(),
269
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(),
270
508
  statusBar: this.statusBarConfig(),
271
509
  isLoading: this.isLoadingResolved(),
272
510
  filters: this.filters(),
@@ -287,7 +525,18 @@ var OGridService = class {
287
525
  onClearAll: this.clearAllFiltersFn,
288
526
  message: this.emptyState()?.message,
289
527
  render: this.emptyState()?.render
290
- }
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
+ } : {}
291
540
  }));
292
541
  this.pagination = computed(() => ({
293
542
  page: this.page(),
@@ -444,6 +693,21 @@ var OGridService = class {
444
693
  this.sideBarActivePanel.set(parsed.defaultPanel);
445
694
  }
446
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
+ });
447
711
  this.destroyRef.onDestroy(() => {
448
712
  this.fetchAbortController?.abort();
449
713
  this.filterAbortController?.abort();
@@ -575,6 +839,14 @@ var OGridService = class {
575
839
  if (props.columnReorder !== void 0) this.columnReorder.set(props.columnReorder);
576
840
  if (props.virtualScroll !== void 0) this.virtualScroll.set(props.virtualScroll);
577
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);
578
850
  if (props.entityLabelPlural !== void 0) this.entityLabelPlural.set(props.entityLabelPlural);
579
851
  if (props.className !== void 0) this.className.set(props.className);
580
852
  if (props.layoutMode !== void 0) this.layoutMode.set(props.layoutMode);
@@ -1112,7 +1384,7 @@ var DataGridInteractionHelper = class {
1112
1384
  const maxColIndex = visibleColumnCount - 1 + colOffset;
1113
1385
  if (items.length === 0) return;
1114
1386
  if (activeCell === null) {
1115
- 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)) {
1116
1388
  this.setActiveCell({ rowIndex: 0, columnIndex: colOffset });
1117
1389
  e.preventDefault();
1118
1390
  }
@@ -1268,6 +1540,36 @@ var DataGridInteractionHelper = class {
1268
1540
  this.setActiveCell({ rowIndex: newRowEnd, columnIndex: maxColIndex });
1269
1541
  break;
1270
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
+ }
1271
1573
  case "Enter":
1272
1574
  case "F2": {
1273
1575
  e.preventDefault();
@@ -1341,20 +1643,8 @@ var DataGridInteractionHelper = class {
1341
1643
  const range = selectionRange ?? (activeCell != null ? { startRow: activeCell.rowIndex, startCol: activeCell.columnIndex - colOffset, endRow: activeCell.rowIndex, endCol: activeCell.columnIndex - colOffset } : null);
1342
1644
  if (range == null) break;
1343
1645
  e.preventDefault();
1344
- const norm = normalizeSelectionRange(range);
1345
- for (let r = norm.startRow; r <= norm.endRow; r++) {
1346
- for (let c = norm.startCol; c <= norm.endCol; c++) {
1347
- if (r >= items.length || c >= visibleCols.length) continue;
1348
- const item = items[r];
1349
- const col = visibleCols[c];
1350
- const colEditable = col.editable === true || typeof col.editable === "function" && col.editable(item);
1351
- if (!colEditable) continue;
1352
- const oldValue = getCellValue(item, col);
1353
- const result = parseValue("", oldValue, item, col);
1354
- if (!result.valid) continue;
1355
- wrappedOnCellValueChanged({ item, columnId: col.columnId, oldValue, newValue: result.value, rowIndex: r });
1356
- }
1357
- }
1646
+ const deleteEvents = applyCellDeletion(normalizeSelectionRange(range), items, visibleCols);
1647
+ for (const evt of deleteEvents) wrappedOnCellValueChanged(evt);
1358
1648
  break;
1359
1649
  }
1360
1650
  case "F10":
@@ -2750,6 +3040,8 @@ var OGridLayoutComponent = class {
2750
3040
  this.hasPagination = false;
2751
3041
  this.sideBar = null;
2752
3042
  this.fullScreen = false;
3043
+ this.showNameBox = false;
3044
+ this.activeCellRef = null;
2753
3045
  this.isFullScreen = false;
2754
3046
  this.borderRadius = GRID_BORDER_RADIUS;
2755
3047
  this.escListener = null;
@@ -2797,6 +3089,12 @@ __decorateClass([
2797
3089
  __decorateClass([
2798
3090
  Input()
2799
3091
  ], OGridLayoutComponent.prototype, "fullScreen", 2);
3092
+ __decorateClass([
3093
+ Input()
3094
+ ], OGridLayoutComponent.prototype, "showNameBox", 2);
3095
+ __decorateClass([
3096
+ Input()
3097
+ ], OGridLayoutComponent.prototype, "activeCellRef", 2);
2800
3098
  OGridLayoutComponent = __decorateClass([
2801
3099
  Component({
2802
3100
  selector: "ogrid-layout",
@@ -2846,18 +3144,28 @@ OGridLayoutComponent = __decorateClass([
2846
3144
  color: var(--ogrid-fg, rgba(0, 0, 0, 0.87));
2847
3145
  }
2848
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
+ }
2849
3154
  `],
2850
3155
  template: `
2851
3156
  <div [class]="rootClass">
2852
3157
  <div class="ogrid-layout-container" [style.border-radius.px]="isFullScreen ? 0 : borderRadius">
2853
3158
  <!-- Toolbar strip -->
2854
- @if (hasToolbar || fullScreen) {
3159
+ @if (hasToolbar || fullScreen || showNameBox) {
2855
3160
  <div
2856
3161
  class="ogrid-layout-toolbar"
2857
3162
  [class.ogrid-layout-toolbar--has-below]="hasToolbarBelow"
2858
3163
  [class.ogrid-layout-toolbar--no-below]="!hasToolbarBelow"
2859
3164
  >
2860
3165
  <div class="ogrid-layout-toolbar-left">
3166
+ @if (showNameBox) {
3167
+ <div class="ogrid-name-box">{{ activeCellRef ?? '\u2014' }}</div>
3168
+ }
2861
3169
  <ng-content select="[toolbar]"></ng-content>
2862
3170
  </div>
2863
3171
  <div class="ogrid-layout-toolbar-right">
@@ -3787,8 +4095,8 @@ var BaseDataGridTableComponent = class {
3787
4095
  return "";
3788
4096
  }
3789
4097
  }
3790
- resolveCellStyleFn(col, item) {
3791
- return resolveCellStyle(col, item);
4098
+ resolveCellStyleFn(col, item, displayValue) {
4099
+ return resolveCellStyle(col, item, displayValue);
3792
4100
  }
3793
4101
  buildPopoverEditorProps(item, col, descriptor) {
3794
4102
  return buildPopoverEditorProps(item, col, descriptor, this.pendingEditorValue(), {
@@ -4522,7 +4830,12 @@ var BaseInlineCellEditorComponent = class {
4522
4830
  const el = this.inputEl?.nativeElement;
4523
4831
  if (el) {
4524
4832
  el.focus();
4525
- 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") {
4526
4839
  el.select();
4527
4840
  }
4528
4841
  }
@@ -4644,6 +4957,7 @@ var BaseInlineCellEditorComponent = class {
4644
4957
  dropdown.style.maxHeight = `${maxH}px`;
4645
4958
  dropdown.style.zIndex = "9999";
4646
4959
  dropdown.style.right = "auto";
4960
+ dropdown.style.textAlign = "left";
4647
4961
  if (flipUp) {
4648
4962
  dropdown.style.top = "auto";
4649
4963
  dropdown.style.bottom = `${window.innerHeight - rect.top}px`;
@@ -4728,7 +5042,7 @@ var INLINE_CELL_EDITOR_TEMPLATE = `
4728
5042
  style="width:100%;padding:0;border:none;background:transparent;color:inherit;font:inherit;font-size:13px;outline:none;min-width:0"
4729
5043
  />
4730
5044
  <div #richSelectDropdown role="listbox"
4731
- 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">
4732
5046
  @for (opt of filteredOptions(); track opt; let i = $index) {
4733
5047
  <div role="option"
4734
5048
  [attr.aria-selected]="i === highlightedIndex()"
@@ -4752,7 +5066,7 @@ var INLINE_CELL_EDITOR_TEMPLATE = `
4752
5066
  <span style="margin-left:4px;font-size:10px;opacity:0.5">&#9662;</span>
4753
5067
  </div>
4754
5068
  <div #selectDropdown role="listbox"
4755
- 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">
4756
5070
  @for (opt of selectOptions(); track opt; let i = $index) {
4757
5071
  <div role="option"
4758
5072
  [attr.aria-selected]="i === highlightedIndex()"
@@ -4892,4 +5206,4 @@ __decorateClass([
4892
5206
  ViewChild("editorContainer")
4893
5207
  ], BasePopoverCellEditorComponent.prototype, "editorContainerRef", 2);
4894
5208
 
4895
- 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 };
@@ -245,7 +245,7 @@ export declare abstract class BaseDataGridTableComponent<T = unknown> {
245
245
  };
246
246
  getCellDescriptor(item: T, col: IColumnDef<T>, rowIndex: number, colIdx: number): CellRenderDescriptor;
247
247
  resolveCellContent(col: IColumnDef<T>, item: T, displayValue: unknown): unknown;
248
- resolveCellStyleFn(col: IColumnDef<T>, item: T): Record<string, string> | undefined;
248
+ resolveCellStyleFn(col: IColumnDef<T>, item: T, displayValue?: unknown): Record<string, string> | undefined;
249
249
  buildPopoverEditorProps(item: T, col: IColumnDef<T>, descriptor: CellRenderDescriptor): unknown;
250
250
  /** Check if a specific cell is the active cell (PrimeNG inline template helper). */
251
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';
@@ -109,6 +109,31 @@ export declare class OGridService<T> {
109
109
  readonly ariaLabel: import("@angular/core").WritableSignal<string | undefined>;
110
110
  readonly ariaLabelledBy: import("@angular/core").WritableSignal<string | undefined>;
111
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;
112
137
  private readonly internalData;
113
138
  private readonly internalLoading;
114
139
  private readonly internalPage;
@@ -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>)[];
@@ -72,6 +72,24 @@ interface IOGridBaseProps<T> {
72
72
  onError?: (error: unknown) => void;
73
73
  onCellError?: (error: Error, info: unknown) => void;
74
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>;
75
93
  'aria-label'?: string;
76
94
  'aria-labelledby'?: string;
77
95
  }
@@ -123,6 +141,9 @@ export interface IOGridDataGridProps<T> {
123
141
  selectedRows?: Set<RowId>;
124
142
  onSelectionChange?: (event: IRowSelectionChangeEvent<T>) => void;
125
143
  showRowNumbers?: boolean;
144
+ showColumnLetters?: boolean;
145
+ showNameBox?: boolean;
146
+ onActiveCellChange?: (ref: string | null) => void;
126
147
  currentPage?: number;
127
148
  pageSize?: number;
128
149
  statusBar?: IStatusBarProps;
@@ -147,4 +168,22 @@ export interface IOGridDataGridProps<T> {
147
168
  'aria-labelledby'?: string;
148
169
  /** Custom keydown handler. Called before grid's built-in handling. Call event.preventDefault() to suppress grid default. */
149
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;
150
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.2.0",
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.2.0"
38
+ "@alaarab/ogrid-core": "2.3.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@angular/core": "^21.0.0",