@svgrid/grid 3.0.7 → 3.0.9

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.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/dist/GridMenus.svelte +3 -2
  3. package/dist/SvGrid.controller.svelte.d.ts +60 -8
  4. package/dist/SvGrid.controller.svelte.js +490 -114
  5. package/dist/SvGrid.css +186 -3
  6. package/dist/SvGrid.svelte +285 -164
  7. package/dist/SvGrid.types.d.ts +24 -7
  8. package/dist/cdn/{GridMenus-CcwU8I4t.js → GridMenus-B1sipj6y.js} +165 -165
  9. package/dist/cdn/{GridMenus-BgUS-W3o.js → GridMenus-DlAzfHLk.js} +201 -201
  10. package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
  11. package/dist/cdn/{src-kE9B98r6.js → src-BYsa66I0.js} +9747 -10178
  12. package/dist/cdn/{src-25JmJ_7g.js → src-pA4RroLV.js} +10709 -11140
  13. package/dist/cdn/svgrid.js +27 -27
  14. package/dist/cdn/svgrid.svelte-external.js +27 -27
  15. package/dist/cdn/validate-XuMe4_KR.js +80 -0
  16. package/dist/cell-render.js +25 -3
  17. package/dist/columns.js +16 -1
  18. package/dist/conditional-formatting.js +9 -2
  19. package/dist/core.d.ts +93 -0
  20. package/dist/core.js +313 -47
  21. package/dist/filtering/excel-filters.js +41 -6
  22. package/dist/headless.d.ts +1 -1
  23. package/dist/headless.js +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +2 -1
  26. package/dist/keyboard-handlers.js +5 -1
  27. package/dist/selection.d.ts +2 -2
  28. package/dist/selection.js +46 -12
  29. package/dist/server-block-cache.d.ts +7 -32
  30. package/dist/server-block-cache.js +21 -12
  31. package/dist/server-data-source.js +3 -1
  32. package/dist/server.d.ts +1 -0
  33. package/dist/server.js +1 -0
  34. package/dist/spreadsheet.js +7 -0
  35. package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
  36. package/dist/validate.js +13 -14
  37. package/dist/virtualization/column-virtualizer.d.ts +4 -0
  38. package/dist/virtualization/column-virtualizer.js +2 -0
  39. package/dist/virtualization/types.d.ts +20 -0
  40. package/dist/virtualization/virtualizer.js +90 -24
  41. package/dist/windowed-brand.d.ts +22 -0
  42. package/dist/windowed-brand.js +60 -0
  43. package/dist/windowed-data.d.ts +6 -0
  44. package/dist/windowed-data.js +34 -0
  45. package/dist/windowed-row-model.d.ts +1 -0
  46. package/dist/windowed-row-model.js +97 -0
  47. package/package.json +1 -1
  48. package/src/GridMenus.svelte +3 -2
  49. package/src/SvGrid.controller.svelte.ts +483 -122
  50. package/src/SvGrid.css +186 -3
  51. package/src/SvGrid.svelte +285 -164
  52. package/src/SvGrid.types.ts +24 -7
  53. package/src/cell-render.ts +24 -2
  54. package/src/columns.ts +16 -1
  55. package/src/conditional-formatting.ts +6 -4
  56. package/src/core.sort.test.ts +61 -0
  57. package/src/core.ts +309 -49
  58. package/src/counting-cell.test.svelte +11 -0
  59. package/src/filtering/excel-filters.ts +39 -6
  60. package/src/headless.reactivity.svelte.test.ts +89 -0
  61. package/src/headless.ts +1 -1
  62. package/src/index.ts +7 -1
  63. package/src/keyboard-handlers.ts +5 -1
  64. package/src/new-features.test.ts +24 -0
  65. package/src/selection.test.ts +51 -0
  66. package/src/selection.ts +47 -14
  67. package/src/server-block-cache.ts +27 -10
  68. package/src/server-data-source.ts +3 -1
  69. package/src/server.ts +6 -0
  70. package/src/spreadsheet.ts +6 -0
  71. package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
  72. package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
  73. package/src/svgrid.behavior.test.ts +58 -0
  74. package/src/svgrid.column-cell-recycling.test.ts +120 -0
  75. package/src/svgrid.interaction.test.ts +4 -1
  76. package/src/svgrid.new-features.wrapper.test.ts +5 -4
  77. package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
  78. package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
  79. package/src/validate.test.ts +20 -4
  80. package/src/validate.ts +13 -16
  81. package/src/virtualization/column-virtualizer.ts +6 -0
  82. package/src/virtualization/types.ts +20 -0
  83. package/src/virtualization/virtualizer.test.ts +152 -0
  84. package/src/virtualization/virtualizer.ts +93 -27
  85. package/src/windowed-brand.ts +67 -0
  86. package/src/windowed-data.test.ts +105 -0
  87. package/src/windowed-data.ts +36 -0
  88. package/src/windowed-row-model.ts +113 -0
  89. package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
  90. package/dist/cdn/validate-AZoD8BoR.js +0 -80
  91. /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
