@svgrid/grid 3.0.5 → 3.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.
Files changed (148) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +21 -3
  4. package/dist/SvGrid.controller.svelte.js +205 -50
  5. package/dist/SvGrid.css +127 -2
  6. package/dist/SvGrid.svelte +223 -38
  7. package/dist/SvGrid.types.d.ts +77 -4
  8. package/dist/SvGridDropdown.svelte +20 -0
  9. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  10. package/dist/SvModal.svelte +22 -0
  11. package/dist/SvModal.svelte.d.ts +7 -0
  12. package/dist/a11y/dismissable.d.ts +10 -0
  13. package/dist/a11y/dismissable.js +24 -2
  14. package/dist/build-api.js +81 -31
  15. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-C-NtjlrJ.js} +150 -141
  16. package/dist/cdn/GridMenus-TssHej3q.js +644 -0
  17. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  18. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  19. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  21. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
  23. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
  25. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
  27. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
  28. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
  29. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
  31. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  32. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  33. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  34. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  35. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
  36. package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
  37. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
  39. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
  40. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
  41. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  42. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
  43. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  44. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  45. package/dist/cdn/{src-jcFMkBnN.js → src-CgoeSM8k.js} +4697 -4131
  46. package/dist/cdn/{src-Bklofsad.js → src-DfwxB1TH.js} +4727 -4161
  47. package/dist/cdn/svgrid.js +17 -17
  48. package/dist/cdn/svgrid.svelte-external.js +17 -17
  49. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  50. package/dist/cell-formatting.js +18 -1
  51. package/dist/cell-render.js +25 -6
  52. package/dist/clipboard.d.ts +2 -2
  53. package/dist/clipboard.js +72 -23
  54. package/dist/columns.js +7 -2
  55. package/dist/conditional-formatting.d.ts +2 -0
  56. package/dist/conditional-formatting.js +2 -0
  57. package/dist/core.d.ts +22 -6
  58. package/dist/core.js +365 -25
  59. package/dist/editing.js +24 -0
  60. package/dist/grid-messages.d.ts +4 -0
  61. package/dist/grid-messages.js +4 -0
  62. package/dist/headless.d.ts +5 -0
  63. package/dist/headless.js +12 -0
  64. package/dist/history.d.ts +21 -0
  65. package/dist/history.js +34 -4
  66. package/dist/index.d.ts +1 -1
  67. package/dist/keyboard-handlers.js +24 -1
  68. package/dist/keyboard.js +4 -0
  69. package/dist/row-drag-touch.d.ts +1 -1
  70. package/dist/row-drag.js +35 -1
  71. package/dist/row-model.d.ts +6 -0
  72. package/dist/row-resize.js +22 -1
  73. package/dist/selection.js +1 -1
  74. package/dist/server-block-cache.d.ts +5 -0
  75. package/dist/server-block-cache.js +8 -0
  76. package/dist/server-data-source.d.ts +35 -3
  77. package/dist/server-data-source.js +11 -2
  78. package/dist/server.d.ts +1 -1
  79. package/dist/sparkline.js +7 -5
  80. package/dist/svgrid-wrapper.types.d.ts +33 -0
  81. package/dist/validate.js +9 -1
  82. package/package.json +6 -1
  83. package/src/GridMenus.svelte +34 -4
  84. package/src/SvGrid.controller.svelte.ts +206 -53
  85. package/src/SvGrid.css +127 -2
  86. package/src/SvGrid.svelte +223 -38
  87. package/src/SvGrid.types.ts +77 -4
  88. package/src/SvGridDropdown.svelte +20 -0
  89. package/src/SvModal.svelte +22 -0
  90. package/src/SvModal.test.ts +30 -1
  91. package/src/a11y/dismissable.test.ts +10 -1
  92. package/src/a11y/dismissable.ts +22 -2
  93. package/src/build-api.ts +85 -27
  94. package/src/cell-formatting.test.ts +17 -0
  95. package/src/cell-formatting.ts +18 -1
  96. package/src/cell-render.test.ts +24 -2
  97. package/src/cell-render.ts +24 -8
  98. package/src/clipboard.test.ts +107 -0
  99. package/src/clipboard.ts +62 -23
  100. package/src/columns.test.ts +4 -4
  101. package/src/columns.ts +7 -2
  102. package/src/conditional-formatting.test.ts +8 -0
  103. package/src/conditional-formatting.ts +3 -0
  104. package/src/core.tick-repair.test.ts +287 -0
  105. package/src/core.ts +374 -26
  106. package/src/editing.test.ts +52 -0
  107. package/src/editing.ts +25 -0
  108. package/src/grid-messages.test.ts +7 -0
  109. package/src/grid-messages.ts +10 -0
  110. package/src/headless.ts +24 -0
  111. package/src/history.test.ts +49 -1
  112. package/src/history.ts +48 -2
  113. package/src/index.ts +2 -0
  114. package/src/keyboard-handlers.coverage.test.ts +65 -0
  115. package/src/keyboard-handlers.ts +26 -1
  116. package/src/keyboard.ts +3 -0
  117. package/src/row-drag-touch.ts +2 -2
  118. package/src/row-drag.test.ts +51 -0
  119. package/src/row-drag.ts +35 -2
  120. package/src/row-model.ts +6 -0
  121. package/src/row-resize.ts +24 -1
  122. package/src/selection.ts +1 -1
  123. package/src/server-block-cache.test.ts +27 -0
  124. package/src/server-block-cache.ts +13 -0
  125. package/src/server-data-source.infinite.test.ts +62 -0
  126. package/src/server-data-source.test.ts +19 -0
  127. package/src/server-data-source.ts +48 -4
  128. package/src/server.ts +2 -0
  129. package/src/sparkline.test.ts +7 -0
  130. package/src/sparkline.ts +6 -4
  131. package/src/svgrid-wrapper.types.ts +32 -0
  132. package/src/svgrid.api.test.ts +21 -0
  133. package/src/svgrid.charting.test.ts +12 -4
  134. package/src/svgrid.context-menu.test.ts +12 -0
  135. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  136. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  137. package/src/svgrid.gantt-seam.test.ts +1 -1
  138. package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
  139. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  140. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  141. package/src/svgrid.row-pinning.test.ts +15 -1
  142. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  143. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  144. package/src/svgrid.upsell-license.test.ts +1 -1
  145. package/src/test-setup.ts +12 -0
  146. package/src/transaction.test.ts +58 -0
  147. package/src/validate.ts +7 -1
  148. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
