@ornery/ui-grid-vanilla 1.0.5 → 1.0.7
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/README.md +75 -11
- package/demo/main.ts +1 -2
- package/dist/browser/ui-grid-element.js +9586 -0
- package/dist/index.d.mts +43 -2
- package/dist/index.d.ts +43 -2
- package/dist/index.js +281 -100
- package/dist/index.mjs +283 -102
- package/package.json +14 -6
- package/src/browser.test.ts +9 -0
- package/src/browser.ts +8 -0
- package/src/cell-state-management.test.ts +419 -0
- package/src/events.ts +77 -26
- package/src/framework-slots.ts +18 -0
- package/src/grid-controller.ts +166 -98
- package/src/index.integration.test.ts +51 -8
- package/src/index.test.ts +0 -4
- package/src/index.ts +6 -9
- package/src/keyboard-navigation.test.ts +1 -7
- package/src/patch.ts +118 -1
- package/src/row-edit.test.ts +1 -1
- package/src/ui-grid-standalone.element.ts +79 -1
- package/tsup.config.ts +22 -7
- package/vitest.config.ts +30 -0
package/src/grid-controller.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SORT_DIRECTIONS,
|
|
3
|
+
activeGridEngineBackend,
|
|
3
4
|
beginGridCellEditCommand,
|
|
4
5
|
buildGridRows,
|
|
5
6
|
buildGridCellContext,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
defaultGridEngine,
|
|
16
17
|
expandAllGridRowsCommand,
|
|
17
18
|
expandAllGridTreeRowsCommand,
|
|
19
|
+
enableUiGridWasmEngine,
|
|
18
20
|
findGridRowById as coreFindGridRowById,
|
|
19
21
|
formatGridCellDisplayValue,
|
|
20
22
|
getCellValue,
|
|
@@ -121,6 +123,7 @@ import {
|
|
|
121
123
|
type DisplayItem,
|
|
122
124
|
type GridCellPosition,
|
|
123
125
|
type GridColumnDef,
|
|
126
|
+
type GridBenchmarkResult,
|
|
124
127
|
type GridRowColumn,
|
|
125
128
|
type GridLabels,
|
|
126
129
|
type GridOptions,
|
|
@@ -215,7 +218,10 @@ const DEFAULT_COL_MIN_WIDTH = 176;
|
|
|
215
218
|
* 3. Flex columns (*, 1fr, minmax): divide remaining space equally.
|
|
216
219
|
* 4. Leftover/excess pixels distributed 1px at a time for pixel-perfect fit.
|
|
217
220
|
*/
|
|
218
|
-
function buildGridTemplateColumns(
|
|
221
|
+
function buildGridTemplateColumns(
|
|
222
|
+
columns: readonly GridColumnDef[],
|
|
223
|
+
viewportWidth?: number,
|
|
224
|
+
): string {
|
|
219
225
|
if (!viewportWidth || viewportWidth <= 0) {
|
|
220
226
|
return columns.map((column) => gridColumnWidth(column)).join(' ');
|
|
221
227
|
}
|
|
@@ -339,6 +345,8 @@ export class VanillaGridController {
|
|
|
339
345
|
private labels: GridLabels;
|
|
340
346
|
private visibleColumns: GridColumnDef[] = [];
|
|
341
347
|
private apiRegistered = false;
|
|
348
|
+
private disposed = false;
|
|
349
|
+
private wasmEngineInitRequested = false;
|
|
342
350
|
|
|
343
351
|
private readonly subscribers = new Set<GridControllerSubscriber>();
|
|
344
352
|
|
|
@@ -355,7 +363,9 @@ export class VanillaGridController {
|
|
|
355
363
|
this.groupByColumns = options.grouping?.groupBy ? [...options.grouping.groupBy] : [];
|
|
356
364
|
this.lastOptionsGroupBy = options.grouping?.groupBy ? [...options.grouping.groupBy] : undefined;
|
|
357
365
|
this.pinnedColumns = buildInitialPinnedState(options.columnDefs);
|
|
358
|
-
const declaresPin = options.columnDefs.some(
|
|
366
|
+
const declaresPin = options.columnDefs.some(
|
|
367
|
+
(col) => col.pinnedLeft === true || col.pinnedRight === true,
|
|
368
|
+
);
|
|
359
369
|
this.lastOptionsPinnedState = declaresPin ? { ...this.pinnedColumns } : undefined;
|
|
360
370
|
this.lastOptionsInfiniteScrollUp = options.infiniteScrollUp;
|
|
361
371
|
this.lastOptionsInfiniteScrollDown = options.infiniteScrollDown;
|
|
@@ -396,12 +406,14 @@ export class VanillaGridController {
|
|
|
396
406
|
exportCsv: (rowType, colType) => this.exportCsv(rowType, colType),
|
|
397
407
|
buildCsv: (rowType, colType) => this.buildCsv(rowType, colType),
|
|
398
408
|
pdfExport: (rowType, colType) => this.exportPdf(rowType, colType),
|
|
399
|
-
buildPdfDocDefinition: (rowType, colType) =>
|
|
400
|
-
this.buildPdfDocDefinition(rowType, colType),
|
|
409
|
+
buildPdfDocDefinition: (rowType, colType) => this.buildPdfDocDefinition(rowType, colType),
|
|
401
410
|
excelExport: (rowType, colType) => this.exportExcel(rowType, colType),
|
|
402
411
|
buildExcelSheetData: (rowType, colType) => this.buildExcelSheetData(rowType, colType),
|
|
403
412
|
getExporterMenuItems: () => this.buildExporterMenuItems(),
|
|
404
|
-
getExporterOptions: () => ({
|
|
413
|
+
getExporterOptions: () => ({
|
|
414
|
+
...this.exporterOverrides,
|
|
415
|
+
...resolveGridExporterOptions(this.options),
|
|
416
|
+
}),
|
|
405
417
|
setExporterOptions: (overrides) => {
|
|
406
418
|
this.exporterOverrides = { ...this.exporterOverrides, ...overrides };
|
|
407
419
|
},
|
|
@@ -491,8 +503,7 @@ export class VanillaGridController {
|
|
|
491
503
|
infiniteScrollSetDirections: (scrollUp, scrollDown) =>
|
|
492
504
|
this.infiniteScrollSetDirections(scrollUp, scrollDown),
|
|
493
505
|
// Row-edit bindings — port of ui.grid.rowEdit public API.
|
|
494
|
-
rowEditSetSavePromise: (rowEntity, promise) =>
|
|
495
|
-
this.rowEditSetSavePromise(rowEntity, promise),
|
|
506
|
+
rowEditSetSavePromise: (rowEntity, promise) => this.rowEditSetSavePromise(rowEntity, promise),
|
|
496
507
|
rowEditGetDirtyRows: () => this.rowEditGetDirtyRows(),
|
|
497
508
|
rowEditGetErrorRows: () => this.rowEditGetErrorRows(),
|
|
498
509
|
rowEditFlushDirtyRows: () => this.rowEditFlushDirtyRows(),
|
|
@@ -584,6 +595,7 @@ export class VanillaGridController {
|
|
|
584
595
|
* subscriptions). */
|
|
585
596
|
private disposeLanguageListener: (() => void) | null = null;
|
|
586
597
|
dispose(): void {
|
|
598
|
+
this.disposed = true;
|
|
587
599
|
this.disposeLanguageListener?.();
|
|
588
600
|
this.disposeLanguageListener = null;
|
|
589
601
|
this.subscribers.clear();
|
|
@@ -621,7 +633,8 @@ export class VanillaGridController {
|
|
|
621
633
|
if (options.grouping?.groupBy !== undefined) {
|
|
622
634
|
const incoming = options.grouping.groupBy;
|
|
623
635
|
const prev = this.lastOptionsGroupBy;
|
|
624
|
-
const changed =
|
|
636
|
+
const changed =
|
|
637
|
+
!prev || prev.length !== incoming.length || prev.some((col, i) => col !== incoming[i]);
|
|
625
638
|
if (changed) {
|
|
626
639
|
this.groupByColumns = [...incoming];
|
|
627
640
|
this.lastOptionsGroupBy = [...incoming];
|
|
@@ -651,8 +664,12 @@ export class VanillaGridController {
|
|
|
651
664
|
// Re-seed infinite-scroll directions only when the values structurally
|
|
652
665
|
// changed from the last options pass.
|
|
653
666
|
if (options.infiniteScrollUp !== undefined || options.infiniteScrollDown !== undefined) {
|
|
654
|
-
const upChanged =
|
|
655
|
-
|
|
667
|
+
const upChanged =
|
|
668
|
+
options.infiniteScrollUp !== undefined &&
|
|
669
|
+
options.infiniteScrollUp !== this.lastOptionsInfiniteScrollUp;
|
|
670
|
+
const downChanged =
|
|
671
|
+
options.infiniteScrollDown !== undefined &&
|
|
672
|
+
options.infiniteScrollDown !== this.lastOptionsInfiniteScrollDown;
|
|
656
673
|
if (upChanged || downChanged) {
|
|
657
674
|
this.infiniteScrollState = setInfiniteScrollDirectionsState(
|
|
658
675
|
this.infiniteScrollState,
|
|
@@ -1039,10 +1056,7 @@ export class VanillaGridController {
|
|
|
1039
1056
|
// re-renders with the updated `isSelected` / `isFocused` flags.
|
|
1040
1057
|
|
|
1041
1058
|
/** Internal: run a core selection mutation, raise events, trigger a render. */
|
|
1042
|
-
private applySelectionChange(
|
|
1043
|
-
apply: () => { changed: GridRow[] },
|
|
1044
|
-
evt?: Event | null,
|
|
1045
|
-
): void {
|
|
1059
|
+
private applySelectionChange(apply: () => { changed: GridRow[] }, evt?: Event | null): void {
|
|
1046
1060
|
const { changed } = apply();
|
|
1047
1061
|
if (changed.length === 0) {
|
|
1048
1062
|
return;
|
|
@@ -1329,10 +1343,7 @@ export class VanillaGridController {
|
|
|
1329
1343
|
|
|
1330
1344
|
/** Raise viewPortKeyDown / viewPortKeyPress on behalf of the element.
|
|
1331
1345
|
* Element gates this with `options.keyDownOverrides`. */
|
|
1332
|
-
raiseCellNavKeyEvent(
|
|
1333
|
-
type: 'keydown' | 'keypress',
|
|
1334
|
-
event: KeyboardEvent,
|
|
1335
|
-
): void {
|
|
1346
|
+
raiseCellNavKeyEvent(type: 'keydown' | 'keypress', event: KeyboardEvent): void {
|
|
1336
1347
|
const rowCol = this.resolveCellNavRowCol(this.cellNavLastRowCol);
|
|
1337
1348
|
if (type === 'keydown') this.gridApi.cellNav.raise.viewPortKeyDown(event, rowCol);
|
|
1338
1349
|
else this.gridApi.cellNav.raise.viewPortKeyPress(event, rowCol);
|
|
@@ -1388,8 +1399,11 @@ export class VanillaGridController {
|
|
|
1388
1399
|
|
|
1389
1400
|
/** Element registers its scroll-and-focus handler here so the API
|
|
1390
1401
|
* binding above can delegate DOM work. Controller stays DOM-free. */
|
|
1391
|
-
private cellNavScrollRequest: ((rowId: string | null, columnName: string | null) => void) | null =
|
|
1392
|
-
|
|
1402
|
+
private cellNavScrollRequest: ((rowId: string | null, columnName: string | null) => void) | null =
|
|
1403
|
+
null;
|
|
1404
|
+
setCellNavScrollHandler(
|
|
1405
|
+
handler: ((rowId: string | null, columnName: string | null) => void) | null,
|
|
1406
|
+
): void {
|
|
1393
1407
|
this.cellNavScrollRequest = handler;
|
|
1394
1408
|
}
|
|
1395
1409
|
|
|
@@ -1437,10 +1451,7 @@ export class VanillaGridController {
|
|
|
1437
1451
|
}
|
|
1438
1452
|
|
|
1439
1453
|
private infiniteScrollResetScroll(scrollUp?: boolean, scrollDown?: boolean): void {
|
|
1440
|
-
this.infiniteScrollState = resetInfiniteScrollState(
|
|
1441
|
-
scrollUp ?? false,
|
|
1442
|
-
scrollDown ?? true,
|
|
1443
|
-
);
|
|
1454
|
+
this.infiniteScrollState = resetInfiniteScrollState(scrollUp ?? false, scrollDown ?? true);
|
|
1444
1455
|
this.infiniteScrollScrollToTopRequest?.();
|
|
1445
1456
|
}
|
|
1446
1457
|
|
|
@@ -1484,6 +1495,16 @@ export class VanillaGridController {
|
|
|
1484
1495
|
this.infiniteScrollScrollToTopRequest = handler;
|
|
1485
1496
|
}
|
|
1486
1497
|
|
|
1498
|
+
/** Element-owned render benchmark. When present, `core.benchmark()`
|
|
1499
|
+
* measures the actual render path instead of the controller-only
|
|
1500
|
+
* pipeline builder. */
|
|
1501
|
+
private benchmarkRequest: ((iterations?: number) => Promise<GridBenchmarkResult>) | null = null;
|
|
1502
|
+
setBenchmarkHandler(
|
|
1503
|
+
handler: ((iterations?: number) => Promise<GridBenchmarkResult>) | null,
|
|
1504
|
+
): void {
|
|
1505
|
+
this.benchmarkRequest = handler;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1487
1508
|
getInfiniteScrollState(): Readonly<GridInfiniteScrollState> {
|
|
1488
1509
|
return this.infiniteScrollState;
|
|
1489
1510
|
}
|
|
@@ -1495,7 +1516,15 @@ export class VanillaGridController {
|
|
|
1495
1516
|
}
|
|
1496
1517
|
|
|
1497
1518
|
setViewportWidth(width: number): void {
|
|
1498
|
-
|
|
1519
|
+
// Under-estimate by a couple pixels: the body viewport's `overflow-x:
|
|
1520
|
+
// auto` plus sub-pixel rounding on the host's box (border, padding,
|
|
1521
|
+
// theme-applied transforms) means measuring the host's clientWidth
|
|
1522
|
+
// tends to be ~1-2px wider than the actual track-renderable width.
|
|
1523
|
+
// Without the margin a horizontal scrollbar appears by default even
|
|
1524
|
+
// when columns nominally fit. Floor instead of round so we never
|
|
1525
|
+
// round up over the actual width.
|
|
1526
|
+
const adjusted = Math.max(0, Math.floor(width) - 2);
|
|
1527
|
+
this.viewportWidth = adjusted;
|
|
1499
1528
|
}
|
|
1500
1529
|
|
|
1501
1530
|
getViewportWidth(): number {
|
|
@@ -1513,9 +1542,7 @@ export class VanillaGridController {
|
|
|
1513
1542
|
buildTemplateColumnsWithOverride(columnName: string, widthPx: number): string {
|
|
1514
1543
|
const widthStr = `${Math.max(88, Math.round(widthPx))}px`;
|
|
1515
1544
|
return buildGridTemplateColumns(
|
|
1516
|
-
this.visibleColumns.map((c) =>
|
|
1517
|
-
c.name === columnName ? { ...c, width: widthStr } : c,
|
|
1518
|
-
),
|
|
1545
|
+
this.visibleColumns.map((c) => (c.name === columnName ? { ...c, width: widthStr } : c)),
|
|
1519
1546
|
this.viewportWidth,
|
|
1520
1547
|
);
|
|
1521
1548
|
}
|
|
@@ -1663,6 +1690,13 @@ export class VanillaGridController {
|
|
|
1663
1690
|
return this.editingCell?.rowId === rowId && this.editingCell?.columnName === columnName;
|
|
1664
1691
|
}
|
|
1665
1692
|
|
|
1693
|
+
/** Compact key for the editing cell, or null when no edit is in flight.
|
|
1694
|
+
* Used by the patch path to detect when the editing position moves so it
|
|
1695
|
+
* can force the affected cells through the slow rebuild path. */
|
|
1696
|
+
getEditingCellKey(): string | null {
|
|
1697
|
+
return this.editingCell ? `${this.editingCell.rowId} ${this.editingCell.columnName}` : null;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1666
1700
|
/**
|
|
1667
1701
|
* Serialise the current grid state. Mirrors `ui.grid.saveState.save`:
|
|
1668
1702
|
* every field is gated by an `options.save*` flag with the old grid's
|
|
@@ -1684,7 +1718,7 @@ export class VanillaGridController {
|
|
|
1684
1718
|
const saveScroll = opts.saveScroll === true;
|
|
1685
1719
|
const saveFocus = saveScroll ? false : opts.saveFocus !== false;
|
|
1686
1720
|
void saveVisible; // Visible column state is derived from columnOrder + defs;
|
|
1687
|
-
|
|
1721
|
+
// kept as an opt-flag for forward compat.
|
|
1688
1722
|
|
|
1689
1723
|
const state: GridSaveState = {};
|
|
1690
1724
|
if (saveSort) state.sortState = { ...this.sortState };
|
|
@@ -1735,7 +1769,8 @@ export class VanillaGridController {
|
|
|
1735
1769
|
if (state.collapsedGroups !== undefined) this.collapsedGroups = { ...state.collapsedGroups };
|
|
1736
1770
|
if (state.pinnedColumns !== undefined) this.pinnedColumns = { ...state.pinnedColumns };
|
|
1737
1771
|
if (state.columnOrder !== undefined) this.columnOrder = [...state.columnOrder];
|
|
1738
|
-
if (state.columnWidthOverrides !== undefined)
|
|
1772
|
+
if (state.columnWidthOverrides !== undefined)
|
|
1773
|
+
this.columnWidthOverrides = { ...state.columnWidthOverrides };
|
|
1739
1774
|
if (state.currentPage !== undefined) this.currentPage = state.currentPage;
|
|
1740
1775
|
if (state.pageSize !== undefined) this.pageSize = state.pageSize;
|
|
1741
1776
|
if (state.expandedRows !== undefined) this.expandedRows = { ...state.expandedRows };
|
|
@@ -1781,6 +1816,8 @@ export class VanillaGridController {
|
|
|
1781
1816
|
}
|
|
1782
1817
|
|
|
1783
1818
|
private refresh(): void {
|
|
1819
|
+
this.maybeEnableWasmEngine();
|
|
1820
|
+
|
|
1784
1821
|
const orderedColumns = orderVisibleColumns(this.options.columnDefs, this.columnOrder);
|
|
1785
1822
|
const applyWidthOverrides = (columns: GridColumnDef[]): GridColumnDef[] =>
|
|
1786
1823
|
columns.map((col) => {
|
|
@@ -1867,6 +1904,23 @@ export class VanillaGridController {
|
|
|
1867
1904
|
this.emit();
|
|
1868
1905
|
}
|
|
1869
1906
|
|
|
1907
|
+
private maybeEnableWasmEngine(): void {
|
|
1908
|
+
if (this.wasmEngineInitRequested || activeGridEngineBackend() === 'rust-wasm') {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
this.wasmEngineInitRequested = true;
|
|
1913
|
+
void enableUiGridWasmEngine()
|
|
1914
|
+
.then(() => {
|
|
1915
|
+
if (!this.disposed) {
|
|
1916
|
+
this.refresh();
|
|
1917
|
+
}
|
|
1918
|
+
})
|
|
1919
|
+
.catch(() => {
|
|
1920
|
+
this.wasmEngineInitRequested = false;
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1870
1924
|
private emit(): void {
|
|
1871
1925
|
const snapshot = this.getSnapshot();
|
|
1872
1926
|
for (const subscriber of this.subscribers) {
|
|
@@ -1975,37 +2029,43 @@ export class VanillaGridController {
|
|
|
1975
2029
|
this.refresh();
|
|
1976
2030
|
}
|
|
1977
2031
|
|
|
1978
|
-
private benchmark(iterations?: number) {
|
|
2032
|
+
private async benchmark(iterations?: number): Promise<GridBenchmarkResult> {
|
|
1979
2033
|
const now = () => (typeof performance === 'undefined' ? Date.now() : performance.now());
|
|
1980
|
-
|
|
1981
|
-
const started = now();
|
|
1982
|
-
let lastResult = this.pipeline;
|
|
1983
|
-
|
|
1984
|
-
for (let index = 0; index < loops; index += 1) {
|
|
1985
|
-
lastResult = defaultGridEngine.buildPipeline({
|
|
1986
|
-
options: this.options,
|
|
1987
|
-
columns: this.visibleColumns,
|
|
1988
|
-
activeFilters: this.activeFilters,
|
|
1989
|
-
sortState: this.sortState,
|
|
1990
|
-
groupByColumns: this.groupByColumns,
|
|
1991
|
-
collapsedGroups: this.collapsedGroups,
|
|
1992
|
-
hiddenRowReasons: this.hiddenRowReasons,
|
|
1993
|
-
expandedRows: this.expandedRows,
|
|
1994
|
-
expandedTreeRows: this.expandedTreeRows,
|
|
1995
|
-
currentPage: this.currentPage,
|
|
1996
|
-
pageSize: this.pageSize,
|
|
1997
|
-
rowSize: this.getRowSize(),
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2034
|
+
let result: GridBenchmarkResult;
|
|
2000
2035
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2036
|
+
if (this.benchmarkRequest) {
|
|
2037
|
+
result = await this.benchmarkRequest(iterations);
|
|
2038
|
+
} else {
|
|
2039
|
+
const loops = Math.max(1, iterations ?? this.options.benchmark?.iterations ?? 25);
|
|
2040
|
+
const started = now();
|
|
2041
|
+
let lastResult = this.pipeline;
|
|
2042
|
+
|
|
2043
|
+
for (let index = 0; index < loops; index += 1) {
|
|
2044
|
+
lastResult = defaultGridEngine.buildPipeline({
|
|
2045
|
+
options: this.options,
|
|
2046
|
+
columns: this.visibleColumns,
|
|
2047
|
+
activeFilters: this.activeFilters,
|
|
2048
|
+
sortState: this.sortState,
|
|
2049
|
+
groupByColumns: this.groupByColumns,
|
|
2050
|
+
collapsedGroups: this.collapsedGroups,
|
|
2051
|
+
hiddenRowReasons: this.hiddenRowReasons,
|
|
2052
|
+
expandedRows: this.expandedRows,
|
|
2053
|
+
expandedTreeRows: this.expandedTreeRows,
|
|
2054
|
+
currentPage: this.currentPage,
|
|
2055
|
+
pageSize: this.pageSize,
|
|
2056
|
+
rowSize: this.getRowSize(),
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const elapsedMs = now() - started;
|
|
2061
|
+
result = {
|
|
2062
|
+
iterations: loops,
|
|
2063
|
+
totalMs: elapsedMs,
|
|
2064
|
+
averageMs: elapsedMs / loops,
|
|
2065
|
+
visibleRows: lastResult.visibleRows.length,
|
|
2066
|
+
renderedItems: lastResult.displayItems.length,
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2009
2069
|
|
|
2010
2070
|
this.gridApi.core.raise.benchmarkComplete(result);
|
|
2011
2071
|
return result;
|
|
@@ -2074,18 +2134,21 @@ export class VanillaGridController {
|
|
|
2074
2134
|
// present, open the generated PDF the same way the old module did;
|
|
2075
2135
|
// when missing, just return the doc definition so the caller can
|
|
2076
2136
|
// render it themselves. This mirrors `uiGridExporterService.pdfExport`.
|
|
2077
|
-
const win =
|
|
2078
|
-
|
|
2079
|
-
|
|
2137
|
+
const win =
|
|
2138
|
+
typeof window !== 'undefined'
|
|
2139
|
+
? (window as typeof window & {
|
|
2140
|
+
pdfMake?: {
|
|
2141
|
+
createPdf: (doc: GridExporterPdfDocDefinition) => {
|
|
2142
|
+
open: () => void;
|
|
2143
|
+
download: (filename: string) => void;
|
|
2144
|
+
};
|
|
2145
|
+
};
|
|
2146
|
+
})
|
|
2147
|
+
: undefined;
|
|
2080
2148
|
if (win?.pdfMake) {
|
|
2081
2149
|
const resolved = this.resolveExporterOptions();
|
|
2082
2150
|
const pdfOpts = resolveGridExporterPdfOptions(this.options);
|
|
2083
|
-
const filename = resolveExporterFilename(
|
|
2084
|
-
pdfOpts.filename,
|
|
2085
|
-
'download.pdf',
|
|
2086
|
-
rowType,
|
|
2087
|
-
colType,
|
|
2088
|
-
);
|
|
2151
|
+
const filename = resolveExporterFilename(pdfOpts.filename, 'download.pdf', rowType, colType);
|
|
2089
2152
|
const pdf = win.pdfMake.createPdf(doc);
|
|
2090
2153
|
// Prefer download when the consumer supplied a filename; otherwise
|
|
2091
2154
|
// fall back to .open() which pops the pdf in a new browser tab.
|
|
@@ -2104,7 +2167,12 @@ export class VanillaGridController {
|
|
|
2104
2167
|
): GridExporterExcelSheetData {
|
|
2105
2168
|
const exporterOptions = this.resolveExporterOptions();
|
|
2106
2169
|
const columns = colType === 'all' ? this.options.columnDefs : this.visibleColumns;
|
|
2107
|
-
return buildGridExcelSheetData(
|
|
2170
|
+
return buildGridExcelSheetData(
|
|
2171
|
+
columns,
|
|
2172
|
+
this.exporterRowsFor(rowType),
|
|
2173
|
+
exporterOptions,
|
|
2174
|
+
colType,
|
|
2175
|
+
);
|
|
2108
2176
|
}
|
|
2109
2177
|
|
|
2110
2178
|
private exportExcel(
|
|
@@ -2115,21 +2183,27 @@ export class VanillaGridController {
|
|
|
2115
2183
|
// ExcelBuilder is an optional global (consumers load it separately).
|
|
2116
2184
|
// When present, produce + download the xlsx the same way the old module
|
|
2117
2185
|
// did; when missing, just return the raw sheet data.
|
|
2118
|
-
const win =
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2186
|
+
const win =
|
|
2187
|
+
typeof window !== 'undefined'
|
|
2188
|
+
? (window as typeof window & {
|
|
2189
|
+
ExcelBuilder?: {
|
|
2190
|
+
Worksheet: new (config: { name: string }) => { setData: (data: unknown) => void };
|
|
2191
|
+
Workbook: new () => { addWorksheet: (sheet: unknown) => void };
|
|
2192
|
+
Builder: {
|
|
2193
|
+
createFile: (workbook: unknown, options: { type: 'blob' }) => Promise<Blob>;
|
|
2194
|
+
};
|
|
2195
|
+
};
|
|
2196
|
+
})
|
|
2197
|
+
: undefined;
|
|
2125
2198
|
const ExcelBuilder = win?.ExcelBuilder;
|
|
2126
2199
|
if (!ExcelBuilder) {
|
|
2127
2200
|
return sheetData;
|
|
2128
2201
|
}
|
|
2129
2202
|
const excelOptions = resolveGridExporterExcelOptions(this.options);
|
|
2130
|
-
const sheetName =
|
|
2131
|
-
|
|
2132
|
-
|
|
2203
|
+
const sheetName =
|
|
2204
|
+
typeof excelOptions.sheetName === 'function'
|
|
2205
|
+
? excelOptions.sheetName(rowType, colType)
|
|
2206
|
+
: (excelOptions.sheetName ?? 'Sheet1');
|
|
2133
2207
|
const filename = sanitizeDownloadFilename(
|
|
2134
2208
|
resolveExporterFilename(excelOptions.filename, 'download.xlsx', rowType, colType),
|
|
2135
2209
|
);
|
|
@@ -2186,11 +2260,7 @@ export class VanillaGridController {
|
|
|
2186
2260
|
}
|
|
2187
2261
|
|
|
2188
2262
|
private async validateGetInvalidRows(): Promise<GridRecord[]> {
|
|
2189
|
-
return validateAllGridRows(
|
|
2190
|
-
this.options.data,
|
|
2191
|
-
this.options.columnDefs,
|
|
2192
|
-
this.validatorRegistry,
|
|
2193
|
-
);
|
|
2263
|
+
return validateAllGridRows(this.options.data, this.options.columnDefs, this.validatorRegistry);
|
|
2194
2264
|
}
|
|
2195
2265
|
|
|
2196
2266
|
/** Test hook — exposes the validator registry so consumers can inspect or
|
|
@@ -2247,7 +2317,12 @@ export class VanillaGridController {
|
|
|
2247
2317
|
// the caller didn't force JSON, fall through to CSV. The old module
|
|
2248
2318
|
// decided purely based on MIME type; `importText` is a newer entry
|
|
2249
2319
|
// point we provide for drag/drop flows.
|
|
2250
|
-
const asJson =
|
|
2320
|
+
const asJson =
|
|
2321
|
+
type === 'csv'
|
|
2322
|
+
? null
|
|
2323
|
+
: parseGridImporterJson(text, {
|
|
2324
|
+
errorCallback: type === 'json' ? opts.errorCallback : undefined,
|
|
2325
|
+
});
|
|
2251
2326
|
if (asJson && asJson.length > 0) {
|
|
2252
2327
|
const objects = buildGridImporterObjectsFromJson(asJson, opts);
|
|
2253
2328
|
this.importerDispatchObjects(objects);
|
|
@@ -2320,11 +2395,9 @@ export class VanillaGridController {
|
|
|
2320
2395
|
/** Menu items the importer contributes. Just the single "Import"
|
|
2321
2396
|
* entry when the feature is enabled; returns an empty array otherwise. */
|
|
2322
2397
|
private buildImporterMenuItems(): GridMenuItem[] {
|
|
2323
|
-
return buildGridImporterMenuItems(
|
|
2324
|
-
this.
|
|
2325
|
-
|
|
2326
|
-
{ importAFile: () => this.importerRequestFile() },
|
|
2327
|
-
);
|
|
2398
|
+
return buildGridImporterMenuItems(this.options, this.labels, {
|
|
2399
|
+
importAFile: () => this.importerRequestFile(),
|
|
2400
|
+
});
|
|
2328
2401
|
}
|
|
2329
2402
|
|
|
2330
2403
|
/** Menu items row-edit contributes: "Save changes" + "Retry errored
|
|
@@ -2354,15 +2427,11 @@ export class VanillaGridController {
|
|
|
2354
2427
|
}
|
|
2355
2428
|
|
|
2356
2429
|
private rowEditGetDirtyRows(): GridRow[] {
|
|
2357
|
-
return this.pipeline.visibleRows.filter((row) =>
|
|
2358
|
-
this.rowEditState.dirtyRowIds.has(row.id),
|
|
2359
|
-
);
|
|
2430
|
+
return this.pipeline.visibleRows.filter((row) => this.rowEditState.dirtyRowIds.has(row.id));
|
|
2360
2431
|
}
|
|
2361
2432
|
|
|
2362
2433
|
private rowEditGetErrorRows(): GridRow[] {
|
|
2363
|
-
return this.pipeline.visibleRows.filter((row) =>
|
|
2364
|
-
this.rowEditState.errorRowIds.has(row.id),
|
|
2365
|
-
);
|
|
2434
|
+
return this.pipeline.visibleRows.filter((row) => this.rowEditState.errorRowIds.has(row.id));
|
|
2366
2435
|
}
|
|
2367
2436
|
|
|
2368
2437
|
private rowEditFlushDirtyRows(): Promise<void> {
|
|
@@ -2437,8 +2506,7 @@ export class VanillaGridController {
|
|
|
2437
2506
|
markGridRowSaving(this.rowEditState, row);
|
|
2438
2507
|
this.rowEditSavePromiseOverrides.delete(row.id);
|
|
2439
2508
|
this.gridApi.rowEdit.raise.saveRow(row.entity);
|
|
2440
|
-
const savePromise =
|
|
2441
|
-
this.rowEditSavePromiseOverrides.get(row.id) ?? Promise.resolve();
|
|
2509
|
+
const savePromise = this.rowEditSavePromiseOverrides.get(row.id) ?? Promise.resolve();
|
|
2442
2510
|
this.rowEditState.savePromises.set(row.id, savePromise);
|
|
2443
2511
|
this.emit();
|
|
2444
2512
|
return savePromise
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import * as core from '@ornery/ui-grid-core';
|
|
3
4
|
import {
|
|
4
5
|
SORT_DIRECTIONS,
|
|
5
6
|
activeGridEngineBackend,
|
|
7
|
+
buildGridRows,
|
|
6
8
|
clearRustWasmGridEngine,
|
|
7
9
|
} from '@ornery/ui-grid-core';
|
|
8
10
|
import {
|
|
@@ -143,18 +145,11 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
143
145
|
it('registers Rust/WASM bindings through the vanilla API', async () => {
|
|
144
146
|
const module: UiGridRustWebModule = {
|
|
145
147
|
default: vi.fn(async () => undefined),
|
|
146
|
-
build_pipeline_js: vi.fn((context) => ({
|
|
147
|
-
visibleRows: context.options.data,
|
|
148
|
-
displayItems: [],
|
|
149
|
-
virtualizationEnabled: false,
|
|
150
|
-
pipelineMs: 0,
|
|
151
|
-
totalItems: context.options.data.length,
|
|
152
|
-
})),
|
|
153
148
|
};
|
|
154
149
|
|
|
155
150
|
await registerVanillaUiGridRustModule(module);
|
|
156
151
|
expect(module.default).toHaveBeenCalledTimes(1);
|
|
157
|
-
expect(activeGridEngineBackend()).toBe('
|
|
152
|
+
expect(activeGridEngineBackend()).toBe('typescript');
|
|
158
153
|
});
|
|
159
154
|
|
|
160
155
|
it('controller API exposes subscribe and action methods', () => {
|
|
@@ -184,6 +179,54 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
184
179
|
]);
|
|
185
180
|
});
|
|
186
181
|
|
|
182
|
+
it('lazy-loads the wasm engine once and refreshes through it when ready', async () => {
|
|
183
|
+
const enableSpy = vi.spyOn(core, 'enableUiGridWasmEngine').mockImplementation(async () => {
|
|
184
|
+
core.registerRustWasmGridEngine({
|
|
185
|
+
buildPipeline(context) {
|
|
186
|
+
return {
|
|
187
|
+
visibleRows: buildGridRows(
|
|
188
|
+
context.options,
|
|
189
|
+
context.rowSize,
|
|
190
|
+
context.hiddenRowReasons,
|
|
191
|
+
context.expandedRows,
|
|
192
|
+
),
|
|
193
|
+
displayItems: [],
|
|
194
|
+
virtualizationEnabled: false,
|
|
195
|
+
pipelineMs: 123,
|
|
196
|
+
totalItems: context.options.data.length,
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const controller = createVanillaGridController({
|
|
203
|
+
id: 'controller-wasm-bootstrap',
|
|
204
|
+
data: [
|
|
205
|
+
{ id: 'r1', name: 'Gamma' },
|
|
206
|
+
{ id: 'r2', name: 'Alpha' },
|
|
207
|
+
],
|
|
208
|
+
columnDefs: [{ name: 'name' }],
|
|
209
|
+
enableSorting: true,
|
|
210
|
+
enableFiltering: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(enableSpy).toHaveBeenCalledTimes(1);
|
|
214
|
+
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
const snapshot = controller.getSnapshot();
|
|
217
|
+
return activeGridEngineBackend() === 'rust-wasm' && snapshot.pipeline.pipelineMs === 123
|
|
218
|
+
? snapshot
|
|
219
|
+
: null;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
controller.setFilter('name', 'Alpha');
|
|
223
|
+
controller.sortColumn('name', SORT_DIRECTIONS.asc);
|
|
224
|
+
|
|
225
|
+
expect(enableSpy).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(controller.getSnapshot().pipeline.pipelineMs).toBe(123);
|
|
227
|
+
enableSpy.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
|
|
187
230
|
it('applies overridable SVG icons for controls', async () => {
|
|
188
231
|
const target = document.getElementById('app');
|
|
189
232
|
if (!target) {
|
package/src/index.test.ts
CHANGED
|
@@ -4,11 +4,9 @@ vi.mock('@ornery/ui-grid-core', async (importOriginal) => {
|
|
|
4
4
|
const actual = await importOriginal<typeof import('@ornery/ui-grid-core')>();
|
|
5
5
|
return {
|
|
6
6
|
...actual,
|
|
7
|
-
registerRustWasmGridEngine: vi.fn(),
|
|
8
7
|
};
|
|
9
8
|
});
|
|
10
9
|
|
|
11
|
-
import { registerRustWasmGridEngine } from '@ornery/ui-grid-core';
|
|
12
10
|
import { defineUiGridElement, mountVanillaUiGrid } from './index';
|
|
13
11
|
|
|
14
12
|
describe('mountVanillaUiGrid', () => {
|
|
@@ -31,13 +29,11 @@ describe('mountVanillaUiGrid', () => {
|
|
|
31
29
|
|
|
32
30
|
const module = {
|
|
33
31
|
default: vi.fn(async () => undefined),
|
|
34
|
-
build_pipeline_js: vi.fn(),
|
|
35
32
|
};
|
|
36
33
|
|
|
37
34
|
const grid = await mountVanillaUiGrid(target, options, module);
|
|
38
35
|
|
|
39
36
|
expect(module.default).toHaveBeenCalledTimes(1);
|
|
40
|
-
expect(registerRustWasmGridEngine).toHaveBeenCalledTimes(1);
|
|
41
37
|
expect(customElements.get('ui-grid-element')).toBeDefined();
|
|
42
38
|
expect(grid.tagName.toLowerCase()).toBe('ui-grid-element');
|
|
43
39
|
expect(grid.options).toStrictEqual(options);
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { BuildGridPipelineContext, GridOptions, PipelineResult } from '@ornery/ui-grid-core';
|
|
1
|
+
import type { GridOptions } from '@ornery/ui-grid-core';
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
defineStandaloneUiGridElement,
|
|
@@ -33,7 +32,6 @@ export type { GridOptions, UiGridApi } from '@ornery/ui-grid-core';
|
|
|
33
32
|
|
|
34
33
|
export interface UiGridRustWebModule {
|
|
35
34
|
default(input?: unknown): Promise<unknown>;
|
|
36
|
-
build_pipeline_js(context: BuildGridPipelineContext): PipelineResult;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
export { defineStandaloneUiGridElement as defineUiGridElement };
|
|
@@ -43,11 +41,6 @@ export async function registerVanillaUiGridRustModule(
|
|
|
43
41
|
input?: unknown,
|
|
44
42
|
): Promise<void> {
|
|
45
43
|
await module.default(input);
|
|
46
|
-
registerRustWasmGridEngine({
|
|
47
|
-
buildPipeline(context: BuildGridPipelineContext): PipelineResult {
|
|
48
|
-
return module.build_pipeline_js(context);
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
export async function mountVanillaUiGrid(
|
|
@@ -57,7 +50,11 @@ export async function mountVanillaUiGrid(
|
|
|
57
50
|
tagName = 'ui-grid-element',
|
|
58
51
|
): Promise<VanillaUiGridElement> {
|
|
59
52
|
if (rustModule) {
|
|
60
|
-
|
|
53
|
+
try {
|
|
54
|
+
await registerVanillaUiGridRustModule(rustModule);
|
|
55
|
+
} catch {
|
|
56
|
+
// Fall back to JS engine when WASM cannot be initialized.
|
|
57
|
+
}
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
await defineStandaloneUiGridElement(tagName);
|
|
@@ -346,16 +346,10 @@ describe('vanilla grid keyboard navigation', () => {
|
|
|
346
346
|
});
|
|
347
347
|
|
|
348
348
|
it('Enter on a cell enters edit mode for that cell', async () => {
|
|
349
|
-
const { shadow
|
|
349
|
+
const { shadow } = await mountGrid();
|
|
350
350
|
const cell = cellIn(shadow, 'r2', 'name');
|
|
351
351
|
cell.focus();
|
|
352
352
|
pressKey(cell, 'Enter');
|
|
353
|
-
const snapshot = getApi(options)!.core.getState
|
|
354
|
-
? null
|
|
355
|
-
: null;
|
|
356
|
-
// Assert via the controller — query it via the element's getState hook
|
|
357
|
-
// indirectly: the cell editor should be mounted in the shadow DOM.
|
|
358
|
-
void snapshot;
|
|
359
353
|
await waitFor(() => shadow.querySelector(`ui-grid-cell-editor[data-row="r2"][data-column="name"]`));
|
|
360
354
|
const editorInput = shadow.querySelector<HTMLInputElement>(
|
|
361
355
|
`ui-grid-cell-editor[data-row="r2"][data-column="name"] input`,
|