@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.
@@ -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(columns: readonly GridColumnDef[], viewportWidth?: number): string {
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((col) => col.pinnedLeft === true || col.pinnedRight === true);
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: () => ({ ...this.exporterOverrides, ...resolveGridExporterOptions(this.options) }),
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 = !prev || prev.length !== incoming.length || prev.some((col, i) => col !== incoming[i]);
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 = options.infiniteScrollUp !== undefined && options.infiniteScrollUp !== this.lastOptionsInfiniteScrollUp;
655
- const downChanged = options.infiniteScrollDown !== undefined && options.infiniteScrollDown !== this.lastOptionsInfiniteScrollDown;
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 = null;
1392
- setCellNavScrollHandler(handler: ((rowId: string | null, columnName: string | null) => void) | null): void {
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
- this.viewportWidth = Math.round(width);
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
- // kept as an opt-flag for forward compat.
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) this.columnWidthOverrides = { ...state.columnWidthOverrides };
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
- const loops = Math.max(1, iterations ?? this.options.benchmark?.iterations ?? 25);
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
- const elapsedMs = now() - started;
2002
- const result = {
2003
- iterations: loops,
2004
- totalMs: elapsedMs,
2005
- averageMs: elapsedMs / loops,
2006
- visibleRows: lastResult.visibleRows.length,
2007
- renderedItems: lastResult.displayItems.length,
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 = typeof window !== 'undefined' ? (window as typeof window & {
2078
- pdfMake?: { createPdf: (doc: GridExporterPdfDocDefinition) => { open: () => void; download: (filename: string) => void } };
2079
- }) : undefined;
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(columns, this.exporterRowsFor(rowType), exporterOptions, colType);
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 = typeof window !== 'undefined' ? (window as typeof window & {
2119
- ExcelBuilder?: {
2120
- Worksheet: new (config: { name: string }) => { setData: (data: unknown) => void };
2121
- Workbook: new () => { addWorksheet: (sheet: unknown) => void };
2122
- Builder: { createFile: (workbook: unknown, options: { type: 'blob' }) => Promise<Blob> };
2123
- };
2124
- }) : undefined;
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 = typeof excelOptions.sheetName === 'function'
2131
- ? excelOptions.sheetName(rowType, colType)
2132
- : (excelOptions.sheetName ?? 'Sheet1');
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 = type === 'csv' ? null : parseGridImporterJson(text, { errorCallback: type === 'json' ? opts.errorCallback : undefined });
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.options,
2325
- this.labels,
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('rust-wasm');
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 { registerRustWasmGridEngine } from '@ornery/ui-grid-core';
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
- await registerVanillaUiGridRustModule(rustModule);
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, options } = await mountGrid();
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`,