@svgrid/grid 3.0.5 → 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +23 -3
  4. package/dist/SvGrid.controller.svelte.js +235 -54
  5. package/dist/SvGrid.css +143 -7
  6. package/dist/SvGrid.helpers.js +2 -4
  7. package/dist/SvGrid.svelte +223 -38
  8. package/dist/SvGrid.types.d.ts +77 -4
  9. package/dist/SvGridDropdown.svelte +20 -0
  10. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  11. package/dist/SvModal.svelte +22 -0
  12. package/dist/SvModal.svelte.d.ts +7 -0
  13. package/dist/a11y/dismissable.d.ts +10 -0
  14. package/dist/a11y/dismissable.js +24 -2
  15. package/dist/build-api.js +81 -31
  16. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
  17. package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
  18. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  19. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  21. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
  23. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
  25. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
  27. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
  28. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
  29. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
  31. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
  32. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  33. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  34. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  35. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  36. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
  37. package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
  39. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
  40. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
  41. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
  42. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  43. package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
  44. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
  45. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  46. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  47. package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
  48. package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
  49. package/dist/cdn/svgrid.js +18 -18
  50. package/dist/cdn/svgrid.svelte-external.js +18 -18
  51. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  52. package/dist/cell-formatting.d.ts +9 -0
  53. package/dist/cell-formatting.js +52 -1
  54. package/dist/cell-render.js +25 -6
  55. package/dist/clipboard.d.ts +2 -2
  56. package/dist/clipboard.js +72 -23
  57. package/dist/columns.js +7 -2
  58. package/dist/conditional-formatting.d.ts +2 -0
  59. package/dist/conditional-formatting.js +2 -0
  60. package/dist/core.d.ts +22 -6
  61. package/dist/core.js +478 -40
  62. package/dist/editing.js +24 -0
  63. package/dist/export-format.d.ts +8 -0
  64. package/dist/export-format.js +23 -16
  65. package/dist/grid-messages.d.ts +4 -0
  66. package/dist/grid-messages.js +4 -0
  67. package/dist/headless.d.ts +5 -0
  68. package/dist/headless.js +12 -0
  69. package/dist/history.d.ts +21 -0
  70. package/dist/history.js +34 -4
  71. package/dist/index.d.ts +1 -1
  72. package/dist/keyboard-handlers.js +24 -1
  73. package/dist/keyboard.js +4 -0
  74. package/dist/row-drag-touch.d.ts +1 -1
  75. package/dist/row-drag.js +35 -1
  76. package/dist/row-model.d.ts +6 -0
  77. package/dist/row-resize.js +22 -1
  78. package/dist/selection.js +1 -1
  79. package/dist/server-block-cache.d.ts +5 -0
  80. package/dist/server-block-cache.js +8 -0
  81. package/dist/server-data-source.d.ts +35 -3
  82. package/dist/server-data-source.js +11 -2
  83. package/dist/server.d.ts +1 -1
  84. package/dist/sparkline.js +7 -5
  85. package/dist/svgrid-wrapper.types.d.ts +33 -0
  86. package/dist/validate.js +9 -1
  87. package/dist/virtualization/column-virtualizer.d.ts +1 -0
  88. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
  89. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
  90. package/dist/virtualization/virtualizer.d.ts +3 -0
  91. package/dist/virtualization/virtualizer.js +14 -1
  92. package/package.json +6 -1
  93. package/src/GridMenus.svelte +34 -4
  94. package/src/SvGrid.controller.svelte.ts +236 -57
  95. package/src/SvGrid.css +143 -7
  96. package/src/SvGrid.helpers.ts +2 -4
  97. package/src/SvGrid.svelte +223 -38
  98. package/src/SvGrid.types.ts +77 -4
  99. package/src/SvGridDropdown.svelte +20 -0
  100. package/src/SvModal.svelte +22 -0
  101. package/src/SvModal.test.ts +30 -1
  102. package/src/a11y/dismissable.test.ts +10 -1
  103. package/src/a11y/dismissable.ts +22 -2
  104. package/src/build-api.ts +85 -27
  105. package/src/cell-formatting.test.ts +17 -0
  106. package/src/cell-formatting.ts +49 -1
  107. package/src/cell-render.test.ts +24 -2
  108. package/src/cell-render.ts +24 -8
  109. package/src/clipboard.test.ts +107 -0
  110. package/src/clipboard.ts +62 -23
  111. package/src/columns.test.ts +4 -4
  112. package/src/columns.ts +7 -2
  113. package/src/conditional-formatting.test.ts +8 -0
  114. package/src/conditional-formatting.ts +3 -0
  115. package/src/core.sort.test.ts +86 -0
  116. package/src/core.tick-repair.test.ts +287 -0
  117. package/src/core.ts +478 -40
  118. package/src/editing.test.ts +52 -0
  119. package/src/editing.ts +25 -0
  120. package/src/export-dates.test.ts +114 -0
  121. package/src/export-format.ts +26 -14
  122. package/src/grid-messages.test.ts +7 -0
  123. package/src/grid-messages.ts +10 -0
  124. package/src/headless.ts +24 -0
  125. package/src/history.test.ts +49 -1
  126. package/src/history.ts +48 -2
  127. package/src/index.ts +2 -0
  128. package/src/keyboard-handlers.coverage.test.ts +65 -0
  129. package/src/keyboard-handlers.ts +26 -1
  130. package/src/keyboard.ts +3 -0
  131. package/src/row-drag-touch.ts +2 -2
  132. package/src/row-drag.test.ts +51 -0
  133. package/src/row-drag.ts +35 -2
  134. package/src/row-model.ts +6 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.ts +1 -1
  137. package/src/server-block-cache.test.ts +27 -0
  138. package/src/server-block-cache.ts +13 -0
  139. package/src/server-data-source.infinite.test.ts +62 -0
  140. package/src/server-data-source.test.ts +19 -0
  141. package/src/server-data-source.ts +48 -4
  142. package/src/server.ts +2 -0
  143. package/src/sparkline.test.ts +7 -0
  144. package/src/sparkline.ts +6 -4
  145. package/src/svgrid-wrapper.types.ts +32 -0
  146. package/src/svgrid.api.test.ts +21 -0
  147. package/src/svgrid.charting.test.ts +12 -4
  148. package/src/svgrid.context-menu.test.ts +12 -0
  149. package/src/svgrid.data-push.svelte.test.ts +94 -0
  150. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  151. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  152. package/src/svgrid.gantt-seam.test.ts +1 -1
  153. package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  156. package/src/svgrid.row-pinning.test.ts +15 -1
  157. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  158. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  159. package/src/svgrid.upsell-license.test.ts +1 -1
  160. package/src/test-setup.ts +12 -0
  161. package/src/transaction.test.ts +58 -0
  162. package/src/validate.ts +7 -1
  163. package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
  164. package/src/virtualization/virtualizer.test.ts +44 -0
  165. package/src/virtualization/virtualizer.ts +13 -1
  166. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
