@svgrid/grid 3.0.4 → 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 (176) hide show
  1. package/README.md +32 -11
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +33 -5
  4. package/dist/SvGrid.controller.svelte.js +397 -58
  5. package/dist/SvGrid.css +200 -2
  6. package/dist/SvGrid.svelte +350 -40
  7. package/dist/SvGrid.types.d.ts +433 -7
  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 +92 -37
  15. package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
  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-DIqb3VZn.js +241 -0
  45. package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
  46. package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
  47. package/dist/cdn/svgrid.js +28 -27
  48. package/dist/cdn/svgrid.svelte-external.js +28 -27
  49. package/dist/cdn/validate-AZoD8BoR.js +80 -0
  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 +401 -38
  59. package/dist/editing.js +34 -0
  60. package/dist/gantt-view.svelte.d.ts +24 -0
  61. package/dist/gantt-view.svelte.js +13 -0
  62. package/dist/grid-icons.d.ts +2 -2
  63. package/dist/grid-icons.js +2 -2
  64. package/dist/grid-messages.d.ts +10 -0
  65. package/dist/grid-messages.js +9 -0
  66. package/dist/headless.d.ts +5 -0
  67. package/dist/headless.js +12 -0
  68. package/dist/history.d.ts +21 -0
  69. package/dist/history.js +34 -4
  70. package/dist/index.d.ts +5 -4
  71. package/dist/index.js +3 -3
  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 +139 -0
  77. package/dist/row-model.js +16 -0
  78. package/dist/row-resize.js +22 -1
  79. package/dist/selection.d.ts +2 -1
  80. package/dist/selection.js +31 -3
  81. package/dist/server-block-cache.d.ts +219 -0
  82. package/dist/server-block-cache.js +539 -0
  83. package/dist/server-data-source.d.ts +274 -8
  84. package/dist/server-data-source.js +229 -20
  85. package/dist/server.d.ts +13 -0
  86. package/dist/server.js +13 -0
  87. package/dist/sparkline.js +7 -5
  88. package/dist/svgrid-wrapper.types.d.ts +33 -0
  89. package/dist/validate.d.ts +4 -0
  90. package/dist/validate.js +29 -3
  91. package/package.json +11 -1
  92. package/src/GridMenus.svelte +34 -4
  93. package/src/SvGrid.controller.svelte.ts +397 -61
  94. package/src/SvGrid.css +200 -2
  95. package/src/SvGrid.svelte +350 -40
  96. package/src/SvGrid.types.ts +453 -7
  97. package/src/SvGridDropdown.svelte +20 -0
  98. package/src/SvModal.svelte +22 -0
  99. package/src/SvModal.test.ts +30 -1
  100. package/src/a11y/dismissable.test.ts +10 -1
  101. package/src/a11y/dismissable.ts +22 -2
  102. package/src/build-api.ts +96 -31
  103. package/src/cell-formatting.test.ts +17 -0
  104. package/src/cell-formatting.ts +18 -1
  105. package/src/cell-render.test.ts +24 -2
  106. package/src/cell-render.ts +24 -8
  107. package/src/clipboard.test.ts +107 -0
  108. package/src/clipboard.ts +62 -23
  109. package/src/columns.test.ts +4 -4
  110. package/src/columns.ts +7 -2
  111. package/src/conditional-formatting.test.ts +8 -0
  112. package/src/conditional-formatting.ts +3 -0
  113. package/src/core.rowmodel-cache.test.ts +51 -0
  114. package/src/core.tick-repair.test.ts +287 -0
  115. package/src/core.ts +407 -38
  116. package/src/editing.test.ts +52 -0
  117. package/src/editing.ts +33 -0
  118. package/src/gantt-stub.test.svelte +38 -0
  119. package/src/gantt-view.svelte.ts +35 -0
  120. package/src/grid-icons.ts +2 -2
  121. package/src/grid-messages.test.ts +7 -0
  122. package/src/grid-messages.ts +22 -0
  123. package/src/headless.ts +24 -0
  124. package/src/history.test.ts +49 -1
  125. package/src/history.ts +48 -2
  126. package/src/icon-seam.test.ts +2 -4
  127. package/src/index.ts +38 -10
  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 +152 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.test.ts +3 -0
  137. package/src/selection.ts +33 -3
  138. package/src/server-block-cache.test.ts +672 -0
  139. package/src/server-block-cache.ts +690 -0
  140. package/src/server-data-source.infinite.test.ts +405 -0
  141. package/src/server-data-source.test.ts +19 -0
  142. package/src/server-data-source.ts +496 -29
  143. package/src/server.ts +49 -0
  144. package/src/sparkline.test.ts +7 -0
  145. package/src/sparkline.ts +6 -4
  146. package/src/svgrid-wrapper.types.ts +32 -0
  147. package/src/svgrid.api.test.ts +21 -0
  148. package/src/svgrid.charting.test.ts +12 -4
  149. package/src/svgrid.context-menu.test.ts +12 -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 +199 -0
  153. package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  156. package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
  157. package/src/svgrid.row-pinning.test.ts +15 -1
  158. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  159. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  160. package/src/svgrid.upsell-license.test.ts +8 -5
  161. package/src/test-setup.ts +12 -0
  162. package/src/transaction.test.ts +58 -0
  163. package/src/validate.test.ts +29 -0
  164. package/src/validate.ts +34 -3
  165. package/dist/SvGroupCell.svelte +0 -141
  166. package/dist/SvGroupCell.svelte.d.ts +0 -49
  167. package/dist/SvRowGroupPanel.svelte +0 -186
  168. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  169. package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
  170. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  171. package/dist/server-group-model.d.ts +0 -98
  172. package/dist/server-group-model.js +0 -263
  173. package/src/SvGroupCell.svelte +0 -141
  174. package/src/SvRowGroupPanel.svelte +0 -186
  175. package/src/server-group-model.test.ts +0 -294
  176. package/src/server-group-model.ts +0 -370
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,9 @@ 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;
1096
+ let cachedRowCtx = null;
791
1097
  let cachedRowModel = null;