package/dist/core.js CHANGED
@@ -165,6 +165,19 @@ export const filterFns = {
165
165
  const ROW_CTX = Symbol('svgrid.row.ctx');
166
166
  const ROW_VALUES = Symbol('svgrid.row.values');
167
167
  const ROW_CELLS = Symbol('svgrid.row.cells');
168
+ // Where the filtered stage last put this row: the generation of that full
169
+ // filter and the row's index in its output. Lets a tick swap a replaced row
170
+ // into place without re-filtering everything (see createFilteredRowModel).
171
+ const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen');
172
+ const ROW_FILTER_POS = Symbol('svgrid.row.filterPos');
173
+ // Stamped on the rows a sort repair drops, so the merge skips them with one
174
+ // property read per row instead of a Set lookup (3 ms per 100k rows).
175
+ const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped');
176
+ // The key(s) the last full sort computed for a row: the key itself for a
177
+ // one-clause sort, an array for several. Read by the repair path so a tick
178
+ // never recomputes a kept row's key.
179
+ const ROW_SORT_KEY = Symbol('svgrid.row.sortKey');
180
+ let sortDropGeneration = 0;
168
181
  /**
169
182
  * The methods every base row carries, defined ONCE and assigned by reference.
170
183
  *
@@ -276,17 +289,32 @@ export function createCoreRowModel() {
276
289
  * `columnFilteringFeature`; without it there are no filters to apply.
277
290
  */
278
291
  export function createFilteredRowModel() {
292
+ /**
293
+ * The last full filter, so a tick can repair it. When `getRowModel`
294
+ * reports which rows were replaced (`replacedRowsOf`) and the filter state
295
+ * and columns are the same objects as last time, each replacement is tested
296
+ * on its own: a row that stays in keeps its slot, a row that stays out
297
+ * stays out, and a row whose membership flips sends the whole thing back
298
+ * through the full pass, because an insertion has to land in data order.
299
+ * Positions come from stamps on the rows (ROW_FILTER_GEN / ROW_FILTER_POS)
300
+ * written by the full pass; no map is built.
301
+ */
302
+ let cache = null;
303
+ let generation = 0;
279
304
  return ({ table, rows }) => {
280
305
  const filters = table.getState().columnFilters ?? [];
281
- if (!filters.length)
306
+ if (!filters.length) {
307
+ cache = null;
282
308
  return rows;
309
+ }
310
+ const columns = table.getAllColumns();
283
311
  // Resolve each filter's match function once, outside the row loop.
284
312
  const compiled = filters.map((filter) => ({
285
313
  id: filter.id,
286
314
  value: filter.value,
287
315
  fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,
288
316
  }));
289
- return rows.filter((row) => {
317
+ const passes = (row) => {
290
318
  for (let i = 0; i < compiled.length; i++) {
291
319
  const filter = compiled[i];
292
320
  // `getCellValueByColumnId` rather than `getAllCells().find(...)`.
@@ -299,7 +327,56 @@ export function createFilteredRowModel() {
299
327
  return false;
300
328
  }
301
329
  return true;
302
- });
330
+ };
331
+ if (cache && cache.filters === filters && cache.columns === columns) {
332
+ if (cache.input === rows)
333
+ return cache.output;
334
+ const replaced = replacedRowsOf(table);
335
+ if (replaced && rows.length === cache.input.length) {
336
+ let out = null;
337
+ let ok = true;
338
+ for (let i = 0; i < rows.length && ok; i++) {
339
+ const prev = cache.input[i];
340
+ const next = rows[i];
341
+ if (next === prev)
342
+ continue;
343
+ if (replaced.get(prev) !== next) {
344
+ ok = false;
345
+ break;
346
+ }
347
+ const wasIn = prev[ROW_FILTER_GEN] === cache.gen;
348
+ const isIn = passes(next);
349
+ if (wasIn !== isIn) {
350
+ ok = false;
351
+ break;
352
+ }
353
+ if (!isIn)
354
+ continue;
355
+ out ?? (out = cache.output.slice());
356
+ const pos = prev[ROW_FILTER_POS];
357
+ out[pos] = next;
358
+ next[ROW_FILTER_GEN] = cache.gen;
359
+ next[ROW_FILTER_POS] = pos;
360
+ }
361
+ if (ok) {
362
+ const output = out ?? cache.output;
363
+ cache = { ...cache, input: rows, output };
364
+ return output;
365
+ }
366
+ }
367
+ }
368
+ const gen = ++generation;
369
+ const output = [];
370
+ for (let i = 0; i < rows.length; i++) {
371
+ const row = rows[i];
372
+ if (!passes(row))
373
+ continue;
374
+ row[ROW_FILTER_GEN] = gen;
375
+ row[ROW_FILTER_POS] = output.length;
376
+ output.push(row);
377
+ }
378
+ cache = { input: rows, output, filters, columns, gen };
379
+ return output;
303
380
  };
304
381
  }
305
382
  /**
@@ -542,6 +619,19 @@ export function flattenTreeData(data, options) {
542
619
  export function createExpandedRowModel() {
543
620
  return ({ table, rows }) => {
544
621
  const expanded = table.getState().expanded ?? {};
622
+ // Nothing expanded means every collapsed row hides its children and the
623
+ // output is the input, element for element. Returning it as is keeps the
624
+ // array identity the stages downstream and the render window key on, and
625
+ // skips a 100k-element copy per data change on a flat grid.
626
+ let anyExpanded = false;
627
+ for (const id in expanded) {
628
+ if (expanded[id]) {
629
+ anyExpanded = true;
630
+ break;
631
+ }
632
+ }
633
+ if (!anyExpanded)
634
+ return rows;
545
635
  const flattened = [];
546
636
  const visit = (row) => {
547
637
  flattened.push(row);
@@ -560,11 +650,58 @@ export function createExpandedRowModel() {
560
650
  * override the built-in {@link sortFns} - useful for locale-aware or
561
651
  * domain-specific ordering.
562
652
  */
653
+ /**
654
+ * Which rows the last `getRowModel` rebuild replaced, old row -> new row, for
655
+ * the duration of the pipeline run that follows it.
656
+ *
657
+ * A live feed replaces a few hundred row objects in a hundred-thousand-row
658
+ * array and hands the grid the new array. The base rows are rebuilt reusing
659
+ * every row whose data object is the same (see `getRowModel`), so the rebuild
660
+ * knows exactly which rows are new. The filtered and sorted stages read that
661
+ * here and repair their previous output instead of recomputing it, when the
662
+ * replacements are few and nothing else changed. Cleared after the run; a
663
+ * stage that reads it outside a run sees null and does the full work.
664
+ *
665
+ * Keyed on the grid object in a WeakMap rather than carried on the public
666
+ * `SvGrid` type, so the stage signature stays `({ table, rows }) => rows`.
667
+ */
668
+ const replacedRowsByGrid = new WeakMap();
669
+ /** The rows `getRowModel` replaced in the rebuild this pipeline run follows, or null. */
670
+ export function replacedRowsOf(table) {
671
+ return replacedRowsByGrid.get(table) ?? null;
672
+ }
673
+ /**
674
+ * The sorting stage of the row pipeline: orders the rows by the grid's
675
+ * `sorting` state with the built-in comparators (or the `localSortFns` map
676
+ * handed in), stable, equal keys keeping their data order. A tick that
677
+ * replaces a few rows out of many is repaired incrementally rather than
678
+ * re-sorted (see the note inside). Registered by `rowSortingFeature`; an app
679
+ * only calls this to install its own comparator set.
680
+ */
563
681
  export function createSortedRowModel(localSortFns = sortFns) {
564
- return function sortedRowModelStage({ table, rows }) {
565
- const sorting = table.getState().sorting ?? [];
566
- if (!sorting.length)
567
- return rows;
682
+ /**
683
+ * What the last full sort produced, kept so a tick can repair it.
684
+ *
685
+ * A live feed replaces a few hundred row objects out of a hundred thousand
686
+ * and hands the grid a new array. Re-sorting all of it costs the same as
687
+ * the first sort did (115-149 ms in the browser for 100k rows, see
688
+ * docs/help/benchmarks.md); the order it would produce differs from the
689
+ * previous one only where the replaced rows land. So the previous output
690
+ * is kept, with its sort keys aligned to it, and when `getRowModel` says
691
+ * which rows were replaced (see `replacedRowsOf`) the stage drops those,
692
+ * sorts the replacements among themselves and merges them back in one
693
+ * linear pass: O(n + k log k) instead of O(n log n) key builds and
694
+ * comparisons.
695
+ *
696
+ * The repair is only attempted when everything else is the same object as
697
+ * last time - the sorting state, the columns, and every input row except
698
+ * the replaced ones - and it produces exactly what the full sort would:
699
+ * equal keys fall back to the data index, which is the order a stable sort
700
+ * over data-ordered input keeps.
701
+ */
702
+ let cache = null;
703
+ const MISSING_KEY = Symbol('svgrid.sort.missing-key');
704
+ function fullSort(rows, sorting, allColumns) {
568
705
  // Resolve every clause ONCE, before sorting.
569
706
  //
570
707
  // This used to live inside the comparator, so `getAllColumns().find(...)`
@@ -572,7 +709,6 @@ export function createSortedRowModel(localSortFns = sortFns) {
572
709
  // 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;
573
710
  // `pnpm bench --case=sort-1col`). The comparator is called O(n log n)
574
711
  // times, so anything inside it that is not O(1) sets the cost of the sort.
575
- const allColumns = table.getAllColumns();
576
712
  const clauses = [];
577
713
  for (const clause of sorting) {
578
714
  const column = allColumns.find((col) => col.id === clause.id);
@@ -599,15 +735,17 @@ export function createSortedRowModel(localSortFns = sortFns) {
599
735
  const n = rows.length;
600
736
  let keys = new Array(n);
601
737
  let compare;
738
+ let keyOf;
602
739
  if (comparator === sortFns.number) {
740
+ keyOf = (row) => Number(row.getCellValueByColumnId(columnId) ?? 0);
603
741
  for (let i = 0; i < n; i++)
604
- keys[i] = Number(rows[i].getCellValueByColumnId(columnId) ?? 0);
742
+ keys[i] = keyOf(rows[i]);
605
743
  compare = compareNumericKeys;
606
744
  }
607
745
  else if (comparator === sortFns.date) {
608
- for (let i = 0; i < n; i++) {
609
- keys[i] = new Date(rows[i].getCellValueByColumnId(columnId)).getTime();
610
- }
746
+ keyOf = (row) => new Date(row.getCellValueByColumnId(columnId)).getTime();
747
+ for (let i = 0; i < n; i++)
748
+ keys[i] = keyOf(rows[i]);
611
749
  compare = compareNumericKeys;
612
750
  }
613
751
  else if (comparator === sortFns.auto) {
@@ -669,21 +807,26 @@ export function createSortedRowModel(localSortFns = sortFns) {
669
807
  for (let i = 0; i < n; i++)
670
808
  keys[i] = rankOf.get(strings[i]);
671
809
  compare = compareNumericKeys;
810
+ // A replacement whose text is not among the ranked values has no
811
+ // rank; the repair path treats that as "sort everything again".
812
+ keyOf = (row) => rankOf.get(String(row.getCellValueByColumnId(columnId))) ?? MISSING_KEY;
672
813
  }
673
814
  else {
674
815
  keys = strings;
675
816
  compare = compareCollatedKeys;
817
+ keyOf = (row) => String(row.getCellValueByColumnId(columnId));
676
818
  }
677
819
  }
678
820
  else {
821
+ keyOf = (row) => row.getCellValueByColumnId(columnId);
679
822
  for (let i = 0; i < n; i++)
680
- keys[i] = rows[i].getCellValueByColumnId(columnId);
823
+ keys[i] = keyOf(rows[i]);
681
824
  compare = comparator;
682
825
  }
683
- clauses.push({ keys, desc: clause.desc, compare });
826
+ clauses.push({ keys, desc: clause.desc, compare, keyOf });
684
827
  }
685
828
  if (!clauses.length)
686
- return rows;
829
+ return null;
687
830
  // Sort an index array, then materialise. `Array.prototype.sort` is stable,
688
831
  // so equal keys keep their original relative order exactly as the previous
689
832
  // `[...rows].sort(...)` did.
@@ -730,7 +873,167 @@ export function createSortedRowModel(localSortFns = sortFns) {
730
873
  const sorted = new Array(rows.length);
731
874
  for (let i = 0; i < order.length; i++)
732
875
  sorted[i] = rows[order[i]];
733
- return sorted;
876
+ // Each row keeps its key(s), so a later tick can place a replacement
877
+ // among the kept rows without reading any of them again. One property
878
+ // write per row; an array per row only when there are several clauses.
879
+ if (clauses.length === 1) {
880
+ const keys = clauses[0].keys;
881
+ for (let i = 0; i < rows.length; i++)
882
+ rows[i][ROW_SORT_KEY] = keys[i];
883
+ }
884
+ else {
885
+ for (let i = 0; i < rows.length; i++) {
886
+ const per = new Array(clauses.length);
887
+ for (let k = 0; k < clauses.length; k++)
888
+ per[k] = clauses[k].keys[i];
889
+ rows[i][ROW_SORT_KEY] = per;
890
+ }
891
+ }
892
+ return { sorted, clauses: clauses.map(({ desc, compare, keyOf }) => ({ desc, compare, keyOf })) };
893
+ }
894
+ /**
895
+ * The previous output with the replaced rows swapped for their
896
+ * replacements, in sorted position. Null when the input is not the cached
897
+ * input plus replacements, or a replacement has no incremental key.
898
+ *
899
+ * Every row carries the key(s) the last full sort computed for it
900
+ * (ROW_SORT_KEY: the key itself for one clause, an array for several), so
901
+ * the merge moves row references and nothing else. At 100k rows the loop
902
+ * that also copied an aligned key array cost 3.7 ms; this one is the
903
+ * reference copy alone.
904
+ */
905
+ function repair(rows, replaced) {
906
+ const c = cache;
907
+ if (rows.length !== c.input.length)
908
+ return null;
909
+ // Which rows of THIS stage's input changed. The map may name rows a
910
+ // stage upstream filtered out; only the ones that reach here matter.
911
+ const dropGen = ++sortDropGeneration;
912
+ const added = [];
913
+ for (let i = 0; i < rows.length; i++) {
914
+ const prev = c.input[i];
915
+ const next = rows[i];
916
+ if (next === prev)
917
+ continue;
918
+ if (replaced.get(prev) !== next)
919
+ return null;
920
+ prev[ROW_SORT_DROPPED] = dropGen;
921
+ added.push(next);
922
+ }
923
+ if (!added.length)
924
+ return c.output;
925
+ const clauses = c.clauses;
926
+ const single = clauses.length === 1;
927
+ for (let j = 0; j < added.length; j++) {
928
+ const row = added[j];
929
+ if (single) {
930
+ const key = clauses[0].keyOf(row);
931
+ if (key === MISSING_KEY)
932
+ return null;
933
+ row[ROW_SORT_KEY] = key;
934
+ }
935
+ else {
936
+ const keys = new Array(clauses.length);
937
+ for (let k = 0; k < clauses.length; k++) {
938
+ const key = clauses[k].keyOf(row);
939
+ if (key === MISSING_KEY)
940
+ return null;
941
+ keys[k] = key;
942
+ }
943
+ row[ROW_SORT_KEY] = keys;
944
+ }
945
+ }
946
+ // Keys first, then the data index: the tie-break a stable sort over
947
+ // data-ordered input applies, so the merge lands where a full sort would.
948
+ const prevOut = c.output;
949
+ const compareRows = single
950
+ ? (() => {
951
+ const { compare, desc } = clauses[0];
952
+ return (a, b) => {
953
+ const r = compare(a[ROW_SORT_KEY], b[ROW_SORT_KEY]);
954
+ if (r !== 0)
955
+ return desc ? -r : r;
956
+ return a.index - b.index;
957
+ };
958
+ })()
959
+ : (a, b) => {
960
+ const ka = a[ROW_SORT_KEY];
961
+ const kb = b[ROW_SORT_KEY];
962
+ for (let k = 0; k < clauses.length; k++) {
963
+ const clause = clauses[k];
964
+ const r = clause.compare(ka[k], kb[k]);
965
+ if (r !== 0)
966
+ return clause.desc ? -r : r;
967
+ }
968
+ return a.index - b.index;
969
+ };
970
+ // Sort the replacements among themselves (k log k)...
971
+ added.sort(compareRows);
972
+ // ...find where each one goes in the previous output by binary search
973
+ // (k log n comparisons, against the dropped rows too - they are skipped
974
+ // below, which does not move an insertion point)...
975
+ const n = prevOut.length;
976
+ const insertAt = new Array(added.length);
977
+ let lo = 0;
978
+ for (let j = 0; j < added.length; j++) {
979
+ // Monotone: the replacements are sorted, so each search starts where
980
+ // the previous one landed.
981
+ const row = added[j];
982
+ let hi = n;
983
+ while (lo < hi) {
984
+ const mid = (lo + hi) >>> 1;
985
+ if (compareRows(row, prevOut[mid]) < 0)
986
+ hi = mid;
987
+ else
988
+ lo = mid + 1;
989
+ }
990
+ insertAt[j] = lo;
991
+ }
992
+ // ...then one linear pass that only moves references.
993
+ const out = new Array(rows.length);
994
+ let w = 0;
995
+ let j = 0;
996
+ let nextInsert = insertAt[0] ?? -1;
997
+ for (let i = 0; i < n; i++) {
998
+ while (nextInsert === i) {
999
+ out[w++] = added[j++];
1000
+ nextInsert = j < added.length ? insertAt[j] : -1;
1001
+ }
1002
+ const prev = prevOut[i];
1003
+ if (prev[ROW_SORT_DROPPED] === dropGen)
1004
+ continue;
1005
+ out[w++] = prev;
1006
+ }
1007
+ while (j < added.length)
1008
+ out[w++] = added[j++];
1009
+ return out;
1010
+ }
1011
+ return function sortedRowModelStage({ table, rows }) {
1012
+ const sorting = table.getState().sorting ?? [];
1013
+ if (!sorting.length) {
1014
+ cache = null;
1015
+ return rows;
1016
+ }
1017
+ const columns = table.getAllColumns();
1018
+ if (cache && cache.sorting === sorting && cache.columns === columns) {
1019
+ if (cache.input === rows)
1020
+ return cache.output;
1021
+ const replaced = replacedRowsOf(table);
1022
+ if (replaced) {
1023
+ const repaired = repair(rows, replaced);
1024
+ if (repaired) {
1025
+ cache = { ...cache, input: rows, output: repaired };
1026
+ return repaired;
1027
+ }
1028
+ }
1029
+ }
1030
+ const full = fullSort(rows, sorting, columns);
1031
+ if (!full) {
1032
+ cache = null;
1033
+ return rows;
1034
+ }
1035
+ cache = { input: rows, output: full.sorted, sorting, columns, clauses: full.clauses };
1036
+ return full.sorted;
734
1037
  };
735
1038
  }
736
1039
  /**
@@ -788,6 +1091,8 @@ export function createSvGridCore(options) {
788
1091
  let cachedBaseRowsInput = null;
789
1092
  let cachedBaseRowsColumns = null;
790
1093
  let cachedBaseRows = [];
1094
+ /** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */
1095
+ let replacedRows = null;
791
1096
  let cachedRowCtx = null;
792
1097
  let cachedRowModel = null;
793
1098
  let cachedRowModelBaseRows = null;
@@ -968,7 +1273,11 @@ export function createSvGridCore(options) {
968
1273
  },
969
1274
  getRowModel() {
970
1275
  const columns = grid.getAllColumns();
971
- if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
1276
+ // Read once. `options.data` is a getter when <SvGrid> drives the engine
1277
+ // (a $state.raw signal), and the rebuild below touched it twice per row:
1278
+ // 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.
1279
+ const data = options.data;
1280
+ if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {
972
1281
  // Same columns as last time: the shared context still describes them,
973
1282
  // and a row whose data object sits at the same index can keep its
974
1283
  // row object. A row model that streams blocks hands the grid a new
@@ -978,7 +1287,11 @@ export function createSvGridCore(options) {
978
1287
  // an app may have changed the object in place before passing a new
979
1288
  // array - that is the case the old rebuild covered by accident.
980
1289
  const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null;
981
- cachedBaseRowsInput = options.data;
1290
+ // Same length as last time: a replacement, not an add or remove, so
1291
+ // each new row stands in for the old row at its index and the stages
1292
+ // can repair rather than recompute. Anything else is structural.
1293
+ replacedRows = previous && previous.length === data.length ? new Map() : null;
1294
+ cachedBaseRowsInput = data;
982
1295
  cachedBaseRowsColumns = columns;
983
1296
  // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
984
1297
  // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
@@ -1001,11 +1314,11 @@ export function createSvGridCore(options) {
1001
1314
  };
1002
1315
  cachedRowCtx = rowCtx;
1003
1316
  }
1004
- cachedBaseRows = new Array(options.data.length);
1317
+ cachedBaseRows = new Array(data.length);
1005
1318
  const getRowId = options.getRowId;
1006
1319
  const m = BASE_ROW_METHODS;
1007
- for (let index = 0; index < options.data.length; index++) {
1008
- const original = options.data[index];
1320
+ for (let index = 0; index < data.length; index++) {
1321
+ const original = data[index];
1009
1322
  const kept = previous?.[index];
1010
1323
  if (kept && kept.original === original) {
1011
1324
  kept[ROW_VALUES] = null;
@@ -1013,6 +1326,10 @@ export function createSvGridCore(options) {
1013
1326
  cachedBaseRows[index] = kept;
1014
1327
  continue;
1015
1328
  }
1329
+ // Past a quarter of the rows the merge stops being cheaper than a
1330
+ // sort with the ranking pass; let the stages do the full work.
1331
+ if (replacedRows && replacedRows.size * 4 >= data.length)
1332
+ replacedRows = null;
1016
1333
  // `_values` and `_cells` stay null until something reads them - a
1017
1334
  // 100k-row grid showing twenty rows must not materialise every row's
1018
1335
  // values or cell objects to paint.
@@ -1024,6 +1341,10 @@ export function createSvGridCore(options) {
1024
1341
  [ROW_CTX]: rowCtx,
1025
1342
  [ROW_VALUES]: null,
1026
1343
  [ROW_CELLS]: null,
1344
+ [ROW_FILTER_GEN]: 0,
1345
+ [ROW_FILTER_POS]: -1,
1346
+ [ROW_SORT_DROPPED]: 0,
1347
+ [ROW_SORT_KEY]: undefined,
1027
1348
  getCanExpand: m.getCanExpand,
1028
1349
  getIsExpanded: m.getIsExpanded,
1029
1350
  toggleExpanded: m.toggleExpanded,
@@ -1033,7 +1354,17 @@ export function createSvGridCore(options) {
1033
1354
  getCellValueByColumnId: m.getCellValueByColumnId,
1034
1355
  };
1035
1356
  cachedBaseRows[index] = row;
1357
+ if (replacedRows && kept)
1358
+ replacedRows.set(kept, row);
1036
1359
  }
1360
+ // A new array with every object the same is the documented way to
1361
+ // say "I changed rows in place, re-read them" (docs/help/cells/
1362
+ // view-refresh.md). Nothing was replaced, so there is nothing to
1363
+ // repair around: the stages recompute from the current values. A
1364
+ // repair assumes the rows it keeps are unchanged in value as well as
1365
+ // identity, which is the immutable-update contract a tick follows.
1366
+ if (replacedRows && replacedRows.size === 0)
1367
+ replacedRows = null;
1037
1368
  }
1038
1369
  // Only the slices a pipeline stage actually READS belong in this key.
1039
1370
  //
@@ -1075,10 +1406,19 @@ export function createSvGridCore(options) {
1075
1406
  pipeline.expandedRowModel,
1076
1407
  pipeline.paginatedRowModel,
1077
1408
  ];
1078
- ordered.forEach((fn) => {
1079
- if (fn)
1080
- rows = fn({ table: grid, rows });
1081
- });
1409
+ // The stages read the replacements through `replacedRowsOf` while the
1410
+ // run lasts; a stage that runs outside one sees null.
1411
+ replacedRowsByGrid.set(grid, replacedRows);
1412
+ try {
1413
+ ordered.forEach((fn) => {
1414
+ if (fn)
1415
+ rows = fn({ table: grid, rows });
1416
+ });
1417
+ }
1418
+ finally {
1419
+ replacedRowsByGrid.set(grid, null);
1420
+ replacedRows = null;
1421
+ }
1082
1422
  cachedPipeline = options._rowModels;
1083
1423
  cachedSlices = currentSlices;
1084
1424
  cachedRowModelBaseRows = cachedBaseRows;
package/dist/editing.js CHANGED
@@ -133,6 +133,22 @@ export function createEditing(ctx) {
133
133
  // belongs to the same Ctrl+Z as the value.
134
134
  runHistoryGroup(ctx, () => commitEditingCell());
135
135
  }
136
+ /**
137
+ * True when a column with `rejectInvalid` refuses `value`: its `validate`
138
+ * hook returns `false` or a message. Without the flag nothing is refused.
139
+ */
140
+ function editRejected(column, value, rowData) {
141
+ const def = column?.columnDef;
142
+ if (!def?.rejectInvalid || typeof def.validate !== "function")
143
+ return false;
144
+ const verdict = def.validate({
145
+ value,
146
+ row: rowData,
147
+ rowIndex: ctx.internalData.indexOf(rowData),
148
+ column,
149
+ });
150
+ return verdict === false || (typeof verdict === "string" && verdict.length > 0);
151
+ }
136
152
  function commitEditingCell() {
137
153
  if (!ctx.editingCell)
138
154
  return;
@@ -165,6 +181,12 @@ export function createEditing(ctx) {
165
181
  columnId: ctx.editingCell.columnId,
166
182
  });
167
183
  }
184
+ // A refused edit closes the editor and leaves the row, the overlay,
185
+ // the history and the consumer untouched.
186
+ if (editRejected(column, finalValue, rowData)) {
187
+ ctx.editingCell = null;
188
+ return;
189
+ }
168
190
  rowData[column.columnDef.field] = finalValue;
169
191
  }
170
192
  const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
@@ -764,6 +786,8 @@ export function createEditing(ctx) {
764
786
  columnId: column.id,
765
787
  })
766
788
  : parsed;
789
+ if (editRejected(column, finalValue, row.original))
790
+ continue;
767
791
  if (oldValue !== finalValue) {
768
792
  row.original[field] = finalValue;
769
793
  const key = getCellKey(fr.rowId, column.id);
@@ -76,6 +76,10 @@ export type GridChromeMessages = {
76
76
  chartRange: string;
77
77
  rowLoadFailed: string;
78
78
  rowLoadRetry: string;
79
+ expandGroup: string;
80
+ collapseGroup: string;
81
+ openDetail: string;
82
+ closeDetail: string;
79
83
  pivotUpsellTitle: string;
80
84
  pivotUpsellBody: string;
81
85
  ganttUpsellTitle: string;
@@ -86,6 +86,10 @@ export const defaultGridMessages = {
86
86
  chartRange: 'Chart selected range',
87
87
  rowLoadFailed: 'Could not load these rows.',
88
88
  rowLoadRetry: 'Retry',
89
+ expandGroup: 'Expand group',
90
+ collapseGroup: 'Collapse group',
91
+ openDetail: 'Open details',
92
+ closeDetail: 'Close details',
89
93
  pivotUpsellTitle: 'Pivot mode',
90
94
  pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
91
95
  ganttUpsellTitle: 'Gantt view',
@@ -27,3 +27,8 @@ export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, crea
27
27
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
29
  export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
30
+ export { createVirtualizer } from './virtualization/virtualizer.js';
31
+ export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
32
+ export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
33
+ export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types.js';
34
+ export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, type GridCellA11yInput, type GridColumnA11yInput, type GridSortDirection, } from './a11y.js';
package/dist/headless.js CHANGED
@@ -27,3 +27,15 @@ export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, crea
27
27
  export { createGrid, createSvGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
29
  export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
30
+ // The virtualizers are pure window arithmetic (scroll offset in, visible
31
+ // range out) with no DOM of their own, and a custom renderer over 50k rows
32
+ // needs one. They were only on the barrel, so the Headless demos imported
33
+ // the barrel to reach them and paid for the whole thing.
34
+ export { createVirtualizer } from './virtualization/virtualizer.js';
35
+ export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
36
+ export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
37
+ // The WAI-ARIA attribute factories: plain objects to spread onto your own
38
+ // markup (`role`, `aria-sort`, `aria-rowindex`, ...). No DOM is touched here
39
+ // either; a custom renderer that wants keyboard navigation reads
40
+ // `activeCell` from the store and spreads these.
41
+ export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y.js';
package/dist/history.d.ts CHANGED
@@ -38,6 +38,15 @@ export type HistoryStep = {
38
38
  undo: () => void;
39
39
  redo: () => void;
40
40
  };
41
+ /**
42
+ * The consumer's own mark on the step, copied from `api.setHistoryTag` at
43
+ * the moment it is recorded. The grid never reads it: it is there for a
44
+ * consumer that shows one of several data sets through the grid and has
45
+ * to know which one a step belongs to before it lets Ctrl+Z apply it (a
46
+ * workbook with several sheets, say). Kept as given, so an object tag
47
+ * compares by identity. Not serialised.
48
+ */
49
+ tag?: unknown;
41
50
  };
42
51
  /** Ctx surface these helpers touch. The controller satisfies it structurally. */
43
52
  type HistoryCtx = {
@@ -49,8 +58,20 @@ type HistoryCtx = {
49
58
  * inside it joins that group, so a command does not have to thread a group
50
59
  * id down through the writers it calls. */
51
60
  historyGroupId: string | undefined;
61
+ /** Stamped on every step recorded while it is set; see `HistoryStep.tag`. */
62
+ historyTag?: unknown;
52
63
  applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo'): void;
53
64
  };
65
+ /** What the next undo would apply: the tag of the step Ctrl+Z takes next
66
+ * (a group carries one tag, so its last step's is the group's). Null when
67
+ * there is nothing to undo. */
68
+ export declare function peekUndoHistory(ctx: HistoryCtx): {
69
+ tag: unknown;
70
+ } | null;
71
+ /** The redo counterpart of `peekUndoHistory`. */
72
+ export declare function peekRedoHistory(ctx: HistoryCtx): {
73
+ tag: unknown;
74
+ } | null;
54
75
  /** Undo one action. Returns false when there is nothing left to undo. */
55
76
  export declare function undoHistory(ctx: HistoryCtx): boolean;
56
77
  /** Redo one action. Returns false when there is nothing left to redo. */