@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 +338 -24
- package/dist/types/components/base-datagrid-table.component.d.ts +1 -1
- package/dist/types/components/inline-cell-editor-template.d.ts +1 -1
- package/dist/types/components/ogrid-layout.component.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/services/formula-engine.service.d.ts +133 -0
- package/dist/types/services/ogrid.service.d.ts +26 -1
- package/dist/types/types/dataGridTypes.d.ts +41 -2
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +2 -2
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,
|
|
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
|
|
1345
|
-
for (
|
|
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 === "
|
|
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">▾</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\">▾</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\">▾</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";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
38
|
+
"@alaarab/ogrid-core": "2.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@angular/core": "^21.0.0",
|