@@ -151,6 +151,41 @@ function scanInTokens(value) {
151
151
  // pull in SvGrid.svelte, which is a runtime cost (and a bundler hazard) for
152
152
  // anything that only wants operator semantics.
153
153
  export { ALL_FILTER_OPERATORS, SET_OPERATOR_IDS, VALUELESS_OPERATOR_IDS, RANGE_OPERATOR_IDS, isSetOperator, isValuelessOperator, isRangeOperator, } from './filter-operator-catalogue.js';
154
+ /**
155
+ * A text test, remembered per distinct cell text.
156
+ *
157
+ * The text operators fold every cell (lowercase, and for non-ASCII text NFD
158
+ * plus diacritic stripping) before comparing, and a column the user filters
159
+ * is usually a category - region, status, owner - with a handful of distinct
160
+ * values over every row. Folding each row again was most of a 1M-row filter
161
+ * (417 ms). The answer depends only on the cell's text, so it is kept per
162
+ * text. A column that turns out mostly unique (more than half of the first
163
+ * 2,048 lookups missed) stops remembering and tests directly, and the memo
164
+ * never grows past MEMO_CAP entries.
165
+ */
166
+ const MEMO_CAP = 16384;
167
+ function memoText(test) {
168
+ let memo = new Map();
169
+ let lookups = 0;
170
+ let hits = 0;
171
+ return (cellValue) => {
172
+ const s = typeof cellValue === 'string' ? cellValue : String(cellValue ?? '');
173
+ if (memo === null)
174
+ return test(s);
175
+ lookups++;
176
+ const known = memo.get(s);
177
+ if (known !== undefined) {
178
+ hits++;
179
+ return known;
180
+ }
181
+ const result = test(s);
182
+ if (memo.size < MEMO_CAP)
183
+ memo.set(s, result);
184
+ if (lookups >= 2048 && hits * 2 < lookups)
185
+ memo = null;
186
+ return result;
187
+ };
188
+ }
154
189
  /**
155
190
  * Compile a filter once, then test many rows against it.
156
191
  *
@@ -168,23 +203,23 @@ export function compileExcelFilter(filter, options) {
168
203
  const needle = fold(filter.value);
169
204
  switch (filter.operator) {
170
205
  case 'contains':
171
- return (cellValue) => fold(cellValue).includes(needle);
206
+ return memoText((s) => fold(s).includes(needle));
172
207
  case 'notContains':
173
208
  // An empty needle is no constraint (mirrors `contains` returning true),
174
209
  // so nothing is excluded until the user types something.
175
210
  if (needle === '')
176
211
  return () => true;
177
- return (cellValue) => !fold(cellValue).includes(needle);
212
+ return memoText((s) => !fold(s).includes(needle));
178
213
  case 'equals':
179
- return (cellValue) => fold(cellValue) === needle;
214
+ return memoText((s) => fold(s) === needle);
180
215
  case 'notEquals':
181
216
  if (needle === '')
182
217
  return () => true;
183
- return (cellValue) => fold(cellValue) !== needle;
218
+ return memoText((s) => fold(s) !== needle);
184
219
  case 'startsWith':
185
- return (cellValue) => fold(cellValue).startsWith(needle);
220
+ return memoText((s) => fold(s).startsWith(needle));
186
221
  case 'endsWith':
187
- return (cellValue) => fold(cellValue).endsWith(needle);
222
+ return memoText((s) => fold(s).endsWith(needle));
188
223
  case 'regex': {
189
224
  // Case-insensitive by default (matches the accent/case-folded feel of
190
225
  // the other text operators). An invalid pattern matches nothing rather
@@ -26,7 +26,7 @@
26
26
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, createSvGridCore, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type ColumnFilter, type ColumnFiltersState, type ExpandedState, type GridColumnDef, type GridColumns, type GroupAggregator, type GroupingState, type Header, type HeaderContext, type HeaderGroup, type PaginationState, type Row, type RowData, type RowModel, type RowModelFactory, type RowSelectionState, type SortingState, type Store, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core.js';
27
27
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
29
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
30
30
  export { createVirtualizer } from './virtualization/virtualizer.js';
31
31
  export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
32
32
  export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
package/dist/headless.js CHANGED
@@ -26,7 +26,7 @@
26
26
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, createSvGridCore, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core.js';
27
27
  export { createGrid, createSvGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
29
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
30
30
  // The virtualizers are pure window arithmetic (scroll offset in, visible
31
31
  // range out) with no DOM of their own, and a custom renderer over 50k rows
32
32
  // needs one. They were only on the barrel, so the Headless demos imported
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt,
3
3
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type GridColumnDef, type GridColumns, type GroupAggregator, type Header, type HeaderContext, type HeaderGroup, type Row, type RowData, type SortingState, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core.js';
4
4
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
5
5
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
6
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
6
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
7
7
  export { default as SvGrid } from './SvGrid.svelte';
8
8
  export type { BoardConfig, BoardLane, BoardCardMoveEvent, BoardCardCommitEvent, BoardLayout, BoardSubtask, BoardComment, BoardMenuContext, BoardLaneMenuContext, BoardDrawerConfig } from './SvGrid.types.js';
9
9
  export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte.js';
@@ -215,6 +215,7 @@ export { createHyperFormulaSheet, type HyperFormulaSheet, type HyperFormulaSheet
215
215
  export { createCollaboration, broadcastChannelTransport, type CollabUser, type CollabCell, type CollabPresence, type CollabMessage, type CollabTransport, type Collaboration, } from './collaboration.js';
216
216
  export { createServerDataSource, type ServerDataSource, type ServerSelectionRule, type ServerRequest, type ServerResult, type ServerController, type ServerControllerOptions, type ServerState, type ServerSortModel, type ServerFilterModel, type ServerAggFn, type ServerAggregation, type ServerGroupRow, type ServerLeafRow, type ServerMoreRow, type ServerFooterRow, type ServerSkeletonRow, type ServerGrandTotalRow, type ServerPlaceholderRow, type ServerDisplayRow, type ServerDetailRow, } from './server-data-source.js';
217
217
  export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
218
+ export { createWindowedData, windowedSourceOf, isWindowedData, type WindowedSource, } from './windowed-data.js';
218
219
  export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } from './row-model.js';
219
220
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, type SavedView, type ViewStorage, type NamedViews, type AutoSavedViewOptions, } from './named-views.js';
220
221
  export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, type ConditionalFormat, type ConditionalFormatSpec, type ColorScaleFormat, type ColorScaleStop, type ScaleBounds, type DataBarFormat, type IconSetFormat, type RuleFormat, type ResolvedCellFormat, } from './conditional-formatting.js';
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt,
3
3
  export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core.js';
4
4
  export { createGrid, createSvGrid } from './createGrid.svelte.js';
5
5
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
6
- export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
6
+ export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
7
7
  export { default as SvGrid } from './SvGrid.svelte';
8
8
  export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte.js';
9
9
  // Built-in AI helpers (free): natural-language filter, smart-fill, summarize,
@@ -255,6 +255,7 @@ export { createHyperFormulaSheet, } from './hyperformula-adapter.js';
255
255
  export { createCollaboration, broadcastChannelTransport, } from './collaboration.js';
256
256
  export { createServerDataSource, } from './server-data-source.js';
257
257
  export { createBlockCache, createRowPlaceholder, rowPlaceholderState, } from './server-block-cache.js';
258
+ export { createWindowedData, windowedSourceOf, isWindowedData, } from './windowed-data.js';
258
259
  export { toServerFilterColumns, } from './row-model.js';
259
260
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, } from './named-views.js';
260
261
  export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, } from './conditional-formatting.js';
@@ -189,7 +189,11 @@ export function createKeyboard(ctx) {
189
189
  }
190
190
  }
191
191
  if (column?.columnDef.editorType === "checkbox") {
192
- ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
192
+ // Same rule as a click: `editable: false` (or an editable callback
193
+ // saying no) keeps the checkbox read-only.
194
+ if (ctx.isCellEditableAt?.(current.rowIndex, current.colIndex) ?? true) {
195
+ ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
196
+ }
193
197
  return;
194
198
  }
195
199
  ctx.onCellDoubleClick(current.rowIndex, current.colIndex);
@@ -1,8 +1,8 @@
1
1
  import { type RowData, type TableFeatures } from "./index.js";
2
2
  import "./sv-grid-scrollbar.js";
3
3
  export declare function createSelection<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(ctx: any): {
4
- isRowSelected: (rowId: string) => any;
5
- toggleRowSelectionById: (rowId: string) => void;
4
+ isRowSelected: (rowId: string, original?: unknown) => any;
5
+ toggleRowSelectionById: (rowId: string, original?: unknown) => void;
6
6
  toggleSelectAllRows: () => void;
7
7
  setSelectAllRows: (select: boolean) => void;
8
8
  setActiveCell: (rowIndex: number, colIndex: number) => void;
package/dist/selection.js CHANGED
@@ -6,21 +6,33 @@ import "./sv-grid-scrollbar.js";
6
6
  import { getColumnBaseValue, isGroupRow, } from "./cell-values.js";
7
7
  import { expandRectToMerges, originOf, endOf } from "./merges.js";
8
8
  export function createSelection(ctx) {
9
- function isRowSelected(rowId) {
9
+ /**
10
+ * A selection model needs the row's data. The body passes the row it is
11
+ * drawing as `original`; without it the row is looked up by id, a scan of
12
+ * every row - on a server model's windowed million rows, once per drawn
13
+ * row, that scan held the page for seconds after a sort.
14
+ */
15
+ function rowOriginal(rowId) {
16
+ const row = ctx.allRows.find((r) => r.id === rowId);
17
+ return row ? { found: true, original: row.original } : { found: false };
18
+ }
19
+ function isRowSelected(rowId, original) {
10
20
  const model = ctx.props.rowSelectionModel;
11
21
  if (model) {
12
- const row = ctx.allRows.find((r) => r.id === rowId);
13
- return row ? model.isSelected(rowId, row.original) : false;
22
+ if (original !== undefined)
23
+ return model.isSelected(rowId, original);
24
+ const hit = rowOriginal(rowId);
25
+ return hit.found ? model.isSelected(rowId, hit.original) : false;
14
26
  }
15
27
  return Boolean(ctx.rowSelectionState[rowId]);
16
28
  }
17
- function toggleRowSelectionById(rowId) {
29
+ function toggleRowSelectionById(rowId, original) {
18
30
  const model = ctx.props.rowSelectionModel;
19
31
  if (model) {
20
- const row = ctx.allRows.find((r) => r.id === rowId);
21
- if (!row)
32
+ const hit = original !== undefined ? { found: true, original } : rowOriginal(rowId);
33
+ if (!hit.found)
22
34
  return;
23
- model.toggle(rowId, row.original, !model.isSelected(rowId, row.original));
35
+ model.toggle(rowId, hit.original, !model.isSelected(rowId, hit.original));
24
36
  return;
25
37
  }
26
38
  ctx.grid.setRowSelection((prev) => ({ ...prev, [rowId]: !prev[rowId] }));
@@ -446,8 +458,20 @@ export function createSelection(ctx) {
446
458
  }
447
459
  /** Look up a column by id without depending on `buildApi`'s private
448
460
  * closure (those helpers don't exist at this scope). */
461
+ // id -> column, rebuilt when the column list changes. Called per header
462
+ // cell as columns scroll into view, so a linear `find` made every scroll
463
+ // frame cost more the more columns the grid has.
464
+ let columnsById = new Map();
465
+ let columnsByIdFor = null;
449
466
  function findColumnById(columnId) {
450
- return ctx.allColumns.find((c) => c.id === columnId);
467
+ const all = ctx.allColumns;
468
+ if (all !== columnsByIdFor) {
469
+ columnsById = new Map();
470
+ for (const c of all)
471
+ columnsById.set(c.id, c);
472
+ columnsByIdFor = all;
473
+ }
474
+ return columnsById.get(columnId);
451
475
  }
452
476
  function onCellPointerDown(rowIndex, colIndex, event) {
453
477
  if (event.button !== 0)
@@ -622,10 +646,20 @@ export function createSelection(ctx) {
622
646
  columnId: column.id,
623
647
  row: row.original,
624
648
  });
625
- const isCheckboxColumn = column.columnDef.editorType === "checkbox" ||
626
- typeof cellValue === "boolean";
627
- if (isCheckboxColumn) {
628
- ctx.toggleBooleanCell(rowIndex, colIndex);
649
+ // A declared checkbox editor toggles on click unless the cell is not
650
+ // editable. A value that only HAPPENS to be boolean is drawn as a read-only
651
+ // checkbox, so it toggles only when inline editing is on - it used to flip
652
+ // and write the row on any click, in grids with editing switched off. A
653
+ // column with its own `cell` renderer draws (and handles) the value itself.
654
+ const declaredCheckbox = column.columnDef.editorType === "checkbox";
655
+ const inferredCheckbox = !declaredCheckbox &&
656
+ typeof cellValue === "boolean" &&
657
+ column.columnDef.cell == null;
658
+ if (declaredCheckbox || inferredCheckbox) {
659
+ const editable = ctx.isCellEditableAt?.(rowIndex, colIndex) ?? true;
660
+ if (editable && (declaredCheckbox || ctx.editingEnabled)) {
661
+ ctx.toggleBooleanCell(rowIndex, colIndex);
662
+ }
629
663
  setActiveCell(rowIndex, colIndex);
630
664
  setSelection(rowIndex, colIndex);
631
665
  ctx.editingCell = null;
@@ -1,35 +1,3 @@
1
- /**
2
- * Block cache for server-backed rows - the engine behind infinite scrolling.
3
- *
4
- * Paging asks for "page 3". Infinite scrolling asks a different question: the
5
- * user is looking at rows 4,000 to 4,020 of a table with a million rows, so
6
- * fetch the block that covers them, keep a bounded number of recently-seen
7
- * blocks, and render something sensible for every row nobody has fetched yet.
8
- * That is all this module does, in plain TypeScript with no Svelte and no DOM.
9
- *
10
- * The shape of the problem (and most of the vocabulary) is the same one every
11
- * grid with a server row model solves, so the options here are deliberately
12
- * recognisable: `blockSize`, `maxBlocksInCache`, `maxConcurrentRequests`,
13
- * `blockLoadDebounceMs`, `initialRowCount`, `overflowRows`.
14
- *
15
- * Three things it takes seriously:
16
- *
17
- * - **Unloaded rows still have to render.** `rows()` always returns a dense
18
- * array, with a shared sentinel object in every slot that has no data. The
19
- * grid recognises those (see {@link rowPlaceholderState}) and draws a
20
- * skeleton or a retry affordance instead of an empty row, so the scrollbar
21
- * never lies and scrolling never leaves a hole.
22
- * - **The row count may be unknown.** A backend that cannot cheaply count
23
- * says so (omit `rowCount`, or send `-1`) and the list grows by a block at
24
- * a time until a short block proves where the end is.
25
- * - **Requests are not free.** In-flight blocks are deduplicated, at most
26
- * `maxConcurrentRequests` are open at once, a fast scroll debounces rather
27
- * than firing a request per frame, and blocks abandoned by eviction or
28
- * purge are aborted.
29
- *
30
- * Enterprise's server row model runs one of these per group level; the free
31
- * flat controller runs exactly one.
32
- */
33
1
  /**
34
2
  * The mark that says "this row is not data yet".
35
3
  *
@@ -155,6 +123,13 @@ export type BlockCache<TData> = {
155
123
  setViewport(startIndex: number, endIndex: number): void;
156
124
  /** The dense row array to hand the grid. Placeholders fill unloaded slots. */
157
125
  rows(): ReadonlyArray<TData>;
126
+ /**
127
+ * The same rows as windowed data (see windowed-data.ts): `length` is the
128
+ * row count and each entry is read through `getRow` when the grid asks for
129
+ * it, so handing it over costs O(1) whatever the count. A new array per
130
+ * change, like `rows()`.
131
+ */
132
+ windowedRows(): ReadonlyArray<TData>;
158
133
  /** One row, without building the array. Returns a placeholder when unloaded. */
159
134
  getRow(index: number): TData | typeof LOADING_ROW | typeof FAILED_ROW;
160
135
  rowCount(): number | null;
@@ -30,6 +30,7 @@
30
30
  * Enterprise's server row model runs one of these per group level; the free
31
31
  * flat controller runs exactly one.
32
32
  */
33
+ import { createWindowedData } from './windowed-data.js';
33
34
  /**
34
35
  * The mark that says "this row is not data yet".
35
36
  *
@@ -99,6 +100,7 @@ export function createBlockCache(options) {
99
100
  let debounceTimer = null;
100
101
  let emitScheduled = false;
101
102
  let rowsCache = null;
103
+ let windowedCache = null;
102
104
  const blockOf = (rowIndex) => Math.floor(rowIndex / blockSize);
103
105
  const startOf = (blockIndex) => blockIndex * blockSize;
104
106
  /**
@@ -156,7 +158,7 @@ export function createBlockCache(options) {
156
158
  * mutations notifies once. Every mutating path ends here.
157
159
  */
158
160
  function emit() {
159
- rowsCache = null; // rebuilt lazily, on the next read
161
+ rowsCache = windowedCache = null; // rebuilt lazily, on the next read
160
162
  if (disposed || emitScheduled)
161
163
  return;
162
164
  emitScheduled = true;
@@ -281,6 +283,15 @@ export function createBlockCache(options) {
281
283
  ensureBlock(i);
282
284
  }
283
285
  }
286
+ function rowAt(index) {
287
+ const block = blocks.get(blockOf(index));
288
+ if (!block)
289
+ return LOADING_ROW;
290
+ if (block.status === 'failed')
291
+ return FAILED_ROW;
292
+ const row = block.rows[index - startOf(blockOf(index))];
293
+ return row ?? LOADING_ROW;
294
+ }
284
295
  function abortAll() {
285
296
  for (const block of blocks.values())
286
297
  block.controller?.abort();
@@ -330,15 +341,13 @@ export function createBlockCache(options) {
330
341
  rowsCache = out;
331
342
  return out;
332
343
  },
333
- getRow(index) {
334
- const block = blocks.get(blockOf(index));
335
- if (!block)
336
- return LOADING_ROW;
337
- if (block.status === 'failed')
338
- return FAILED_ROW;
339
- const row = block.rows[index - startOf(blockOf(index))];
340
- return row ?? LOADING_ROW;
344
+ windowedRows() {
345
+ if (windowedCache)
346
+ return windowedCache;
347
+ windowedCache = createWindowedData(currentLength(), (i) => rowAt(i));
348
+ return windowedCache;
341
349
  },
350
+ getRow: rowAt,
342
351
  rowCount: () => (countKnown ? count : null),
343
352
  lastRowKnown: () => countKnown,
344
353
  retryFailed() {
@@ -376,7 +385,7 @@ export function createBlockCache(options) {
376
385
  blocks.clear();
377
386
  count = null;
378
387
  countKnown = false;
379
- rowsCache = null;
388
+ rowsCache = windowedCache = null;
380
389
  fetchViewport();
381
390
  emit();
382
391
  },
@@ -524,7 +533,7 @@ export function createBlockCache(options) {
524
533
  controller: null,
525
534
  });
526
535
  }
527
- rowsCache = null;
536
+ rowsCache = windowedCache = null;
528
537
  }
529
538
  /** Forget every block from `fromBlock` on, aborting any in flight. */
530
539
  function dropLoadedFrom(fromBlock) {
@@ -534,6 +543,6 @@ export function createBlockCache(options) {
534
543
  block.controller?.abort();
535
544
  blocks.delete(index);
536
545
  }
537
- rowsCache = null;
546
+ rowsCache = windowedCache = null;
538
547
  }
539
548
  }
@@ -53,7 +53,9 @@ export function createServerDataSource(source, options) {
53
53
  function emitFromCache() {
54
54
  if (!cache || disposed)
55
55
  return;
56
- const rows = cache.rows();
56
+ // Windowed: the grid reads only the rows it shows, so a block landing
57
+ // costs the same at 100M rows as at 10K.
58
+ const rows = cache.windowedRows();
57
59
  state.rows = rows;
58
60
  state.rowCount = cache.rowCount();
59
61
  state.lastRowKnown = cache.lastRowKnown();
package/dist/server.d.ts CHANGED
@@ -10,4 +10,5 @@
10
10
  */
11
11
  export { createServerDataSource, type ServerDataSource, type ServerSelectionRule, type ServerRequest, type ServerResult, type ServerController, type ServerControllerOptions, type ServerState, type ServerSortModel, type ServerFilterModel, type ServerAggFn, type ServerAggregation, type ServerGroupRow, type ServerLeafRow, type ServerMoreRow, type ServerFooterRow, type ServerSkeletonRow, type ServerGrandTotalRow, type ServerPlaceholderRow, type ServerDisplayRow, type ServerDetailRow, } from './server-data-source.js';
12
12
  export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
13
+ export { createWindowedData, windowedSourceOf, isWindowedData, type WindowedSource, } from './windowed-data.js';
13
14
  export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } from './row-model.js';
package/dist/server.js CHANGED
@@ -10,4 +10,5 @@
10
10
  */
11
11
  export { createServerDataSource, } from './server-data-source.js';
12
12
  export { createBlockCache, createRowPlaceholder, rowPlaceholderState, } from './server-block-cache.js';
13
+ export { createWindowedData, windowedSourceOf, isWindowedData, } from './windowed-data.js';
13
14
  export { toServerFilterColumns, } from './row-model.js';
@@ -88,6 +88,13 @@ const OVERLAY_CLASS = 'sv-cell-border-overlay';
88
88
  * every relevant DOM mutation inside the grid, and whenever the action
89
89
  * options update. */
90
90
  function apply(root, opts) {
91
+ // Merges here are colspan / rowspan on the rendered cells, which only the
92
+ // table algorithm honours: keep this grid's rows in table layout (the
93
+ // flex-row rules in SvGrid.css skip a table marked this way).
94
+ for (const table of root.querySelectorAll('table.sv-grid-table')) {
95
+ if (table.getAttribute('data-row-layout') !== 'table')
96
+ table.setAttribute('data-row-layout', 'table');
97
+ }
91
98
  // ---- 1. Clean up anything the LAST run added -----------------------
92
99
  for (const td of root.querySelectorAll(`td[${MARK}]`)) {
93
100
  td.removeAttribute('colspan');
@@ -16,7 +16,11 @@ const shallowCompare = (a, b) => {
16
16
  * comparison. Call from `<script>` so the selector is contextually typed.
17
17
  */
18
18
  export function subscribeGrid(grid, selector) {
19
- let current = selector(grid.store.state);
19
+ // $state, not a plain variable: core.ts keeps a deliberately framework-free
20
+ // store, so this Svelte adapter is where reactivity belongs. Without it a
21
+ // `$derived` reading .current never re-runs and the docs' headless examples
22
+ // render their seeded state forever.
23
+ let current = $state(selector(grid.store.state));
20
24
  grid.store.subscribe(() => {
21
25
  const next = selector(grid.store.state);
22
26
  if (!shallowCompare(current, next))
package/dist/validate.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { rowPlaceholderState } from './server-block-cache.js';
2
2
  /** How many rows to sample when deciding whether a `field` exists. */
3
3
  const FIELD_SAMPLE_ROWS = 10;
4
+ /** How many rows to look through for that sample, placeholders included. */
5
+ const FIELD_SCAN_ROWS = 1000;
4
6
  const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/';
5
7
  /** Every leaf column, flattened through group columns. */
6
8
  function leaves(columns, out = []) {
@@ -23,10 +25,15 @@ export function validateGridConfig(input) {
23
25
  // no data keys at all and say nothing about the columns.
24
26
  // Stops at the sample size: this runs on every data change, and a filter
25
27
  // over all the rows first was O(n) per tick on a live feed.
28
+ // The scan is bounded too: a row model scrolled to the middle of a million
29
+ // rows, or just purged by a sort, hands over a list that is placeholders
30
+ // up to the loaded block, and walking all of them for ten real rows was
31
+ // O(n) per block landing. Finding none means nothing to judge by.
26
32
  const sample = [];
27
- for (const row of input.data ?? []) {
28
- if (sample.length >= FIELD_SAMPLE_ROWS)
29
- break;
33
+ const data = input.data ?? [];
34
+ const scan = Math.min(data.length, FIELD_SCAN_ROWS);
35
+ for (let i = 0; i < scan && sample.length < FIELD_SAMPLE_ROWS; i += 1) {
36
+ const row = data[i];
30
37
  if (!rowPlaceholderState(row))
31
38
  sample.push(row);
32
39
  }
@@ -148,17 +155,9 @@ export function validateGridConfig(input) {
148
155
  'so summary bars will under-report. Remove `treeData` and set ' +
149
156
  '`gantt.parentField` instead.');
150
157
  }
151
- // ---- 7. Pinning that column virtualization will hide ----------------------
152
- // Documented incompatibility: the virtualizer recycles column DOM nodes, so
153
- // sticky pinning cannot survive it. `columnVirtualization` defaults to ON,
154
- // which means the natural way to write this silently does nothing.
155
- const pinned = (input.initialColumnPinning?.left?.length ?? 0) +
156
- (input.initialColumnPinning?.right?.length ?? 0);
157
- if (pinned > 0 && input.columnVirtualization !== false) {
158
- messages.push('[svgrid] `initialColumnPinning` is set while column virtualization is on ' +
159
- '(its default), so the pinned columns will not stick - the virtualizer ' +
160
- 'recycles column nodes. Add `columnVirtualization={false}`.');
161
- }
158
+ // (7. was "pinning versus column virtualization". Pinned columns now render
159
+ // as their own runs beside the virtual window, so the pair is supported and
160
+ // there is nothing to warn about.)
162
161
  // ---- 8. Server-mode contracts left half-wired -----------------------------
163
162
  // Each of these makes the grid hand control to the consumer. Miss the other
164
163
  // half and the feature looks broken rather than unconfigured.
@@ -4,6 +4,10 @@ export declare function createColumnVirtualizer(input: {
4
4
  viewportWidth: number;
5
5
  scrollOffset?: number;
6
6
  overscan?: number;
7
+ /** Columns kept behind the scroll direction; defaults to `overscan`. */
8
+ overscanBehind?: number;
9
+ /** Fewest columns left ahead of a scroll before the window moves. */
10
+ overscanMin?: number;
7
11
  /** Either a uniform size (number) or a per-column size function. */
8
12
  estimateSize?: number | ColumnSizeEstimator;
9
13
  }): {
@@ -6,6 +6,8 @@ export function createColumnVirtualizer(input) {
6
6
  viewportHeight: input.viewportWidth,
7
7
  scrollOffset: input.scrollOffset ?? 0,
8
8
  overscan: input.overscan ?? 4,
9
+ overscanBehind: input.overscanBehind,
10
+ overscanMin: input.overscanMin,
9
11
  });
10
12
  return {
11
13
  ...virtualizer,
@@ -15,8 +15,28 @@ export type VirtualizerOptions = {
15
15
  */
16
16
  estimateSize: number | ((index: number) => number);
17
17
  overscan?: number;
18
+ /**
19
+ * Items kept on the side the scroll is moving away from (`overscan` is kept
20
+ * ahead of it). Defaults to `overscan`, a symmetric window. Before the
21
+ * first scroll there is no direction and both sides get `overscan`.
22
+ */
23
+ overscanBehind?: number;
24
+ /**
25
+ * Keep the rendered window while a scroll leaves at least this many items
26
+ * rendered ahead of it, and move the window only when the scroll runs past
27
+ * that: the window is then rebuilt with the full `overscan` ahead, so it
28
+ * moves `overscan - overscanMin + 1` items at a time instead of one. Each
29
+ * move costs about the same whether one item enters or several, so fewer,
30
+ * larger moves are less work, and the scroll frames in between change
31
+ * nothing. A window built at rest, or after a jump to items outside the
32
+ * rendered window, carries `overscanMin` instead of `overscan`. Undefined
33
+ * moves the window on every item boundary, always with `overscan`.
34
+ */
35
+ overscanMin?: number;
18
36
  viewportHeight: number;
19
37
  scrollOffset?: number;
38
+ /** Internal: the sign of the last scroll movement (-1, 0, 1). */
39
+ scrollDirection?: number;
20
40
  };
21
41
  export type VirtualizerState = {
22
42
  items: Array<VirtualItem>;