@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.
@@ -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(columns: readonly GridColumnDef[], viewportWidth?: number): string {
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((col) => col.pinnedLeft === true || col.pinnedRight === true);
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: () => ({ ...this.exporterOverrides, ...resolveGridExporterOptions(this.options) }),
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 = !prev || prev.length !== incoming.length || prev.some((col, i) => col !== incoming[i]);
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 = options.infiniteScrollUp !== undefined && options.infiniteScrollUp !== this.lastOptionsInfiniteScrollUp;
655
- const downChanged = options.infiniteScrollDown !== undefined && options.infiniteScrollDown !== this.lastOptionsInfiniteScrollDown;
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 = null;
1392
- setCellNavScrollHandler(handler: ((rowId: string | null, columnName: string | null) => void) | null): void {
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
- this.viewportWidth = Math.round(width);
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
- // kept as an opt-flag for forward compat.
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) this.columnWidthOverrides = { ...state.columnWidthOverrides };
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
- 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
- }
2010
+ let result: GridBenchmarkResult;
2000
2011
 
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
- };
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 = typeof window !== 'undefined' ? (window as typeof window & {
2078
- pdfMake?: { createPdf: (doc: GridExporterPdfDocDefinition) => { open: () => void; download: (filename: string) => void } };
2079
- }) : undefined;
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(columns, this.exporterRowsFor(rowType), exporterOptions, colType);
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 = 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;
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 = typeof excelOptions.sheetName === 'function'
2131
- ? excelOptions.sheetName(rowType, colType)
2132
- : (excelOptions.sheetName ?? 'Sheet1');
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 = type === 'csv' ? null : parseGridImporterJson(text, { errorCallback: type === 'json' ? opts.errorCallback : undefined });
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.options,
2325
- this.labels,
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('rust-wasm');
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 { 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`,