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