@ornery/ui-grid-vanilla 1.0.4 → 1.0.6
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/demo/main.ts +1 -2
- package/dist/index.d.mts +40 -2
- package/dist/index.d.ts +40 -2
- package/dist/index.js +263 -100
- package/dist/index.mjs +263 -102
- package/package.json +6 -5
- 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 +142 -98
- package/src/index.integration.test.ts +27 -10
- 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/vitest.config.ts +30 -0
package/src/grid-controller.ts
CHANGED
|
@@ -121,6 +121,7 @@ import {
|
|
|
121
121
|
type DisplayItem,
|
|
122
122
|
type GridCellPosition,
|
|
123
123
|
type GridColumnDef,
|
|
124
|
+
type GridBenchmarkResult,
|
|
124
125
|
type GridRowColumn,
|
|
125
126
|
type GridLabels,
|
|
126
127
|
type GridOptions,
|
|
@@ -215,7 +216,10 @@ const DEFAULT_COL_MIN_WIDTH = 176;
|
|
|
215
216
|
* 3. Flex columns (*, 1fr, minmax): divide remaining space equally.
|
|
216
217
|
* 4. Leftover/excess pixels distributed 1px at a time for pixel-perfect fit.
|
|
217
218
|
*/
|
|
218
|
-
function buildGridTemplateColumns(
|
|
219
|
+
function buildGridTemplateColumns(
|
|
220
|
+
columns: readonly GridColumnDef[],
|
|
221
|
+
viewportWidth?: number,
|
|
222
|
+
): string {
|
|
219
223
|
if (!viewportWidth || viewportWidth <= 0) {
|
|
220
224
|
return columns.map((column) => gridColumnWidth(column)).join(' ');
|
|
221
225
|
}
|
|
@@ -355,7 +359,9 @@ export class VanillaGridController {
|
|
|
355
359
|
this.groupByColumns = options.grouping?.groupBy ? [...options.grouping.groupBy] : [];
|
|
356
360
|
this.lastOptionsGroupBy = options.grouping?.groupBy ? [...options.grouping.groupBy] : undefined;
|
|
357
361
|
this.pinnedColumns = buildInitialPinnedState(options.columnDefs);
|
|
358
|
-
const declaresPin = options.columnDefs.some(
|
|
362
|
+
const declaresPin = options.columnDefs.some(
|
|
363
|
+
(col) => col.pinnedLeft === true || col.pinnedRight === true,
|
|
364
|
+
);
|
|
359
365
|
this.lastOptionsPinnedState = declaresPin ? { ...this.pinnedColumns } : undefined;
|
|
360
366
|
this.lastOptionsInfiniteScrollUp = options.infiniteScrollUp;
|
|
361
367
|
this.lastOptionsInfiniteScrollDown = options.infiniteScrollDown;
|
|
@@ -396,12 +402,14 @@ export class VanillaGridController {
|
|
|
396
402
|
exportCsv: (rowType, colType) => this.exportCsv(rowType, colType),
|
|
397
403
|
buildCsv: (rowType, colType) => this.buildCsv(rowType, colType),
|
|
398
404
|
pdfExport: (rowType, colType) => this.exportPdf(rowType, colType),
|
|
399
|
-
buildPdfDocDefinition: (rowType, colType) =>
|
|
400
|
-
this.buildPdfDocDefinition(rowType, colType),
|
|
405
|
+
buildPdfDocDefinition: (rowType, colType) => this.buildPdfDocDefinition(rowType, colType),
|
|
401
406
|
excelExport: (rowType, colType) => this.exportExcel(rowType, colType),
|
|
402
407
|
buildExcelSheetData: (rowType, colType) => this.buildExcelSheetData(rowType, colType),
|
|
403
408
|
getExporterMenuItems: () => this.buildExporterMenuItems(),
|
|
404
|
-
getExporterOptions: () => ({
|
|
409
|
+
getExporterOptions: () => ({
|
|
410
|
+
...this.exporterOverrides,
|
|
411
|
+
...resolveGridExporterOptions(this.options),
|
|
412
|
+
}),
|
|
405
413
|
setExporterOptions: (overrides) => {
|
|
406
414
|
this.exporterOverrides = { ...this.exporterOverrides, ...overrides };
|
|
407
415
|
},
|
|
@@ -491,8 +499,7 @@ export class VanillaGridController {
|
|
|
491
499
|
infiniteScrollSetDirections: (scrollUp, scrollDown) =>
|
|
492
500
|
this.infiniteScrollSetDirections(scrollUp, scrollDown),
|
|
493
501
|
// Row-edit bindings — port of ui.grid.rowEdit public API.
|
|
494
|
-
rowEditSetSavePromise: (rowEntity, promise) =>
|
|
495
|
-
this.rowEditSetSavePromise(rowEntity, promise),
|
|
502
|
+
rowEditSetSavePromise: (rowEntity, promise) => this.rowEditSetSavePromise(rowEntity, promise),
|
|
496
503
|
rowEditGetDirtyRows: () => this.rowEditGetDirtyRows(),
|
|
497
504
|
rowEditGetErrorRows: () => this.rowEditGetErrorRows(),
|
|
498
505
|
rowEditFlushDirtyRows: () => this.rowEditFlushDirtyRows(),
|
|
@@ -621,7 +628,8 @@ export class VanillaGridController {
|
|
|
621
628
|
if (options.grouping?.groupBy !== undefined) {
|
|
622
629
|
const incoming = options.grouping.groupBy;
|
|
623
630
|
const prev = this.lastOptionsGroupBy;
|
|
624
|
-
const changed =
|
|
631
|
+
const changed =
|
|
632
|
+
!prev || prev.length !== incoming.length || prev.some((col, i) => col !== incoming[i]);
|
|
625
633
|
if (changed) {
|
|
626
634
|
this.groupByColumns = [...incoming];
|
|
627
635
|
this.lastOptionsGroupBy = [...incoming];
|
|
@@ -651,8 +659,12 @@ export class VanillaGridController {
|
|
|
651
659
|
// Re-seed infinite-scroll directions only when the values structurally
|
|
652
660
|
// changed from the last options pass.
|
|
653
661
|
if (options.infiniteScrollUp !== undefined || options.infiniteScrollDown !== undefined) {
|
|
654
|
-
const upChanged =
|
|
655
|
-
|
|
662
|
+
const upChanged =
|
|
663
|
+
options.infiniteScrollUp !== undefined &&
|
|
664
|
+
options.infiniteScrollUp !== this.lastOptionsInfiniteScrollUp;
|
|
665
|
+
const downChanged =
|
|
666
|
+
options.infiniteScrollDown !== undefined &&
|
|
667
|
+
options.infiniteScrollDown !== this.lastOptionsInfiniteScrollDown;
|
|
656
668
|
if (upChanged || downChanged) {
|
|
657
669
|
this.infiniteScrollState = setInfiniteScrollDirectionsState(
|
|
658
670
|
this.infiniteScrollState,
|
|
@@ -1039,10 +1051,7 @@ export class VanillaGridController {
|
|
|
1039
1051
|
// re-renders with the updated `isSelected` / `isFocused` flags.
|
|
1040
1052
|
|
|
1041
1053
|
/** Internal: run a core selection mutation, raise events, trigger a render. */
|
|
1042
|
-
private applySelectionChange(
|
|
1043
|
-
apply: () => { changed: GridRow[] },
|
|
1044
|
-
evt?: Event | null,
|
|
1045
|
-
): void {
|
|
1054
|
+
private applySelectionChange(apply: () => { changed: GridRow[] }, evt?: Event | null): void {
|
|
1046
1055
|
const { changed } = apply();
|
|
1047
1056
|
if (changed.length === 0) {
|
|
1048
1057
|
return;
|
|
@@ -1329,10 +1338,7 @@ export class VanillaGridController {
|
|
|
1329
1338
|
|
|
1330
1339
|
/** Raise viewPortKeyDown / viewPortKeyPress on behalf of the element.
|
|
1331
1340
|
* Element gates this with `options.keyDownOverrides`. */
|
|
1332
|
-
raiseCellNavKeyEvent(
|
|
1333
|
-
type: 'keydown' | 'keypress',
|
|
1334
|
-
event: KeyboardEvent,
|
|
1335
|
-
): void {
|
|
1341
|
+
raiseCellNavKeyEvent(type: 'keydown' | 'keypress', event: KeyboardEvent): void {
|
|
1336
1342
|
const rowCol = this.resolveCellNavRowCol(this.cellNavLastRowCol);
|
|
1337
1343
|
if (type === 'keydown') this.gridApi.cellNav.raise.viewPortKeyDown(event, rowCol);
|
|
1338
1344
|
else this.gridApi.cellNav.raise.viewPortKeyPress(event, rowCol);
|
|
@@ -1388,8 +1394,11 @@ export class VanillaGridController {
|
|
|
1388
1394
|
|
|
1389
1395
|
/** Element registers its scroll-and-focus handler here so the API
|
|
1390
1396
|
* binding above can delegate DOM work. Controller stays DOM-free. */
|
|
1391
|
-
private cellNavScrollRequest: ((rowId: string | null, columnName: string | null) => void) | null =
|
|
1392
|
-
|
|
1397
|
+
private cellNavScrollRequest: ((rowId: string | null, columnName: string | null) => void) | null =
|
|
1398
|
+
null;
|
|
1399
|
+
setCellNavScrollHandler(
|
|
1400
|
+
handler: ((rowId: string | null, columnName: string | null) => void) | null,
|
|
1401
|
+
): void {
|
|
1393
1402
|
this.cellNavScrollRequest = handler;
|
|
1394
1403
|
}
|
|
1395
1404
|
|
|
@@ -1437,10 +1446,7 @@ export class VanillaGridController {
|
|
|
1437
1446
|
}
|
|
1438
1447
|
|
|
1439
1448
|
private infiniteScrollResetScroll(scrollUp?: boolean, scrollDown?: boolean): void {
|
|
1440
|
-
this.infiniteScrollState = resetInfiniteScrollState(
|
|
1441
|
-
scrollUp ?? false,
|
|
1442
|
-
scrollDown ?? true,
|
|
1443
|
-
);
|
|
1449
|
+
this.infiniteScrollState = resetInfiniteScrollState(scrollUp ?? false, scrollDown ?? true);
|
|
1444
1450
|
this.infiniteScrollScrollToTopRequest?.();
|
|
1445
1451
|
}
|
|
1446
1452
|
|
|
@@ -1484,6 +1490,16 @@ export class VanillaGridController {
|
|
|
1484
1490
|
this.infiniteScrollScrollToTopRequest = handler;
|
|
1485
1491
|
}
|
|
1486
1492
|
|
|
1493
|
+
/** Element-owned render benchmark. When present, `core.benchmark()`
|
|
1494
|
+
* measures the actual render path instead of the controller-only
|
|
1495
|
+
* pipeline builder. */
|
|
1496
|
+
private benchmarkRequest: ((iterations?: number) => Promise<GridBenchmarkResult>) | null = null;
|
|
1497
|
+
setBenchmarkHandler(
|
|
1498
|
+
handler: ((iterations?: number) => Promise<GridBenchmarkResult>) | null,
|
|
1499
|
+
): void {
|
|
1500
|
+
this.benchmarkRequest = handler;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1487
1503
|
getInfiniteScrollState(): Readonly<GridInfiniteScrollState> {
|
|
1488
1504
|
return this.infiniteScrollState;
|
|
1489
1505
|
}
|
|
@@ -1495,7 +1511,15 @@ export class VanillaGridController {
|
|
|
1495
1511
|
}
|
|
1496
1512
|
|
|
1497
1513
|
setViewportWidth(width: number): void {
|
|
1498
|
-
|
|
1514
|
+
// Under-estimate by a couple pixels: the body viewport's `overflow-x:
|
|
1515
|
+
// auto` plus sub-pixel rounding on the host's box (border, padding,
|
|
1516
|
+
// theme-applied transforms) means measuring the host's clientWidth
|
|
1517
|
+
// tends to be ~1-2px wider than the actual track-renderable width.
|
|
1518
|
+
// Without the margin a horizontal scrollbar appears by default even
|
|
1519
|
+
// when columns nominally fit. Floor instead of round so we never
|
|
1520
|
+
// round up over the actual width.
|
|
1521
|
+
const adjusted = Math.max(0, Math.floor(width) - 2);
|
|
1522
|
+
this.viewportWidth = adjusted;
|
|
1499
1523
|
}
|
|
1500
1524
|
|
|
1501
1525
|
getViewportWidth(): number {
|
|
@@ -1513,9 +1537,7 @@ export class VanillaGridController {
|
|
|
1513
1537
|
buildTemplateColumnsWithOverride(columnName: string, widthPx: number): string {
|
|
1514
1538
|
const widthStr = `${Math.max(88, Math.round(widthPx))}px`;
|
|
1515
1539
|
return buildGridTemplateColumns(
|
|
1516
|
-
this.visibleColumns.map((c) =>
|
|
1517
|
-
c.name === columnName ? { ...c, width: widthStr } : c,
|
|
1518
|
-
),
|
|
1540
|
+
this.visibleColumns.map((c) => (c.name === columnName ? { ...c, width: widthStr } : c)),
|
|
1519
1541
|
this.viewportWidth,
|
|
1520
1542
|
);
|
|
1521
1543
|
}
|
|
@@ -1663,6 +1685,13 @@ export class VanillaGridController {
|
|
|
1663
1685
|
return this.editingCell?.rowId === rowId && this.editingCell?.columnName === columnName;
|
|
1664
1686
|
}
|
|
1665
1687
|
|
|
1688
|
+
/** Compact key for the editing cell, or null when no edit is in flight.
|
|
1689
|
+
* Used by the patch path to detect when the editing position moves so it
|
|
1690
|
+
* can force the affected cells through the slow rebuild path. */
|
|
1691
|
+
getEditingCellKey(): string | null {
|
|
1692
|
+
return this.editingCell ? `${this.editingCell.rowId} ${this.editingCell.columnName}` : null;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1666
1695
|
/**
|
|
1667
1696
|
* Serialise the current grid state. Mirrors `ui.grid.saveState.save`:
|
|
1668
1697
|
* every field is gated by an `options.save*` flag with the old grid's
|
|
@@ -1684,7 +1713,7 @@ export class VanillaGridController {
|
|
|
1684
1713
|
const saveScroll = opts.saveScroll === true;
|
|
1685
1714
|
const saveFocus = saveScroll ? false : opts.saveFocus !== false;
|
|
1686
1715
|
void saveVisible; // Visible column state is derived from columnOrder + defs;
|
|
1687
|
-
|
|
1716
|
+
// kept as an opt-flag for forward compat.
|
|
1688
1717
|
|
|
1689
1718
|
const state: GridSaveState = {};
|
|
1690
1719
|
if (saveSort) state.sortState = { ...this.sortState };
|
|
@@ -1735,7 +1764,8 @@ export class VanillaGridController {
|
|
|
1735
1764
|
if (state.collapsedGroups !== undefined) this.collapsedGroups = { ...state.collapsedGroups };
|
|
1736
1765
|
if (state.pinnedColumns !== undefined) this.pinnedColumns = { ...state.pinnedColumns };
|
|
1737
1766
|
if (state.columnOrder !== undefined) this.columnOrder = [...state.columnOrder];
|
|
1738
|
-
if (state.columnWidthOverrides !== undefined)
|
|
1767
|
+
if (state.columnWidthOverrides !== undefined)
|
|
1768
|
+
this.columnWidthOverrides = { ...state.columnWidthOverrides };
|
|
1739
1769
|
if (state.currentPage !== undefined) this.currentPage = state.currentPage;
|
|
1740
1770
|
if (state.pageSize !== undefined) this.pageSize = state.pageSize;
|
|
1741
1771
|
if (state.expandedRows !== undefined) this.expandedRows = { ...state.expandedRows };
|
|
@@ -1975,37 +2005,43 @@ export class VanillaGridController {
|
|
|
1975
2005
|
this.refresh();
|
|
1976
2006
|
}
|
|
1977
2007
|
|
|
1978
|
-
private benchmark(iterations?: number) {
|
|
2008
|
+
private async benchmark(iterations?: number): Promise<GridBenchmarkResult> {
|
|
1979
2009
|
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
|
-
}
|
|
2010
|
+
let result: GridBenchmarkResult;
|
|
2000
2011
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2012
|
+
if (this.benchmarkRequest) {
|
|
2013
|
+
result = await this.benchmarkRequest(iterations);
|
|
2014
|
+
} else {
|
|
2015
|
+
const loops = Math.max(1, iterations ?? this.options.benchmark?.iterations ?? 25);
|
|
2016
|
+
const started = now();
|
|
2017
|
+
let lastResult = this.pipeline;
|
|
2018
|
+
|
|
2019
|
+
for (let index = 0; index < loops; index += 1) {
|
|
2020
|
+
lastResult = defaultGridEngine.buildPipeline({
|
|
2021
|
+
options: this.options,
|
|
2022
|
+
columns: this.visibleColumns,
|
|
2023
|
+
activeFilters: this.activeFilters,
|
|
2024
|
+
sortState: this.sortState,
|
|
2025
|
+
groupByColumns: this.groupByColumns,
|
|
2026
|
+
collapsedGroups: this.collapsedGroups,
|
|
2027
|
+
hiddenRowReasons: this.hiddenRowReasons,
|
|
2028
|
+
expandedRows: this.expandedRows,
|
|
2029
|
+
expandedTreeRows: this.expandedTreeRows,
|
|
2030
|
+
currentPage: this.currentPage,
|
|
2031
|
+
pageSize: this.pageSize,
|
|
2032
|
+
rowSize: this.getRowSize(),
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const elapsedMs = now() - started;
|
|
2037
|
+
result = {
|
|
2038
|
+
iterations: loops,
|
|
2039
|
+
totalMs: elapsedMs,
|
|
2040
|
+
averageMs: elapsedMs / loops,
|
|
2041
|
+
visibleRows: lastResult.visibleRows.length,
|
|
2042
|
+
renderedItems: lastResult.displayItems.length,
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2009
2045
|
|
|
2010
2046
|
this.gridApi.core.raise.benchmarkComplete(result);
|
|
2011
2047
|
return result;
|
|
@@ -2074,18 +2110,21 @@ export class VanillaGridController {
|
|
|
2074
2110
|
// present, open the generated PDF the same way the old module did;
|
|
2075
2111
|
// when missing, just return the doc definition so the caller can
|
|
2076
2112
|
// render it themselves. This mirrors `uiGridExporterService.pdfExport`.
|
|
2077
|
-
const win =
|
|
2078
|
-
|
|
2079
|
-
|
|
2113
|
+
const win =
|
|
2114
|
+
typeof window !== 'undefined'
|
|
2115
|
+
? (window as typeof window & {
|
|
2116
|
+
pdfMake?: {
|
|
2117
|
+
createPdf: (doc: GridExporterPdfDocDefinition) => {
|
|
2118
|
+
open: () => void;
|
|
2119
|
+
download: (filename: string) => void;
|
|
2120
|
+
};
|
|
2121
|
+
};
|
|
2122
|
+
})
|
|
2123
|
+
: undefined;
|
|
2080
2124
|
if (win?.pdfMake) {
|
|
2081
2125
|
const resolved = this.resolveExporterOptions();
|
|
2082
2126
|
const pdfOpts = resolveGridExporterPdfOptions(this.options);
|
|
2083
|
-
const filename = resolveExporterFilename(
|
|
2084
|
-
pdfOpts.filename,
|
|
2085
|
-
'download.pdf',
|
|
2086
|
-
rowType,
|
|
2087
|
-
colType,
|
|
2088
|
-
);
|
|
2127
|
+
const filename = resolveExporterFilename(pdfOpts.filename, 'download.pdf', rowType, colType);
|
|
2089
2128
|
const pdf = win.pdfMake.createPdf(doc);
|
|
2090
2129
|
// Prefer download when the consumer supplied a filename; otherwise
|
|
2091
2130
|
// fall back to .open() which pops the pdf in a new browser tab.
|
|
@@ -2104,7 +2143,12 @@ export class VanillaGridController {
|
|
|
2104
2143
|
): GridExporterExcelSheetData {
|
|
2105
2144
|
const exporterOptions = this.resolveExporterOptions();
|
|
2106
2145
|
const columns = colType === 'all' ? this.options.columnDefs : this.visibleColumns;
|
|
2107
|
-
return buildGridExcelSheetData(
|
|
2146
|
+
return buildGridExcelSheetData(
|
|
2147
|
+
columns,
|
|
2148
|
+
this.exporterRowsFor(rowType),
|
|
2149
|
+
exporterOptions,
|
|
2150
|
+
colType,
|
|
2151
|
+
);
|
|
2108
2152
|
}
|
|
2109
2153
|
|
|
2110
2154
|
private exportExcel(
|
|
@@ -2115,21 +2159,27 @@ export class VanillaGridController {
|
|
|
2115
2159
|
// ExcelBuilder is an optional global (consumers load it separately).
|
|
2116
2160
|
// When present, produce + download the xlsx the same way the old module
|
|
2117
2161
|
// did; when missing, just return the raw sheet data.
|
|
2118
|
-
const win =
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2162
|
+
const win =
|
|
2163
|
+
typeof window !== 'undefined'
|
|
2164
|
+
? (window as typeof window & {
|
|
2165
|
+
ExcelBuilder?: {
|
|
2166
|
+
Worksheet: new (config: { name: string }) => { setData: (data: unknown) => void };
|
|
2167
|
+
Workbook: new () => { addWorksheet: (sheet: unknown) => void };
|
|
2168
|
+
Builder: {
|
|
2169
|
+
createFile: (workbook: unknown, options: { type: 'blob' }) => Promise<Blob>;
|
|
2170
|
+
};
|
|
2171
|
+
};
|
|
2172
|
+
})
|
|
2173
|
+
: undefined;
|
|
2125
2174
|
const ExcelBuilder = win?.ExcelBuilder;
|
|
2126
2175
|
if (!ExcelBuilder) {
|
|
2127
2176
|
return sheetData;
|
|
2128
2177
|
}
|
|
2129
2178
|
const excelOptions = resolveGridExporterExcelOptions(this.options);
|
|
2130
|
-
const sheetName =
|
|
2131
|
-
|
|
2132
|
-
|
|
2179
|
+
const sheetName =
|
|
2180
|
+
typeof excelOptions.sheetName === 'function'
|
|
2181
|
+
? excelOptions.sheetName(rowType, colType)
|
|
2182
|
+
: (excelOptions.sheetName ?? 'Sheet1');
|
|
2133
2183
|
const filename = sanitizeDownloadFilename(
|
|
2134
2184
|
resolveExporterFilename(excelOptions.filename, 'download.xlsx', rowType, colType),
|
|
2135
2185
|
);
|
|
@@ -2186,11 +2236,7 @@ export class VanillaGridController {
|
|
|
2186
2236
|
}
|
|
2187
2237
|
|
|
2188
2238
|
private async validateGetInvalidRows(): Promise<GridRecord[]> {
|
|
2189
|
-
return validateAllGridRows(
|
|
2190
|
-
this.options.data,
|
|
2191
|
-
this.options.columnDefs,
|
|
2192
|
-
this.validatorRegistry,
|
|
2193
|
-
);
|
|
2239
|
+
return validateAllGridRows(this.options.data, this.options.columnDefs, this.validatorRegistry);
|
|
2194
2240
|
}
|
|
2195
2241
|
|
|
2196
2242
|
/** Test hook — exposes the validator registry so consumers can inspect or
|
|
@@ -2247,7 +2293,12 @@ export class VanillaGridController {
|
|
|
2247
2293
|
// the caller didn't force JSON, fall through to CSV. The old module
|
|
2248
2294
|
// decided purely based on MIME type; `importText` is a newer entry
|
|
2249
2295
|
// point we provide for drag/drop flows.
|
|
2250
|
-
const asJson =
|
|
2296
|
+
const asJson =
|
|
2297
|
+
type === 'csv'
|
|
2298
|
+
? null
|
|
2299
|
+
: parseGridImporterJson(text, {
|
|
2300
|
+
errorCallback: type === 'json' ? opts.errorCallback : undefined,
|
|
2301
|
+
});
|
|
2251
2302
|
if (asJson && asJson.length > 0) {
|
|
2252
2303
|
const objects = buildGridImporterObjectsFromJson(asJson, opts);
|
|
2253
2304
|
this.importerDispatchObjects(objects);
|
|
@@ -2320,11 +2371,9 @@ export class VanillaGridController {
|
|
|
2320
2371
|
/** Menu items the importer contributes. Just the single "Import"
|
|
2321
2372
|
* entry when the feature is enabled; returns an empty array otherwise. */
|
|
2322
2373
|
private buildImporterMenuItems(): GridMenuItem[] {
|
|
2323
|
-
return buildGridImporterMenuItems(
|
|
2324
|
-
this.
|
|
2325
|
-
|
|
2326
|
-
{ importAFile: () => this.importerRequestFile() },
|
|
2327
|
-
);
|
|
2374
|
+
return buildGridImporterMenuItems(this.options, this.labels, {
|
|
2375
|
+
importAFile: () => this.importerRequestFile(),
|
|
2376
|
+
});
|
|
2328
2377
|
}
|
|
2329
2378
|
|
|
2330
2379
|
/** Menu items row-edit contributes: "Save changes" + "Retry errored
|
|
@@ -2354,15 +2403,11 @@ export class VanillaGridController {
|
|
|
2354
2403
|
}
|
|
2355
2404
|
|
|
2356
2405
|
private rowEditGetDirtyRows(): GridRow[] {
|
|
2357
|
-
return this.pipeline.visibleRows.filter((row) =>
|
|
2358
|
-
this.rowEditState.dirtyRowIds.has(row.id),
|
|
2359
|
-
);
|
|
2406
|
+
return this.pipeline.visibleRows.filter((row) => this.rowEditState.dirtyRowIds.has(row.id));
|
|
2360
2407
|
}
|
|
2361
2408
|
|
|
2362
2409
|
private rowEditGetErrorRows(): GridRow[] {
|
|
2363
|
-
return this.pipeline.visibleRows.filter((row) =>
|
|
2364
|
-
this.rowEditState.errorRowIds.has(row.id),
|
|
2365
|
-
);
|
|
2410
|
+
return this.pipeline.visibleRows.filter((row) => this.rowEditState.errorRowIds.has(row.id));
|
|
2366
2411
|
}
|
|
2367
2412
|
|
|
2368
2413
|
private rowEditFlushDirtyRows(): Promise<void> {
|
|
@@ -2437,8 +2482,7 @@ export class VanillaGridController {
|
|
|
2437
2482
|
markGridRowSaving(this.rowEditState, row);
|
|
2438
2483
|
this.rowEditSavePromiseOverrides.delete(row.id);
|
|
2439
2484
|
this.gridApi.rowEdit.raise.saveRow(row.entity);
|
|
2440
|
-
const savePromise =
|
|
2441
|
-
this.rowEditSavePromiseOverrides.get(row.id) ?? Promise.resolve();
|
|
2485
|
+
const savePromise = this.rowEditSavePromiseOverrides.get(row.id) ?? Promise.resolve();
|
|
2442
2486
|
this.rowEditState.savePromises.set(row.id, savePromise);
|
|
2443
2487
|
this.emit();
|
|
2444
2488
|
return savePromise
|
|
@@ -1,12 +1,12 @@
|
|
|
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,
|
|
6
7
|
clearRustWasmGridEngine,
|
|
7
8
|
} from '@ornery/ui-grid-core';
|
|
8
9
|
import {
|
|
9
|
-
defineStandaloneUiGridElement,
|
|
10
10
|
mountVanillaUiGrid,
|
|
11
11
|
registerVanillaUiGridRustModule,
|
|
12
12
|
type GridOptions,
|
|
@@ -143,18 +143,11 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
143
143
|
it('registers Rust/WASM bindings through the vanilla API', async () => {
|
|
144
144
|
const module: UiGridRustWebModule = {
|
|
145
145
|
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
146
|
};
|
|
154
147
|
|
|
155
148
|
await registerVanillaUiGridRustModule(module);
|
|
156
149
|
expect(module.default).toHaveBeenCalledTimes(1);
|
|
157
|
-
expect(activeGridEngineBackend()).toBe('
|
|
150
|
+
expect(activeGridEngineBackend()).toBe('typescript');
|
|
158
151
|
});
|
|
159
152
|
|
|
160
153
|
it('controller API exposes subscribe and action methods', () => {
|
|
@@ -184,6 +177,31 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
184
177
|
]);
|
|
185
178
|
});
|
|
186
179
|
|
|
180
|
+
it('does not auto-enable the wasm engine during controller refreshes', () => {
|
|
181
|
+
const enableSpy = vi.spyOn(core, 'enableUiGridWasmEngine');
|
|
182
|
+
|
|
183
|
+
const controller = createVanillaGridController({
|
|
184
|
+
id: 'controller-wasm-bootstrap',
|
|
185
|
+
data: [
|
|
186
|
+
{ id: 'r1', name: 'Gamma' },
|
|
187
|
+
{ id: 'r2', name: 'Alpha' },
|
|
188
|
+
],
|
|
189
|
+
columnDefs: [{ name: 'name' }],
|
|
190
|
+
enableSorting: true,
|
|
191
|
+
enableFiltering: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(enableSpy).not.toHaveBeenCalled();
|
|
195
|
+
expect(activeGridEngineBackend()).toBe('typescript');
|
|
196
|
+
|
|
197
|
+
controller.setFilter('name', 'Alpha');
|
|
198
|
+
controller.sortColumn('name', SORT_DIRECTIONS.asc);
|
|
199
|
+
|
|
200
|
+
expect(enableSpy).not.toHaveBeenCalled();
|
|
201
|
+
expect(controller.getSnapshot().pipeline.pipelineMs).not.toBe(123);
|
|
202
|
+
enableSpy.mockRestore();
|
|
203
|
+
});
|
|
204
|
+
|
|
187
205
|
it('applies overridable SVG icons for controls', async () => {
|
|
188
206
|
const target = document.getElementById('app');
|
|
189
207
|
if (!target) {
|
|
@@ -304,5 +322,4 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
304
322
|
expect((grid.options.data as Array<{ id: string; name: string }>)[0].name).toBe('Gamma');
|
|
305
323
|
expect(grid.options.minRowsToShow).toBe(20);
|
|
306
324
|
});
|
|
307
|
-
|
|
308
325
|
});
|
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`,
|