package/dist/core.js CHANGED
@@ -130,14 +130,54 @@ export function tableFeatures(features) {
130
130
  * text; set a column's type or supply your own comparator to override.
131
131
  */
132
132
  export const sortFns = {
133
- auto: (a, b) => String(a).localeCompare(String(b)),
134
- number: (a, b) => Number(a ?? 0) - Number(b ?? 0),
135
- date: (a, b) => {
136
- const aa = new Date(a).getTime();
137
- const bb = new Date(b).getTime();
138
- return aa - bb;
139
- },
133
+ auto: (a, b) => compareAutoValues(a, b),
134
+ number: (a, b) => compareNumericKeys(numberSortKey(a), numberSortKey(b)),
135
+ date: (a, b) => compareNumericKeys(dateSortKey(a), dateSortKey(b)),
140
136
  };
137
+ /** Null, undefined and the empty string: the values every built-in sort puts first. */
138
+ function isBlankSortValue(value) {
139
+ return value == null || value === '';
140
+ }
141
+ /**
142
+ * Sort key for the `number` comparator. Blanks and anything that does not
143
+ * coerce to a number ('n/a', a placeholder) become -Infinity, so they sort
144
+ * together ahead of every number instead of producing NaN. A NaN key made the
145
+ * comparator inconsistent - NaN compared equal to everything while the numbers
146
+ * around it did not - and `Array.prototype.sort` then returned a visibly
147
+ * unsorted column (#104).
148
+ */
149
+ function numberSortKey(value) {
150
+ if (isBlankSortValue(value))
151
+ return -Infinity;
152
+ const n = Number(value);
153
+ return Number.isNaN(n) ? -Infinity : n;
154
+ }
155
+ /** Sort key for the `date` comparator; blanks and invalid dates sort first. */
156
+ function dateSortKey(value) {
157
+ if (isBlankSortValue(value))
158
+ return -Infinity;
159
+ const t = new Date(value).getTime();
160
+ return Number.isNaN(t) ? -Infinity : t;
161
+ }
162
+ /**
163
+ * The `auto` comparator: blanks first, then numbers in numeric order, then
164
+ * everything else as collated text. Ordering by kind before value keeps it
165
+ * transitive on mixed columns - comparing a number to a string as text
166
+ * instead would give 2 < 10, 10 < '1a' and '1a' < 2, a cycle.
167
+ */
168
+ function compareAutoValues(a, b) {
169
+ const aBlank = isBlankSortValue(a);
170
+ const bBlank = isBlankSortValue(b);
171
+ if (aBlank || bBlank)
172
+ return aBlank === bBlank ? 0 : aBlank ? -1 : 1;
173
+ const aNum = typeof a === 'number';
174
+ const bNum = typeof b === 'number';
175
+ if (aNum && bNum)
176
+ return compareNumericKeys(numberSortKey(a), numberSortKey(b));
177
+ if (aNum !== bNum)
178
+ return aNum ? -1 : 1;
179
+ return compareCollatedKeys(String(a), String(b));
180
+ }
141
181
  /**
142
182
  * Built-in match functions, named by {@link ColumnFilter}'s `fn`.
143
183
  * `includesString` is case-insensitive substring; `equals` is strict identity.
@@ -165,6 +205,19 @@ export const filterFns = {
165
205
  const ROW_CTX = Symbol('svgrid.row.ctx');
166
206
  const ROW_VALUES = Symbol('svgrid.row.values');
167
207
  const ROW_CELLS = Symbol('svgrid.row.cells');
208
+ // Where the filtered stage last put this row: the generation of that full
209
+ // filter and the row's index in its output. Lets a tick swap a replaced row
210
+ // into place without re-filtering everything (see createFilteredRowModel).
211
+ const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen');
212
+ const ROW_FILTER_POS = Symbol('svgrid.row.filterPos');
213
+ // Stamped on the rows a sort repair drops, so the merge skips them with one
214
+ // property read per row instead of a Set lookup (3 ms per 100k rows).
215
+ const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped');
216
+ // The key(s) the last full sort computed for a row: the key itself for a
217
+ // one-clause sort, an array for several. Read by the repair path so a tick
218
+ // never recomputes a kept row's key.
219
+ const ROW_SORT_KEY = Symbol('svgrid.row.sortKey');
220
+ let sortDropGeneration = 0;
168
221
  /**
169
222
  * The methods every base row carries, defined ONCE and assigned by reference.
170
223
  *
@@ -276,17 +329,32 @@ export function createCoreRowModel() {
276
329
  * `columnFilteringFeature`; without it there are no filters to apply.
277
330
  */
278
331
  export function createFilteredRowModel() {
332
+ /**
333
+ * The last full filter, so a tick can repair it. When `getRowModel`
334
+ * reports which rows were replaced (`replacedRowsOf`) and the filter state
335
+ * and columns are the same objects as last time, each replacement is tested
336
+ * on its own: a row that stays in keeps its slot, a row that stays out
337
+ * stays out, and a row whose membership flips sends the whole thing back
338
+ * through the full pass, because an insertion has to land in data order.
339
+ * Positions come from stamps on the rows (ROW_FILTER_GEN / ROW_FILTER_POS)
340
+ * written by the full pass; no map is built.
341
+ */
342
+ let cache = null;
343
+ let generation = 0;
279
344
  return ({ table, rows }) => {
280
345
  const filters = table.getState().columnFilters ?? [];
281
- if (!filters.length)
346
+ if (!filters.length) {
347
+ cache = null;
282
348
  return rows;
349
+ }
350
+ const columns = table.getAllColumns();
283
351
  // Resolve each filter's match function once, outside the row loop.
284
352
  const compiled = filters.map((filter) => ({
285
353
  id: filter.id,
286
354
  value: filter.value,
287
355
  fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,
288
356
  }));
289
- return rows.filter((row) => {
357
+ const passes = (row) => {
290
358
  for (let i = 0; i < compiled.length; i++) {
291
359
  const filter = compiled[i];
292
360
  // `getCellValueByColumnId` rather than `getAllCells().find(...)`.
@@ -299,7 +367,56 @@ export function createFilteredRowModel() {
299
367
  return false;
300
368
  }
301
369
  return true;
302
- });
370
+ };
371
+ if (cache && cache.filters === filters && cache.columns === columns) {
372
+ if (cache.input === rows)
373
+ return cache.output;
374
+ const replaced = replacedRowsOf(table);
375
+ if (replaced && rows.length === cache.input.length) {
376
+ let out = null;
377
+ let ok = true;
378
+ for (let i = 0; i < rows.length && ok; i++) {
379
+ const prev = cache.input[i];
380
+ const next = rows[i];
381
+ if (next === prev)
382
+ continue;
383
+ if (replaced.get(prev) !== next) {
384
+ ok = false;
385
+ break;
386
+ }
387
+ const wasIn = prev[ROW_FILTER_GEN] === cache.gen;
388
+ const isIn = passes(next);
389
+ if (wasIn !== isIn) {
390
+ ok = false;
391
+ break;
392
+ }
393
+ if (!isIn)
394
+ continue;
395
+ out ?? (out = cache.output.slice());
396
+ const pos = prev[ROW_FILTER_POS];
397
+ out[pos] = next;
398
+ next[ROW_FILTER_GEN] = cache.gen;
399
+ next[ROW_FILTER_POS] = pos;
400
+ }
401
+ if (ok) {
402
+ const output = out ?? cache.output;
403
+ cache = { ...cache, input: rows, output };
404
+ return output;
405
+ }
406
+ }
407
+ }
408
+ const gen = ++generation;
409
+ const output = [];
410
+ for (let i = 0; i < rows.length; i++) {
411
+ const row = rows[i];
412
+ if (!passes(row))
413
+ continue;
414
+ row[ROW_FILTER_GEN] = gen;
415
+ row[ROW_FILTER_POS] = output.length;
416
+ output.push(row);
417
+ }
418
+ cache = { input: rows, output, filters, columns, gen };
419
+ return output;
303
420
  };
304
421
  }
305
422
  /**
@@ -542,6 +659,19 @@ export function flattenTreeData(data, options) {
542
659
  export function createExpandedRowModel() {
543
660
  return ({ table, rows }) => {
544
661
  const expanded = table.getState().expanded ?? {};
662
+ // Nothing expanded means every collapsed row hides its children and the
663
+ // output is the input, element for element. Returning it as is keeps the
664
+ // array identity the stages downstream and the render window key on, and
665
+ // skips a 100k-element copy per data change on a flat grid.
666
+ let anyExpanded = false;
667
+ for (const id in expanded) {
668
+ if (expanded[id]) {
669
+ anyExpanded = true;
670
+ break;
671
+ }
672
+ }
673
+ if (!anyExpanded)
674
+ return rows;
545
675
  const flattened = [];
546
676
  const visit = (row) => {
547
677
  flattened.push(row);
@@ -560,11 +690,58 @@ export function createExpandedRowModel() {
560
690
  * override the built-in {@link sortFns} - useful for locale-aware or
561
691
  * domain-specific ordering.
562
692
  */
693
+ /**
694
+ * Which rows the last `getRowModel` rebuild replaced, old row -> new row, for
695
+ * the duration of the pipeline run that follows it.
696
+ *
697
+ * A live feed replaces a few hundred row objects in a hundred-thousand-row
698
+ * array and hands the grid the new array. The base rows are rebuilt reusing
699
+ * every row whose data object is the same (see `getRowModel`), so the rebuild
700
+ * knows exactly which rows are new. The filtered and sorted stages read that
701
+ * here and repair their previous output instead of recomputing it, when the
702
+ * replacements are few and nothing else changed. Cleared after the run; a
703
+ * stage that reads it outside a run sees null and does the full work.
704
+ *
705
+ * Keyed on the grid object in a WeakMap rather than carried on the public
706
+ * `SvGrid` type, so the stage signature stays `({ table, rows }) => rows`.
707
+ */
708
+ const replacedRowsByGrid = new WeakMap();
709
+ /** The rows `getRowModel` replaced in the rebuild this pipeline run follows, or null. */
710
+ export function replacedRowsOf(table) {
711
+ return replacedRowsByGrid.get(table) ?? null;
712
+ }
713
+ /**
714
+ * The sorting stage of the row pipeline: orders the rows by the grid's
715
+ * `sorting` state with the built-in comparators (or the `localSortFns` map
716
+ * handed in), stable, equal keys keeping their data order. A tick that
717
+ * replaces a few rows out of many is repaired incrementally rather than
718
+ * re-sorted (see the note inside). Registered by `rowSortingFeature`; an app
719
+ * only calls this to install its own comparator set.
720
+ */
563
721
  export function createSortedRowModel(localSortFns = sortFns) {
564
- return function sortedRowModelStage({ table, rows }) {
565
- const sorting = table.getState().sorting ?? [];
566
- if (!sorting.length)
567
- return rows;
722
+ /**
723
+ * What the last full sort produced, kept so a tick can repair it.
724
+ *
725
+ * A live feed replaces a few hundred row objects out of a hundred thousand
726
+ * and hands the grid a new array. Re-sorting all of it costs the same as
727
+ * the first sort did (115-149 ms in the browser for 100k rows, see
728
+ * docs/help/benchmarks.md); the order it would produce differs from the
729
+ * previous one only where the replaced rows land. So the previous output
730
+ * is kept, with its sort keys aligned to it, and when `getRowModel` says
731
+ * which rows were replaced (see `replacedRowsOf`) the stage drops those,
732
+ * sorts the replacements among themselves and merges them back in one
733
+ * linear pass: O(n + k log k) instead of O(n log n) key builds and
734
+ * comparisons.
735
+ *
736
+ * The repair is only attempted when everything else is the same object as
737
+ * last time - the sorting state, the columns, and every input row except
738
+ * the replaced ones - and it produces exactly what the full sort would:
739
+ * equal keys fall back to the data index, which is the order a stable sort
740
+ * over data-ordered input keeps.
741
+ */
742
+ let cache = null;
743
+ const MISSING_KEY = Symbol('svgrid.sort.missing-key');
744
+ function fullSort(rows, sorting, allColumns) {
568
745
  // Resolve every clause ONCE, before sorting.
569
746
  //
570
747
  // This used to live inside the comparator, so `getAllColumns().find(...)`
@@ -572,7 +749,6 @@ export function createSortedRowModel(localSortFns = sortFns) {
572
749
  // 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;
573
750
  // `pnpm bench --case=sort-1col`). The comparator is called O(n log n)
574
751
  // times, so anything inside it that is not O(1) sets the cost of the sort.
575
- const allColumns = table.getAllColumns();
576
752
  const clauses = [];
577
753
  for (const clause of sorting) {
578
754
  const column = allColumns.find((col) => col.id === clause.id);
@@ -599,17 +775,43 @@ export function createSortedRowModel(localSortFns = sortFns) {
599
775
  const n = rows.length;
600
776
  let keys = new Array(n);
601
777
  let compare;
778
+ let keyOf;
779
+ let autoMode;
602
780
  if (comparator === sortFns.number) {
781
+ keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId));
603
782
  for (let i = 0; i < n; i++)
604
- keys[i] = Number(rows[i].getCellValueByColumnId(columnId) ?? 0);
783
+ keys[i] = keyOf(rows[i]);
605
784
  compare = compareNumericKeys;
606
785
  }
607
786
  else if (comparator === sortFns.date) {
608
- for (let i = 0; i < n; i++) {
609
- keys[i] = new Date(rows[i].getCellValueByColumnId(columnId)).getTime();
610
- }
787
+ keyOf = (row) => dateSortKey(row.getCellValueByColumnId(columnId));
788
+ for (let i = 0; i < n; i++)
789
+ keys[i] = keyOf(rows[i]);
790
+ compare = compareNumericKeys;
791
+ }
792
+ else if (comparator === sortFns.auto && (autoMode = autoSortMode(rows, columnId)) === 'number') {
793
+ // A column with no declared type whose values are all numbers. Sorting
794
+ // it as text put 10 before 2 (#104).
795
+ keyOf = (row) => {
796
+ const value = row.getCellValueByColumnId(columnId);
797
+ // A text value arriving in a tick changes the mode; only a full sort
798
+ // can place it.
799
+ return typeof value === 'number' || isBlankSortValue(value)
800
+ ? numberSortKey(value)
801
+ : MISSING_KEY;
802
+ };
803
+ for (let i = 0; i < n; i++)
804
+ keys[i] = keyOf(rows[i]);
611
805
  compare = compareNumericKeys;
612
806
  }
807
+ else if (autoMode === 'mixed') {
808
+ // Numbers and text in one column. Rare, so no key tricks: the raw
809
+ // values through the pairwise comparator, which orders by kind first.
810
+ keyOf = (row) => row.getCellValueByColumnId(columnId);
811
+ for (let i = 0; i < n; i++)
812
+ keys[i] = keyOf(rows[i]);
813
+ compare = compareAutoValues;
814
+ }
613
815
  else if (comparator === sortFns.auto) {
614
816
  const strings = new Array(n);
615
817
  // Decide whether ranking is worth attempting BEFORE paying for it.
@@ -632,7 +834,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
632
834
  const sample = new Set();
633
835
  let sampled = 0;
634
836
  for (let i = 0; i < n; i += stride) {
635
- sample.add(String(rows[i].getCellValueByColumnId(columnId)));
837
+ sample.add(autoTextKey(rows[i].getCellValueByColumnId(columnId)));
636
838
  sampled++;
637
839
  }
638
840
  // Only attempt ranking when the sample suggests real repetition.
@@ -640,7 +842,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
640
842
  distinct = new Set();
641
843
  }
642
844
  for (let i = 0; i < n; i++) {
643
- const s = String(rows[i].getCellValueByColumnId(columnId));
845
+ const s = autoTextKey(rows[i].getCellValueByColumnId(columnId));
644
846
  strings[i] = s;
645
847
  if (distinct) {
646
848
  distinct.add(s);
@@ -669,21 +871,35 @@ export function createSortedRowModel(localSortFns = sortFns) {
669
871
  for (let i = 0; i < n; i++)
670
872
  keys[i] = rankOf.get(strings[i]);
671
873
  compare = compareNumericKeys;
874
+ // A replacement whose text is not among the ranked values has no
875
+ // rank; the repair path treats that as "sort everything again".
876
+ keyOf = (row) => {
877
+ const value = row.getCellValueByColumnId(columnId);
878
+ // A number arriving in a tick makes the column mixed; resort fully.
879
+ if (typeof value === 'number')
880
+ return MISSING_KEY;
881
+ return rankOf.get(autoTextKey(value)) ?? MISSING_KEY;
882
+ };
672
883
  }
673
884
  else {
674
885
  keys = strings;
675
886
  compare = compareCollatedKeys;
887
+ keyOf = (row) => {
888
+ const value = row.getCellValueByColumnId(columnId);
889
+ return typeof value === 'number' ? MISSING_KEY : autoTextKey(value);
890
+ };
676
891
  }
677
892
  }
678
893
  else {
894
+ keyOf = (row) => row.getCellValueByColumnId(columnId);
679
895
  for (let i = 0; i < n; i++)
680
- keys[i] = rows[i].getCellValueByColumnId(columnId);
896
+ keys[i] = keyOf(rows[i]);
681
897
  compare = comparator;
682
898
  }
683
- clauses.push({ keys, desc: clause.desc, compare });
899
+ clauses.push({ keys, desc: clause.desc, compare, keyOf });
684
900
  }
685
901
  if (!clauses.length)
686
- return rows;
902
+ return null;
687
903
  // Sort an index array, then materialise. `Array.prototype.sort` is stable,
688
904
  // so equal keys keep their original relative order exactly as the previous
689
905
  // `[...rows].sort(...)` did.
@@ -730,18 +946,203 @@ export function createSortedRowModel(localSortFns = sortFns) {
730
946
  const sorted = new Array(rows.length);
731
947
  for (let i = 0; i < order.length; i++)
732
948
  sorted[i] = rows[order[i]];
733
- return sorted;
949
+ // Each row keeps its key(s), so a later tick can place a replacement
950
+ // among the kept rows without reading any of them again. One property
951
+ // write per row; an array per row only when there are several clauses.
952
+ if (clauses.length === 1) {
953
+ const keys = clauses[0].keys;
954
+ for (let i = 0; i < rows.length; i++)
955
+ rows[i][ROW_SORT_KEY] = keys[i];
956
+ }
957
+ else {
958
+ for (let i = 0; i < rows.length; i++) {
959
+ const per = new Array(clauses.length);
960
+ for (let k = 0; k < clauses.length; k++)
961
+ per[k] = clauses[k].keys[i];
962
+ rows[i][ROW_SORT_KEY] = per;
963
+ }
964
+ }
965
+ return { sorted, clauses: clauses.map(({ desc, compare, keyOf }) => ({ desc, compare, keyOf })) };
966
+ }
967
+ /**
968
+ * The previous output with the replaced rows swapped for their
969
+ * replacements, in sorted position. Null when the input is not the cached
970
+ * input plus replacements, or a replacement has no incremental key.
971
+ *
972
+ * Every row carries the key(s) the last full sort computed for it
973
+ * (ROW_SORT_KEY: the key itself for one clause, an array for several), so
974
+ * the merge moves row references and nothing else. At 100k rows the loop
975
+ * that also copied an aligned key array cost 3.7 ms; this one is the
976
+ * reference copy alone.
977
+ */
978
+ function repair(rows, replaced) {
979
+ const c = cache;
980
+ if (rows.length !== c.input.length)
981
+ return null;
982
+ // Which rows of THIS stage's input changed. The map may name rows a
983
+ // stage upstream filtered out; only the ones that reach here matter.
984
+ const dropGen = ++sortDropGeneration;
985
+ const added = [];
986
+ for (let i = 0; i < rows.length; i++) {
987
+ const prev = c.input[i];
988
+ const next = rows[i];
989
+ if (next === prev)
990
+ continue;
991
+ if (replaced.get(prev) !== next)
992
+ return null;
993
+ prev[ROW_SORT_DROPPED] = dropGen;
994
+ added.push(next);
995
+ }
996
+ if (!added.length)
997
+ return c.output;
998
+ const clauses = c.clauses;
999
+ const single = clauses.length === 1;
1000
+ for (let j = 0; j < added.length; j++) {
1001
+ const row = added[j];
1002
+ if (single) {
1003
+ const key = clauses[0].keyOf(row);
1004
+ if (key === MISSING_KEY)
1005
+ return null;
1006
+ row[ROW_SORT_KEY] = key;
1007
+ }
1008
+ else {
1009
+ const keys = new Array(clauses.length);
1010
+ for (let k = 0; k < clauses.length; k++) {
1011
+ const key = clauses[k].keyOf(row);
1012
+ if (key === MISSING_KEY)
1013
+ return null;
1014
+ keys[k] = key;
1015
+ }
1016
+ row[ROW_SORT_KEY] = keys;
1017
+ }
1018
+ }
1019
+ // Keys first, then the data index: the tie-break a stable sort over
1020
+ // data-ordered input applies, so the merge lands where a full sort would.
1021
+ const prevOut = c.output;
1022
+ const compareRows = single
1023
+ ? (() => {
1024
+ const { compare, desc } = clauses[0];
1025
+ return (a, b) => {
1026
+ const r = compare(a[ROW_SORT_KEY], b[ROW_SORT_KEY]);
1027
+ if (r !== 0)
1028
+ return desc ? -r : r;
1029
+ return a.index - b.index;
1030
+ };
1031
+ })()
1032
+ : (a, b) => {
1033
+ const ka = a[ROW_SORT_KEY];
1034
+ const kb = b[ROW_SORT_KEY];
1035
+ for (let k = 0; k < clauses.length; k++) {
1036
+ const clause = clauses[k];
1037
+ const r = clause.compare(ka[k], kb[k]);
1038
+ if (r !== 0)
1039
+ return clause.desc ? -r : r;
1040
+ }
1041
+ return a.index - b.index;
1042
+ };
1043
+ // Sort the replacements among themselves (k log k)...
1044
+ added.sort(compareRows);
1045
+ // ...find where each one goes in the previous output by binary search
1046
+ // (k log n comparisons, against the dropped rows too - they are skipped
1047
+ // below, which does not move an insertion point)...
1048
+ const n = prevOut.length;
1049
+ const insertAt = new Array(added.length);
1050
+ let lo = 0;
1051
+ for (let j = 0; j < added.length; j++) {
1052
+ // Monotone: the replacements are sorted, so each search starts where
1053
+ // the previous one landed.
1054
+ const row = added[j];
1055
+ let hi = n;
1056
+ while (lo < hi) {
1057
+ const mid = (lo + hi) >>> 1;
1058
+ if (compareRows(row, prevOut[mid]) < 0)
1059
+ hi = mid;
1060
+ else
1061
+ lo = mid + 1;
1062
+ }
1063
+ insertAt[j] = lo;
1064
+ }
1065
+ // ...then one linear pass that only moves references.
1066
+ const out = new Array(rows.length);
1067
+ let w = 0;
1068
+ let j = 0;
1069
+ let nextInsert = insertAt[0] ?? -1;
1070
+ for (let i = 0; i < n; i++) {
1071
+ while (nextInsert === i) {
1072
+ out[w++] = added[j++];
1073
+ nextInsert = j < added.length ? insertAt[j] : -1;
1074
+ }
1075
+ const prev = prevOut[i];
1076
+ if (prev[ROW_SORT_DROPPED] === dropGen)
1077
+ continue;
1078
+ out[w++] = prev;
1079
+ }
1080
+ while (j < added.length)
1081
+ out[w++] = added[j++];
1082
+ return out;
1083
+ }
1084
+ return function sortedRowModelStage({ table, rows }) {
1085
+ const sorting = table.getState().sorting ?? [];
1086
+ if (!sorting.length) {
1087
+ cache = null;
1088
+ return rows;
1089
+ }
1090
+ const columns = table.getAllColumns();
1091
+ if (cache && cache.sorting === sorting && cache.columns === columns) {
1092
+ if (cache.input === rows)
1093
+ return cache.output;
1094
+ const replaced = replacedRowsOf(table);
1095
+ if (replaced) {
1096
+ const repaired = repair(rows, replaced);
1097
+ if (repaired) {
1098
+ cache = { ...cache, input: rows, output: repaired };
1099
+ return repaired;
1100
+ }
1101
+ }
1102
+ }
1103
+ const full = fullSort(rows, sorting, columns);
1104
+ if (!full) {
1105
+ cache = null;
1106
+ return rows;
1107
+ }
1108
+ cache = { input: rows, output: full.sorted, sorting, columns, clauses: full.clauses };
1109
+ return full.sorted;
734
1110
  };
735
1111
  }
736
1112
  /**
737
- * Numeric key comparison for the built-in `number` and `date` comparators.
738
- * Subtraction rather than `<`/`>` on purpose: it reproduces the originals
739
- * exactly, NaN included. An unparseable date or a non-numeric value yields NaN,
740
- * and the sort spec turns a NaN comparison result into 0 (SortCompare coerces
741
- * it), which is the behaviour callers already depend on.
1113
+ * Numeric key comparison for the built-in `number`, `date` and numeric
1114
+ * `auto` comparators. Keys are never NaN (blanks and unparseable values map to
1115
+ * -Infinity), but two -Infinity keys subtract to NaN, so equal keys return 0
1116
+ * explicitly: the multi-clause sort reads any non-zero result, NaN included,
1117
+ * as decided and would skip the next clause.
742
1118
  */
743
1119
  function compareNumericKeys(a, b) {
744
- return a - b;
1120
+ return a === b ? 0 : a - b;
1121
+ }
1122
+ /** Text key for the `auto` comparator: blanks become '' so they collate first. */
1123
+ function autoTextKey(value) {
1124
+ return isBlankSortValue(value) ? '' : String(value);
1125
+ }
1126
+ /**
1127
+ * How the `auto` comparator should treat a column: 'number' when every
1128
+ * non-blank value is a number, 'mixed' when numbers share it with anything
1129
+ * else, 'text' otherwise. One pass, stopping as soon as the answer is known.
1130
+ */
1131
+ function autoSortMode(rows, columnId) {
1132
+ let numbers = false;
1133
+ let other = false;
1134
+ for (let i = 0; i < rows.length; i++) {
1135
+ const value = rows[i].getCellValueByColumnId(columnId);
1136
+ if (isBlankSortValue(value))
1137
+ continue;
1138
+ if (typeof value === 'number')
1139
+ numbers = true;
1140
+ else
1141
+ other = true;
1142
+ if (numbers && other)
1143
+ return 'mixed';
1144
+ }
1145
+ return numbers ? 'number' : 'text';
745
1146
  }
746
1147
  /**
747
1148
  * Text key comparison for the built-in `auto` comparator.
@@ -788,6 +1189,8 @@ export function createSvGridCore(options) {
788
1189
  let cachedBaseRowsInput = null;
789
1190
  let cachedBaseRowsColumns = null;
790
1191
  let cachedBaseRows = [];
1192
+ /** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */
1193
+ let replacedRows = null;
791
1194
  let cachedRowCtx = null;
792
1195
  let cachedRowModel = null;
793
1196
  let cachedRowModelBaseRows = null;
@@ -968,7 +1371,11 @@ export function createSvGridCore(options) {
968
1371
  },
969
1372
  getRowModel() {
970
1373
  const columns = grid.getAllColumns();
971
- if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
1374
+ // Read once. `options.data` is a getter when <SvGrid> drives the engine
1375
+ // (a $state.raw signal), and the rebuild below touched it twice per row:
1376
+ // 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.
1377
+ const data = options.data;
1378
+ if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {
972
1379
  // Same columns as last time: the shared context still describes them,
973
1380
  // and a row whose data object sits at the same index can keep its
974
1381
  // row object. A row model that streams blocks hands the grid a new
@@ -978,7 +1385,11 @@ export function createSvGridCore(options) {
978
1385
  // an app may have changed the object in place before passing a new
979
1386
  // array - that is the case the old rebuild covered by accident.
980
1387
  const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null;
981
- cachedBaseRowsInput = options.data;
1388
+ // Same length as last time: a replacement, not an add or remove, so
1389
+ // each new row stands in for the old row at its index and the stages
1390
+ // can repair rather than recompute. Anything else is structural.
1391
+ replacedRows = previous && previous.length === data.length ? new Map() : null;
1392
+ cachedBaseRowsInput = data;
982
1393
  cachedBaseRowsColumns = columns;
983
1394
  // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
984
1395
  // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
@@ -1001,11 +1412,11 @@ export function createSvGridCore(options) {
1001
1412
  };
1002
1413
  cachedRowCtx = rowCtx;
1003
1414
  }
1004
- cachedBaseRows = new Array(options.data.length);
1415
+ cachedBaseRows = new Array(data.length);
1005
1416
  const getRowId = options.getRowId;
1006
1417
  const m = BASE_ROW_METHODS;
1007
- for (let index = 0; index < options.data.length; index++) {
1008
- const original = options.data[index];
1418
+ for (let index = 0; index < data.length; index++) {
1419
+ const original = data[index];
1009
1420
  const kept = previous?.[index];
1010
1421
  if (kept && kept.original === original) {
1011
1422
  kept[ROW_VALUES] = null;
@@ -1013,6 +1424,10 @@ export function createSvGridCore(options) {
1013
1424
  cachedBaseRows[index] = kept;
1014
1425
  continue;
1015
1426
  }
1427
+ // Past a quarter of the rows the merge stops being cheaper than a
1428
+ // sort with the ranking pass; let the stages do the full work.
1429
+ if (replacedRows && replacedRows.size * 4 >= data.length)
1430
+ replacedRows = null;
1016
1431
  // `_values` and `_cells` stay null until something reads them - a
1017
1432
  // 100k-row grid showing twenty rows must not materialise every row's
1018
1433
  // values or cell objects to paint.
@@ -1024,6 +1439,10 @@ export function createSvGridCore(options) {
1024
1439
  [ROW_CTX]: rowCtx,
1025
1440
  [ROW_VALUES]: null,
1026
1441
  [ROW_CELLS]: null,
1442
+ [ROW_FILTER_GEN]: 0,
1443
+ [ROW_FILTER_POS]: -1,
1444
+ [ROW_SORT_DROPPED]: 0,
1445
+ [ROW_SORT_KEY]: undefined,
1027
1446
  getCanExpand: m.getCanExpand,
1028
1447
  getIsExpanded: m.getIsExpanded,
1029
1448
  toggleExpanded: m.toggleExpanded,
@@ -1033,7 +1452,17 @@ export function createSvGridCore(options) {
1033
1452
  getCellValueByColumnId: m.getCellValueByColumnId,
1034
1453
  };
1035
1454
  cachedBaseRows[index] = row;
1455
+ if (replacedRows && kept)
1456
+ replacedRows.set(kept, row);
1036
1457
  }
1458
+ // A new array with every object the same is the documented way to
1459
+ // say "I changed rows in place, re-read them" (docs/help/cells/
1460
+ // view-refresh.md). Nothing was replaced, so there is nothing to
1461
+ // repair around: the stages recompute from the current values. A
1462
+ // repair assumes the rows it keeps are unchanged in value as well as
1463
+ // identity, which is the immutable-update contract a tick follows.
1464
+ if (replacedRows && replacedRows.size === 0)
1465
+ replacedRows = null;
1037
1466
  }
1038
1467
  // Only the slices a pipeline stage actually READS belong in this key.
1039
1468
  //
@@ -1075,10 +1504,19 @@ export function createSvGridCore(options) {
1075
1504
  pipeline.expandedRowModel,
1076
1505
  pipeline.paginatedRowModel,
1077
1506
  ];
1078
- ordered.forEach((fn) => {
1079
- if (fn)
1080
- rows = fn({ table: grid, rows });
1081
- });
1507
+ // The stages read the replacements through `replacedRowsOf` while the
1508
+ // run lasts; a stage that runs outside one sees null.
1509
+ replacedRowsByGrid.set(grid, replacedRows);
1510
+ try {
1511
+ ordered.forEach((fn) => {
1512
+ if (fn)
1513
+ rows = fn({ table: grid, rows });
1514
+ });
1515
+ }
1516
+ finally {
1517
+ replacedRowsByGrid.set(grid, null);
1518
+ replacedRows = null;
1519
+ }
1082
1520
  cachedPipeline = options._rowModels;
1083
1521
  cachedSlices = currentSlices;
1084
1522
  cachedRowModelBaseRows = cachedBaseRows;