@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.
- package/README.md +28 -7
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +21 -3
- package/dist/SvGrid.controller.svelte.js +205 -50
- package/dist/SvGrid.css +127 -2
- package/dist/SvGrid.svelte +223 -38
- package/dist/SvGrid.types.d.ts +77 -4
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +81 -31
- package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-C-NtjlrJ.js} +150 -141
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
- package/dist/cdn/{src-jcFMkBnN.js → src-CgoeSM8k.js} +4697 -4131
- package/dist/cdn/{src-Bklofsad.js → src-DfwxB1TH.js} +4727 -4161
- package/dist/cdn/svgrid.js +17 -17
- package/dist/cdn/svgrid.svelte-external.js +17 -17
- package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +365 -25
- package/dist/editing.js +24 -0
- package/dist/grid-messages.d.ts +4 -0
- package/dist/grid-messages.js +4 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +1 -1
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +6 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.js +1 -1
- package/dist/server-block-cache.d.ts +5 -0
- package/dist/server-block-cache.js +8 -0
- package/dist/server-data-source.d.ts +35 -3
- package/dist/server-data-source.js +11 -2
- package/dist/server.d.ts +1 -1
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.js +9 -1
- package/package.json +6 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +206 -53
- package/src/SvGrid.css +127 -2
- package/src/SvGrid.svelte +223 -38
- package/src/SvGrid.types.ts +77 -4
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +85 -27
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +374 -26
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +25 -0
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +10 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/index.ts +2 -0
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +6 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.ts +1 -1
- package/src/server-block-cache.test.ts +27 -0
- package/src/server-block-cache.ts +13 -0
- package/src/server-data-source.infinite.test.ts +62 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +48 -4
- package/src/server.ts +2 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +1 -1
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +1 -1
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.ts +7 -1
- 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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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] =
|
|
742
|
+
keys[i] = keyOf(rows[i]);
|
|
605
743
|
compare = compareNumericKeys;
|
|
606
744
|
}
|
|
607
745
|
else if (comparator === sortFns.date) {
|
|
608
|
-
|
|
609
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 <
|
|
1008
|
-
const original =
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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);
|
package/dist/grid-messages.d.ts
CHANGED
|
@@ -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;
|
package/dist/grid-messages.js
CHANGED
|
@@ -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',
|
package/dist/headless.d.ts
CHANGED
|
@@ -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. */
|