792
1098
  let cachedRowModelBaseRows = null;
793
1099
  let cachedPipeline = options._rowModels;
@@ -967,29 +1273,63 @@ export function createSvGridCore(options) {
967
1273
  },
968
1274
  getRowModel() {
969
1275
  const columns = grid.getAllColumns();
970
- if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
971
- cachedBaseRowsInput = options.data;
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) {
1281
+ // Same columns as last time: the shared context still describes them,
1282
+ // and a row whose data object sits at the same index can keep its
1283
+ // row object. A row model that streams blocks hands the grid a new
1284
+ // array of 60k entries per block where 100 changed; building 60k
1285
+ // fresh row objects each time was most of the cost of a block
1286
+ // landing. The reused row drops its memoised values and cells, since
1287
+ // an app may have changed the object in place before passing a new
1288
+ // array - that is the case the old rebuild covered by accident.
1289
+ const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null;
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;
972
1295
  cachedBaseRowsColumns = columns;
973
1296
  // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
974
1297
  // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
975
- const columnIndexById = new Map();
976
- for (let i = 0; i < columns.length; i++)
977
- columnIndexById.set(columns[i].id, i);
978
- const columnCount = columns.length;
979
- // One shared context for every row in this table, so a row carries a
980
- // pointer rather than a closure scope. See BASE_ROW_METHODS.
981
- const rowCtx = {
982
- grid: grid,
983
- store,
984
- columns,
985
- columnCount,
986
- columnIndexById,
987
- };
988
- cachedBaseRows = new Array(options.data.length);
1298
+ let rowCtx;
1299
+ if (previous && cachedRowCtx) {
1300
+ rowCtx = cachedRowCtx;
1301
+ }
1302
+ else {
1303
+ const columnIndexById = new Map();
1304
+ for (let i = 0; i < columns.length; i++)
1305
+ columnIndexById.set(columns[i].id, i);
1306
+ // One shared context for every row in this table, so a row carries a
1307
+ // pointer rather than a closure scope. See BASE_ROW_METHODS.
1308
+ rowCtx = {
1309
+ grid: grid,
1310
+ store,
1311
+ columns,
1312
+ columnCount: columns.length,
1313
+ columnIndexById,
1314
+ };
1315
+ cachedRowCtx = rowCtx;
1316
+ }
1317
+ cachedBaseRows = new Array(data.length);
989
1318
  const getRowId = options.getRowId;
990
1319
  const m = BASE_ROW_METHODS;
991
- for (let index = 0; index < options.data.length; index++) {
992
- const original = options.data[index];
1320
+ for (let index = 0; index < data.length; index++) {
1321
+ const original = data[index];
1322
+ const kept = previous?.[index];
1323
+ if (kept && kept.original === original) {
1324
+ kept[ROW_VALUES] = null;
1325
+ kept[ROW_CELLS] = null;
1326
+ cachedBaseRows[index] = kept;
1327
+ continue;
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;
993
1333
  // `_values` and `_cells` stay null until something reads them - a
994
1334
  // 100k-row grid showing twenty rows must not materialise every row's
995
1335
  // values or cell objects to paint.
@@ -1001,6 +1341,10 @@ export function createSvGridCore(options) {
1001
1341
  [ROW_CTX]: rowCtx,
1002
1342
  [ROW_VALUES]: null,
1003
1343
  [ROW_CELLS]: null,
1344
+ [ROW_FILTER_GEN]: 0,
1345
+ [ROW_FILTER_POS]: -1,
1346
+ [ROW_SORT_DROPPED]: 0,
1347
+ [ROW_SORT_KEY]: undefined,
1004
1348
  getCanExpand: m.getCanExpand,
1005
1349
  getIsExpanded: m.getIsExpanded,
1006
1350
  toggleExpanded: m.toggleExpanded,
@@ -1010,7 +1354,17 @@ export function createSvGridCore(options) {
1010
1354
  getCellValueByColumnId: m.getCellValueByColumnId,
1011
1355
  };
1012
1356
  cachedBaseRows[index] = row;
1357
+ if (replacedRows && kept)
1358
+ replacedRows.set(kept, row);
1013
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;
1014
1368
  }
1015
1369
  // Only the slices a pipeline stage actually READS belong in this key.
1016
1370
  //
@@ -1052,10 +1406,19 @@ export function createSvGridCore(options) {
1052
1406
  pipeline.expandedRowModel,
1053
1407
  pipeline.paginatedRowModel,
1054
1408
  ];
1055
- ordered.forEach((fn) => {
1056
- if (fn)
1057
- rows = fn({ table: grid, rows });
1058
- });
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
+ }
1059
1422
  cachedPipeline = options._rowModels;
1060
1423
  cachedSlices = currentSlices;
1061
1424
  cachedRowModelBaseRows = cachedBaseRows;
package/dist/editing.js CHANGED
@@ -28,6 +28,16 @@ export function createEditing(ctx) {
28
28
  const editable = column.columnDef.editable;
29
29
  if (editable === false)
30
30
  return false;
31
+ if (row) {
32
+ // A server-side group row is a key with aggregates under the leaf
33
+ // columns, and a placeholder row has no data yet: neither takes an
34
+ // edit, whatever the column says.
35
+ const serverGroup = ctx.props.serverGroup;
36
+ if (serverGroup && row.original !== undefined && serverGroup.isGroup(row.original))
37
+ return false;
38
+ if (ctx.placeholderStateOf?.(row))
39
+ return false;
40
+ }
31
41
  if (typeof editable !== "function")
32
42
  return true;
33
43
  if (!row)
@@ -123,6 +133,22 @@ export function createEditing(ctx) {
123
133
  // belongs to the same Ctrl+Z as the value.
124
134
  runHistoryGroup(ctx, () => commitEditingCell());
125
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
+ }
126
152
  function commitEditingCell() {
127
153
  if (!ctx.editingCell)
128
154
  return;
@@ -155,6 +181,12 @@ export function createEditing(ctx) {
155
181
  columnId: ctx.editingCell.columnId,
156
182
  });
157
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
+ }
158
190
  rowData[column.columnDef.field] = finalValue;
159
191
  }
160
192
  const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
@@ -754,6 +786,8 @@ export function createEditing(ctx) {
754
786
  columnId: column.id,
755
787
  })
756
788
  : parsed;
789
+ if (editRejected(column, finalValue, row.original))
790
+ continue;
757
791
  if (oldValue !== finalValue) {
758
792
  row.original[field] = finalValue;
759
793
  const key = getCellKey(fr.rowId, column.id);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * gantt-view registry - the injection seam for the *Gantt view of the grid*.
3
+ * Setting the `gantt` prop on `<SvGrid>` switches the table for a task table
4
+ * beside a time chart (bars by start / end, a work-breakdown tree, dependency
5
+ * arrows), but the renderer itself ships in `@svgrid/enterprise` (a paid
6
+ * feature). This tiny reactive holder lets enterprise register that renderer at
7
+ * import time; the grid looks it up and mounts it, or shows an upsell
8
+ * placeholder when it is absent. Mirrors the `scheduler-view` / `board-view`
9
+ * pattern.
10
+ *
11
+ * ```ts
12
+ * // in @svgrid/enterprise, at module load:
13
+ * import { registerGanttView } from '@svgrid/grid'
14
+ * import SvGridGantt from './gantt/SvGridGantt.svelte'
15
+ * registerGanttView(SvGridGantt)
16
+ * ```
17
+ */
18
+ import type { Component } from 'svelte';
19
+ /** Register the component that renders `gantt` mode. Enterprise calls this. */
20
+ export declare function registerGanttView(component: Component<any>): void;
21
+ /** The registered Gantt renderer, or null when enterprise is not installed. */
22
+ export declare function getGanttView(): Component<any> | null;
23
+ /** Whether a Gantt renderer has been registered. */
24
+ export declare function hasGanttView(): boolean;
@@ -0,0 +1,13 @@
1
+ let renderer = $state(null);
2
+ /** Register the component that renders `gantt` mode. Enterprise calls this. */
3
+ export function registerGanttView(component) {
4
+ renderer = component;
5
+ }
6
+ /** The registered Gantt renderer, or null when enterprise is not installed. */
7
+ export function getGanttView() {
8
+ return renderer;
9
+ }
10
+ /** Whether a Gantt renderer has been registered. */
11
+ export function hasGanttView() {
12
+ return renderer != null;
13
+ }