@svgrid/grid 3.0.8 → 3.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GridMenus.svelte +3 -2
- package/dist/SvGrid.controller.svelte.d.ts +60 -8
- package/dist/SvGrid.controller.svelte.js +475 -110
- package/dist/SvGrid.css +186 -3
- package/dist/SvGrid.svelte +281 -163
- package/dist/SvGrid.types.d.ts +24 -7
- package/dist/cdn/{GridMenus-D15bhpX5.js → GridMenus-B1sipj6y.js} +165 -165
- package/dist/cdn/{GridMenus-B8-Gbfq2.js → GridMenus-DlAzfHLk.js} +201 -201
- package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
- package/dist/cdn/{src-Dz625Fhx.js → src-BYsa66I0.js} +9738 -10174
- package/dist/cdn/{src-CleI0g9l.js → src-pA4RroLV.js} +10702 -11138
- package/dist/cdn/svgrid.js +27 -27
- package/dist/cdn/svgrid.svelte-external.js +27 -27
- package/dist/cdn/{validate-GEs4ZPSW.js → validate-XuMe4_KR.js} +2 -2
- package/dist/cell-render.js +25 -3
- package/dist/columns.js +16 -1
- package/dist/conditional-formatting.js +9 -2
- package/dist/core.d.ts +93 -0
- package/dist/core.js +313 -52
- package/dist/filtering/excel-filters.js +41 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/selection.d.ts +2 -2
- package/dist/selection.js +32 -8
- package/dist/server-block-cache.d.ts +7 -32
- package/dist/server-block-cache.js +21 -12
- package/dist/server-data-source.js +3 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/spreadsheet.js +7 -0
- package/dist/validate.js +3 -11
- package/dist/virtualization/column-virtualizer.d.ts +4 -0
- package/dist/virtualization/column-virtualizer.js +2 -0
- package/dist/virtualization/types.d.ts +20 -0
- package/dist/virtualization/virtualizer.js +90 -24
- package/dist/windowed-brand.d.ts +22 -0
- package/dist/windowed-brand.js +60 -0
- package/dist/windowed-data.d.ts +6 -0
- package/dist/windowed-data.js +34 -0
- package/dist/windowed-row-model.d.ts +1 -0
- package/dist/windowed-row-model.js +97 -0
- package/package.json +1 -1
- package/src/GridMenus.svelte +3 -2
- package/src/SvGrid.controller.svelte.ts +469 -114
- package/src/SvGrid.css +186 -3
- package/src/SvGrid.svelte +281 -163
- package/src/SvGrid.types.ts +24 -7
- package/src/cell-render.ts +24 -2
- package/src/columns.ts +16 -1
- package/src/conditional-formatting.ts +6 -4
- package/src/core.sort.test.ts +61 -0
- package/src/core.ts +309 -52
- package/src/counting-cell.test.svelte +11 -0
- package/src/filtering/excel-filters.ts +39 -6
- package/src/headless.reactivity.svelte.test.ts +1 -1
- package/src/index.ts +6 -0
- package/src/new-features.test.ts +24 -0
- package/src/selection.test.ts +14 -0
- package/src/selection.ts +31 -8
- package/src/server-block-cache.ts +27 -10
- package/src/server-data-source.ts +3 -1
- package/src/server.ts +6 -0
- package/src/spreadsheet.ts +6 -0
- package/src/svgrid.behavior.test.ts +29 -0
- package/src/svgrid.column-cell-recycling.test.ts +120 -0
- package/src/svgrid.interaction.test.ts +4 -1
- package/src/svgrid.new-features.wrapper.test.ts +5 -4
- package/src/svgrid.pinned-column-virtualization.test.ts +162 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +15 -3
- package/src/validate.test.ts +4 -4
- package/src/validate.ts +3 -14
- package/src/virtualization/column-virtualizer.ts +6 -0
- package/src/virtualization/types.ts +20 -0
- package/src/virtualization/virtualizer.test.ts +152 -0
- package/src/virtualization/virtualizer.ts +93 -27
- package/src/windowed-brand.ts +67 -0
- package/src/windowed-data.test.ts +105 -0
- package/src/windowed-data.ts +36 -0
- package/src/windowed-row-model.ts +113 -0
- package/dist/cdn/server-block-cache-DIqb3VZn.js +0 -241
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { applyGroupAggregate, applyRowPredicate, compileExcelFilter, getAdvancedFilterEngine, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createTreeRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index.js";
|
|
2
2
|
import { createRowScrollScaling, resolveMaxDomHeight, } from "./virtualization/scroll-scaling.js";
|
|
3
3
|
import { buildPreMeasureItems } from "./virtualization/virtualizer.js";
|
|
4
|
+
// Not on the public barrel: turns a lazy row list into a plain array before a full pass.
|
|
5
|
+
import { denseRows } from "./core.js";
|
|
6
|
+
import { isWindowedData } from "./windowed-brand.js";
|
|
4
7
|
import { DEV } from "esm-env";
|
|
5
8
|
import "./sv-grid-scrollbar.js";
|
|
6
9
|
import { computeColumnStat, formatsNeedingStats, formatNeedsStats, } from "./conditional-formatting.js";
|
|
@@ -38,6 +41,13 @@ import { untrack } from "svelte";
|
|
|
38
41
|
* ~17.9M, Chrome/Safari ~33.5M) so it is always safe, if coarser than needed.
|
|
39
42
|
*/
|
|
40
43
|
const MAX_DOM_SCROLL_HEIGHT_FALLBACK = 8000000;
|
|
44
|
+
/** Columns the column window moves by during a horizontal scroll. */
|
|
45
|
+
const COLUMN_WINDOW_STEP = 3;
|
|
46
|
+
/** The default `columnOverscan`. A margin of 2 measured 6% less scroll work,
|
|
47
|
+
* but the window at rest is `1 + columnOverscan` columns wide wherever the
|
|
48
|
+
* viewport measures 0 px - jsdom, where apps test their grids - and 2 left
|
|
49
|
+
* the fourth column out of tests that read it. */
|
|
50
|
+
const COLUMN_OVERSCAN = 3;
|
|
41
51
|
/**
|
|
42
52
|
* The browser's actual maximum *scrollable* element height in CSS px. Browsers
|
|
43
53
|
* clamp how tall a single element may be, and the cap is lower on mobile /
|
|
@@ -163,6 +173,12 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
163
173
|
* Falls back to the historical literal for direct controller construction.
|
|
164
174
|
*/
|
|
165
175
|
const gridDomId = domIdBase ?? "svgrid";
|
|
176
|
+
// Detect the browser's element-height cap now, while this grid has no DOM.
|
|
177
|
+
// The probe reads offsetHeight, which lays out the whole document; run
|
|
178
|
+
// mid-render it laid out this grid's first frame too, and on a wide grid
|
|
179
|
+
// that one layout was seconds. Cached, so later calls are free.
|
|
180
|
+
if (typeof document !== "undefined")
|
|
181
|
+
getMaxDomScrollHeight();
|
|
166
182
|
// Runtime option overrides set via the imperative api (`api.setOption(key, value)`).
|
|
167
183
|
// Every controller read of a prop goes through the `props` proxy below (and the view
|
|
168
184
|
// reads controller-resolved gates like `_features` / `editingEnabled`, not the raw
|
|
@@ -381,7 +397,11 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
381
397
|
/** When an edit starts: true selects all text, false places the caret at the end. */
|
|
382
398
|
let editorSelectAll = true;
|
|
383
399
|
/** Per-column width overrides set by the resize handles. */
|
|
384
|
-
|
|
400
|
+
// Raw, like collapsedColumns and hiddenColumns below: every writer replaces
|
|
401
|
+
// the object, and a deep proxy allocated a reactive source for each column
|
|
402
|
+
// id anything asked about - every column, on every width rebuild, on a
|
|
403
|
+
// 10,000-column grid.
|
|
404
|
+
let columnWidths = $state.raw({});
|
|
385
405
|
/**
|
|
386
406
|
* Columns folded to nothing, the way a sheet hides a column: the column
|
|
387
407
|
* keeps its index, its cells and its width for later, and takes no room.
|
|
@@ -389,7 +409,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
389
409
|
* and shifts every index after it; a spreadsheet's formulas and formats
|
|
390
410
|
* are keyed by index and could not follow that.
|
|
391
411
|
*/
|
|
392
|
-
let collapsedColumns = $state({});
|
|
412
|
+
let collapsedColumns = $state.raw({});
|
|
393
413
|
function setColumnCollapsed(columnId, collapsed) {
|
|
394
414
|
if (!!collapsedColumns[columnId] === collapsed)
|
|
395
415
|
return;
|
|
@@ -478,7 +498,10 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
478
498
|
let contextMenuPos = $state({ x: 0, y: 0 });
|
|
479
499
|
// Editable comments: internal overlay (rowId -> columnId -> note) merged on
|
|
480
500
|
// top of props.notes for immediate feedback, plus the open-editor state.
|
|
481
|
-
|
|
501
|
+
// Raw: writeNote replaces the object (and the row map) on every edit, and a
|
|
502
|
+
// deep proxy made every cell that asked about a note it does not have
|
|
503
|
+
// allocate a reactive source for that row id (0.3 ms of a far jump).
|
|
504
|
+
let noteOverrides = $state.raw({});
|
|
482
505
|
let commentEditFor = $state(null);
|
|
483
506
|
let commentDraft = $state("");
|
|
484
507
|
let valueFilters = $state({});
|
|
@@ -535,11 +558,13 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
535
558
|
scrollHeight: 0,
|
|
536
559
|
scrollWidth: 0,
|
|
537
560
|
});
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
561
|
+
// The four reads below need a clean layout. This effect runs right after a
|
|
562
|
+
// scroll flush has written the new rows and columns, so reading here forced
|
|
563
|
+
// a synchronous layout on every scroll frame (~8 ms a frame on a 10,000-
|
|
564
|
+
// column grid). Instead the read waits for the start of the next frame,
|
|
565
|
+
// when the browser has already laid out and painted; triggers in between
|
|
566
|
+
// collapse into that one read.
|
|
567
|
+
function readScrollMetrics() {
|
|
543
568
|
const el = scrollContainer;
|
|
544
569
|
if (!el)
|
|
545
570
|
return;
|
|
@@ -547,7 +572,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
547
572
|
const scrollLeft = el.scrollLeft;
|
|
548
573
|
const scrollHeight = el.scrollHeight;
|
|
549
574
|
const scrollWidth = el.scrollWidth;
|
|
550
|
-
const prev =
|
|
575
|
+
const prev = scrollMetrics;
|
|
551
576
|
if (prev.scrollTop === scrollTop &&
|
|
552
577
|
prev.scrollLeft === scrollLeft &&
|
|
553
578
|
prev.scrollHeight === scrollHeight &&
|
|
@@ -555,6 +580,20 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
555
580
|
return;
|
|
556
581
|
}
|
|
557
582
|
scrollMetrics = { scrollTop, scrollLeft, scrollHeight, scrollWidth };
|
|
583
|
+
}
|
|
584
|
+
$effect(function scrollMetrics_e() {
|
|
585
|
+
scrollVersion;
|
|
586
|
+
viewportVersion;
|
|
587
|
+
virtualizer.version;
|
|
588
|
+
columnVirtualizerVersion;
|
|
589
|
+
if (!scrollContainer)
|
|
590
|
+
return;
|
|
591
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
592
|
+
untrack(readScrollMetrics);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const raf = requestAnimationFrame(readScrollMetrics);
|
|
596
|
+
return () => cancelAnimationFrame(raf);
|
|
558
597
|
});
|
|
559
598
|
/** Vertical overflow from the virtualizer's authoritative total size,
|
|
560
599
|
* NOT from `scrollMetrics.scrollHeight` alone. Reading DOM dimensions
|
|
@@ -643,7 +682,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
643
682
|
// svelte-ignore state_referenced_locally
|
|
644
683
|
let internalColumns = $state.raw(resolveCols(props.columns));
|
|
645
684
|
// svelte-ignore state_referenced_locally
|
|
646
|
-
let hiddenColumns = $state(initialHiddenColumns(props.columns));
|
|
685
|
+
let hiddenColumns = $state.raw(initialHiddenColumns(props.columns));
|
|
647
686
|
// The columns declared `visible: false` in the ColumnDefs. Kept separately so
|
|
648
687
|
// applying a saved view (setState) can re-seed from these and never un-hide a
|
|
649
688
|
// column the app declared hidden (#54).
|
|
@@ -690,7 +729,9 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
690
729
|
// A fresh array object, because `getRowModel` memoises on identity. The
|
|
691
730
|
// elements are the same objects, so a row that did not move keeps its row
|
|
692
731
|
// object and a field write still reaches its cell through the proxy.
|
|
693
|
-
|
|
732
|
+
// Windowed data (a server row model's rows) is already a fresh array per
|
|
733
|
+
// change and copying it would read every one of its `length` entries.
|
|
734
|
+
internalData = length === 0 ? EMPTY_DATA : isWindowedData(next) ? next : next.slice();
|
|
694
735
|
// When the consumer replaces `data` (e.g. a "Reset" button), drop any
|
|
695
736
|
// accumulated cell-edit overrides - otherwise `getCellDisplayValue`
|
|
696
737
|
// would keep returning the old edited values from `editedCellValues`
|
|
@@ -975,14 +1016,28 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
975
1016
|
};
|
|
976
1017
|
}
|
|
977
1018
|
const allColumns = $derived.by(function allColumns_d() {
|
|
1019
|
+
// Read the hidden set and the responsive flag once, not once per column.
|
|
1020
|
+
// `hiddenColumns` is a deep proxy: asking it about an id it does not
|
|
1021
|
+
// hold allocated a reactive source for that id, 10,000 of them on a
|
|
1022
|
+
// 10,000-column mount, and `props.responsive` is a reactive read too.
|
|
1023
|
+
// Iterating the proxy's own keys tracks additions with one dependency.
|
|
1024
|
+
const hiddenIds = new Set();
|
|
1025
|
+
for (const id in hiddenColumns)
|
|
1026
|
+
if (hiddenColumns[id])
|
|
1027
|
+
hiddenIds.add(id);
|
|
1028
|
+
const responsiveOn = !!props.responsive;
|
|
1029
|
+
// Derived values read once too: a read inside the filter is a tracked
|
|
1030
|
+
// get per column.
|
|
1031
|
+
const groupHidden = hiddenByGroupCollapse;
|
|
1032
|
+
const foldedSources = autoGroupSpec.hiddenSourceIds;
|
|
978
1033
|
let raw = grid
|
|
979
1034
|
.getAllColumns()
|
|
980
|
-
.filter((column) => !
|
|
981
|
-
!
|
|
982
|
-
!isHiddenByResponsive(column) &&
|
|
1035
|
+
.filter((column) => !hiddenIds.has(column.id) &&
|
|
1036
|
+
!groupHidden[column.id] &&
|
|
1037
|
+
!(responsiveOn && isHiddenByResponsive(column)) &&
|
|
983
1038
|
// In a column display mode the grouped columns are folded into the
|
|
984
1039
|
// auto column(s), so showing them too would just duplicate the value.
|
|
985
|
-
!
|
|
1040
|
+
!foldedSources.has(column.id));
|
|
986
1041
|
// Auto-group columns lead, like a row header.
|
|
987
1042
|
if (autoGroupColumns.length)
|
|
988
1043
|
raw = [...autoGroupColumns, ...raw];
|
|
@@ -1204,6 +1259,11 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1204
1259
|
const formats = props.conditionalFormats;
|
|
1205
1260
|
if (!formats?.length || !formatsNeedingStats(formats))
|
|
1206
1261
|
return map;
|
|
1262
|
+
// Windowed data (a server row model) is mostly rows nobody has loaded;
|
|
1263
|
+
// its range is the server's to know. Scanning it would read every row of
|
|
1264
|
+
// the source. Give such formats `minValue` / `maxValue` instead.
|
|
1265
|
+
if (isWindowedData(internalData))
|
|
1266
|
+
return map;
|
|
1207
1267
|
// `filtered` (default): every row that survives the filters, ignoring the
|
|
1208
1268
|
// page slice - so a value keeps the same colour as you page through (#61).
|
|
1209
1269
|
// `visible`: only the rows on screen, rescaling per page. `all`: the full
|
|
@@ -1326,16 +1386,34 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1326
1386
|
? compileCond(columnId, f.operator2, f.value2 ?? "", f.valueTo2)
|
|
1327
1387
|
: null,
|
|
1328
1388
|
}));
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1389
|
+
const only = compiledMenuFilters.length === 1 ? compiledMenuFilters[0] : null;
|
|
1390
|
+
if (only && !only.b && only.a) {
|
|
1391
|
+
// One column, one condition - the common case. A plain loop over
|
|
1392
|
+
// `test(read(row))`, without the per-row every() closure and the
|
|
1393
|
+
// destructure: 1M rows spent most of their time in that wrapper.
|
|
1394
|
+
const read = only.readValue;
|
|
1395
|
+
const test = only.a;
|
|
1396
|
+
const input = denseRows(rows);
|
|
1397
|
+
const kept = [];
|
|
1398
|
+
for (let i = 0; i < input.length; i++) {
|
|
1399
|
+
const row = input[i];
|
|
1400
|
+
if (test(read(row)))
|
|
1401
|
+
kept.push(row);
|
|
1402
|
+
}
|
|
1403
|
+
rows = kept;
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
rows = denseRows(rows).filter((row) => compiledMenuFilters.every(({ readValue, join, a, b }) => {
|
|
1407
|
+
const cellValue = readValue(row);
|
|
1408
|
+
const ra = a ? a(cellValue) : null;
|
|
1409
|
+
const rb = b ? b(cellValue) : null;
|
|
1410
|
+
if (ra === null)
|
|
1411
|
+
return rb ?? true;
|
|
1412
|
+
if (rb === null)
|
|
1413
|
+
return ra;
|
|
1414
|
+
return join === "OR" ? ra || rb : ra && rb;
|
|
1415
|
+
}));
|
|
1416
|
+
}
|
|
1339
1417
|
}
|
|
1340
1418
|
const valueFilterEntries = Object.entries(valueFilters).filter(([id]) => id !== excludeColumnId);
|
|
1341
1419
|
if (valueFilterEntries.length) {
|
|
@@ -1348,7 +1426,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1348
1426
|
readValue: makeColumnReader(columnId),
|
|
1349
1427
|
buckets: facetBucketsByColumn.get(columnId) ?? null,
|
|
1350
1428
|
}));
|
|
1351
|
-
rows = rows.filter((row) => bucketEntries.every(({ allowed, buckets, readValue }) => {
|
|
1429
|
+
rows = denseRows(rows).filter((row) => bucketEntries.every(({ allowed, buckets, readValue }) => {
|
|
1352
1430
|
const raw = readValue(row);
|
|
1353
1431
|
if (buckets) {
|
|
1354
1432
|
// Range-bucketed filter: find which bucket this row's value
|
|
@@ -1395,7 +1473,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1395
1473
|
// `getCellValueByColumnId` read the same per-row values array, so the
|
|
1396
1474
|
// answer is the same. The columns are the ones the rows were built with.
|
|
1397
1475
|
const columnIds = grid.getAllColumns().map((c) => c.id);
|
|
1398
|
-
rows = rows.filter((row) => {
|
|
1476
|
+
rows = denseRows(rows).filter((row) => {
|
|
1399
1477
|
for (const id of columnIds) {
|
|
1400
1478
|
if (normalizeForFilter(String(row.getCellValueByColumnId(id) ?? ""), locale).includes(needle))
|
|
1401
1479
|
return true;
|
|
@@ -1658,11 +1736,17 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1658
1736
|
});
|
|
1659
1737
|
/** Whether the bar has anything to show: the model's count when it has one, else the loaded ticks. */
|
|
1660
1738
|
const selectionBarVisible = $derived(selectionBarOn && selectionBarCount > 0);
|
|
1661
|
-
// Forward selection changes to the consumer.
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1739
|
+
// Forward selection changes to the consumer. The first run records the
|
|
1740
|
+
// starting selection instead of reporting it, so consumers don't get a
|
|
1741
|
+
// spurious callback on mount. (This was seeded with "", which never matched
|
|
1742
|
+
// "{}", so the mount call this comment always promised to skip was made.)
|
|
1743
|
+
let lastSelectionSerialized = null;
|
|
1664
1744
|
$effect(() => {
|
|
1665
1745
|
const serialized = JSON.stringify(rowSelectionState);
|
|
1746
|
+
if (lastSelectionSerialized === null) {
|
|
1747
|
+
lastSelectionSerialized = serialized;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1666
1750
|
if (serialized === lastSelectionSerialized)
|
|
1667
1751
|
return;
|
|
1668
1752
|
lastSelectionSerialized = serialized;
|
|
@@ -1684,16 +1768,23 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1684
1768
|
// Forward cell-selection rectangle changes to the consumer. Same
|
|
1685
1769
|
// dedupe pattern - fires only when the serialized rectangle changes
|
|
1686
1770
|
// so consumers don't see spurious callbacks during re-renders.
|
|
1687
|
-
|
|
1771
|
+
// Every rectangle, the committed ones a Ctrl+click added and the active one
|
|
1772
|
+
// last, the order getSelectionRects gives: a consumer's status bar and
|
|
1773
|
+
// shading cover the whole selection, not only its last range.
|
|
1774
|
+
const currentCellRanges = () => getSelectionRects().map((r) => [r.minRow, r.minCol, r.maxRow, r.maxCol]);
|
|
1775
|
+
// `null` until the first run, which records the starting selection rather
|
|
1776
|
+
// than reporting it: mounting is not a change. (Not seeded eagerly -
|
|
1777
|
+
// getSelectionRects is bound further down this file.)
|
|
1778
|
+
let lastCellRangeSerialized = null;
|
|
1688
1779
|
$effect(() => {
|
|
1689
|
-
//
|
|
1690
|
-
// active one last, the order getSelectionRects gives: a consumer's
|
|
1691
|
-
// status bar and shading cover the whole selection, not only its last
|
|
1692
|
-
// range. Depend on selectionRanges so a change to the committed set
|
|
1693
|
-
// re-runs this too.
|
|
1780
|
+
// Depend on selectionRanges so a change to the committed set re-runs this too.
|
|
1694
1781
|
void selectionRanges;
|
|
1695
|
-
const ranges =
|
|
1782
|
+
const ranges = currentCellRanges();
|
|
1696
1783
|
const serialized = JSON.stringify(ranges);
|
|
1784
|
+
if (lastCellRangeSerialized === null) {
|
|
1785
|
+
lastCellRangeSerialized = serialized;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1697
1788
|
if (serialized === lastCellRangeSerialized)
|
|
1698
1789
|
return;
|
|
1699
1790
|
lastCellRangeSerialized = serialized;
|
|
@@ -2498,25 +2589,39 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2498
2589
|
}
|
|
2499
2590
|
// Forward sort-clause changes to the consumer. Same dedupe pattern as the
|
|
2500
2591
|
// selection callback above - fires only when the serialized clauses change.
|
|
2501
|
-
|
|
2592
|
+
// Seeded from the starting sort, so mounting is not reported as a change:
|
|
2593
|
+
// it used to be, and a consumer that keeps the sort in the URL (reset the
|
|
2594
|
+
// page on a new sort) navigated away from ?page=2 on every load, while a
|
|
2595
|
+
// server data source refetched page 0 for nothing.
|
|
2596
|
+
const currentSorting = () => (grid.getState().sorting ?? []);
|
|
2597
|
+
let lastSortingSerialized = JSON.stringify(untrack(currentSorting));
|
|
2502
2598
|
$effect(() => {
|
|
2503
2599
|
gridStateVersion;
|
|
2504
|
-
const sorting = (
|
|
2600
|
+
const sorting = currentSorting();
|
|
2505
2601
|
const serialized = JSON.stringify(sorting);
|
|
2506
2602
|
if (serialized === lastSortingSerialized)
|
|
2507
2603
|
return;
|
|
2508
2604
|
lastSortingSerialized = serialized;
|
|
2509
2605
|
props.onSortingChange?.(sorting);
|
|
2510
2606
|
});
|
|
2607
|
+
// A row model starts unsorted and takes no initial sort of its own. It
|
|
2608
|
+
// learned `initialSorting` from the mount report above; now that there is
|
|
2609
|
+
// none, hand the starting sort over directly - to each model the grid is
|
|
2610
|
+
// given, and only when there is a sort to hand over.
|
|
2611
|
+
$effect(() => {
|
|
2612
|
+
const model = rawProps.rowModel;
|
|
2613
|
+
untrack(() => {
|
|
2614
|
+
const sorting = currentSorting();
|
|
2615
|
+
if (model?.setSort && sorting.length)
|
|
2616
|
+
model.setSort(sorting);
|
|
2617
|
+
});
|
|
2618
|
+
});
|
|
2511
2619
|
// Forward filter-state changes to the consumer. Consolidates the three
|
|
2512
2620
|
// wrapper-managed filter stores (global text, per-column operator filters,
|
|
2513
2621
|
// facet checklists) into one shape so server-side consumers can build a
|
|
2514
2622
|
// single query. Skipped entirely when no callback is registered to avoid
|
|
2515
2623
|
// serializing on every keystroke.
|
|
2516
|
-
|
|
2517
|
-
$effect(() => {
|
|
2518
|
-
if (!props.onFiltersChange)
|
|
2519
|
-
return;
|
|
2624
|
+
function currentFilters() {
|
|
2520
2625
|
const menuEntries = Object.entries(filterMenuValues)
|
|
2521
2626
|
.filter(([, f]) => {
|
|
2522
2627
|
if (f.operator === "isBlank" || f.operator === "isNotBlank")
|
|
@@ -2549,10 +2654,17 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2549
2654
|
? { ...existing, selectedValues: entry.selectedValues }
|
|
2550
2655
|
: entry);
|
|
2551
2656
|
}
|
|
2552
|
-
|
|
2657
|
+
return {
|
|
2553
2658
|
global: globalFilter,
|
|
2554
2659
|
columns: Array.from(merged.values()),
|
|
2555
2660
|
};
|
|
2661
|
+
}
|
|
2662
|
+
// Seeded from the starting filters (empty), so mounting is not a change.
|
|
2663
|
+
let lastFiltersSerialized = JSON.stringify(untrack(currentFilters));
|
|
2664
|
+
$effect(() => {
|
|
2665
|
+
if (!props.onFiltersChange)
|
|
2666
|
+
return;
|
|
2667
|
+
const payload = currentFilters();
|
|
2556
2668
|
const serialized = JSON.stringify(payload);
|
|
2557
2669
|
if (serialized === lastFiltersSerialized)
|
|
2558
2670
|
return;
|
|
@@ -2712,6 +2824,33 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2712
2824
|
const frozenRowList = $derived(frozenRowCount ? allRows.slice(0, frozenRowCount) : []);
|
|
2713
2825
|
/** The merged cells, indexed; null (the usual case) costs nothing. */
|
|
2714
2826
|
const mergeIndex = $derived(buildMergeIndex(props.mergedCells));
|
|
2827
|
+
/**
|
|
2828
|
+
* Rows laid out as flex lines rather than table rows (SvGrid.css,
|
|
2829
|
+
* `.sv-grid-table-flex`): a change to one row's cells no longer lays out
|
|
2830
|
+
* every other row. A merged cell spanning rows needs the table algorithm,
|
|
2831
|
+
* so any such merge, or `rowLayout: 'table'`, keeps table layout.
|
|
2832
|
+
*/
|
|
2833
|
+
const flexRows = $derived(props.rowLayout !== "table" &&
|
|
2834
|
+
!(props.mergedCells ?? []).some((m) => (m.rowSpan ?? 1) > 1));
|
|
2835
|
+
// The spreadsheetLayout action (a use: directive on a wrapper, outside this
|
|
2836
|
+
// component) switches the table back to table layout on the DOM, with
|
|
2837
|
+
// data-row-layout="table". Watch for it, so the shortcuts that only hold
|
|
2838
|
+
// under flex rows (no right spacer) switch off with it.
|
|
2839
|
+
let tableLayoutForced = $state(false);
|
|
2840
|
+
$effect(() => {
|
|
2841
|
+
const el = gridRootEl;
|
|
2842
|
+
if (!el || typeof MutationObserver === "undefined")
|
|
2843
|
+
return;
|
|
2844
|
+
const read = () => {
|
|
2845
|
+
tableLayoutForced = el.getAttribute("data-row-layout") === "table";
|
|
2846
|
+
};
|
|
2847
|
+
read();
|
|
2848
|
+
const observer = new MutationObserver(read);
|
|
2849
|
+
observer.observe(el, { attributes: true, attributeFilter: ["data-row-layout"] });
|
|
2850
|
+
return () => observer.disconnect();
|
|
2851
|
+
});
|
|
2852
|
+
/** Rows really are flex lines: flexRows, and no spreadsheetLayout forcing table layout. */
|
|
2853
|
+
const flexLayoutActive = $derived(flexRows && !tableLayoutForced);
|
|
2715
2854
|
const frozenBandHeight = $derived.by(() => {
|
|
2716
2855
|
if (!frozenRowCount)
|
|
2717
2856
|
return 0;
|
|
@@ -2859,52 +2998,210 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2859
2998
|
return item;
|
|
2860
2999
|
});
|
|
2861
3000
|
};
|
|
3001
|
+
/**
|
|
3002
|
+
* The column window before any measurement: the columns that fit an
|
|
3003
|
+
* estimated viewport (the measured width once known, else 1,920 px) plus the
|
|
3004
|
+
* overscan, anchored at column 0. Deterministic, so a server render and the
|
|
3005
|
+
* first client render agree. It used to be every column, which put 10,000
|
|
3006
|
+
* columns' cells in the DOM for one frame and cost most of a wide mount.
|
|
3007
|
+
*/
|
|
3008
|
+
const preMeasureColumnItems = () => {
|
|
3009
|
+
const viewport = viewportWidth > 0 ? viewportWidth : 1920;
|
|
3010
|
+
let extra = Math.max(props.columnOverscan ?? COLUMN_OVERSCAN, 0);
|
|
3011
|
+
const items = [];
|
|
3012
|
+
let start = 0;
|
|
3013
|
+
for (let i = 0; i < allColumns.length; i += 1) {
|
|
3014
|
+
if (start >= viewport && extra-- <= 0)
|
|
3015
|
+
break;
|
|
3016
|
+
const size = getColumnWidth(allColumns[i].id);
|
|
3017
|
+
items.push({ index: i, key: i, size, start, end: start + size });
|
|
3018
|
+
start += size;
|
|
3019
|
+
}
|
|
3020
|
+
return items;
|
|
3021
|
+
};
|
|
2862
3022
|
const renderedColumnItems = $derived.by(function renderedColumnItems_d() {
|
|
2863
3023
|
if (!columnVirtualizationEnabled)
|
|
2864
3024
|
return allColumnItems();
|
|
2865
3025
|
// Column virtualization is ON by default and its virtualizer, like the row
|
|
2866
3026
|
// one, only learns `count` from an effect - which never runs on a server.
|
|
2867
|
-
// Without this the server emitted rows containing no data cells.
|
|
2868
|
-
//
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
return allColumnItems();
|
|
3027
|
+
// Without this the server emitted rows containing no data cells. Before a
|
|
3028
|
+
// measurement the window is the columns that fit an estimated viewport.
|
|
3029
|
+
if (virtualColumns.length === 0 && allColumns.length > 0 && effectivePinning.left.length === 0 && effectivePinning.right.length === 0) {
|
|
3030
|
+
return preMeasureColumnItems();
|
|
3031
|
+
}
|
|
2873
3032
|
// Pinned columns are position:sticky, so they only stay pinned while their
|
|
2874
|
-
// cell is in the DOM.
|
|
2875
|
-
//
|
|
2876
|
-
//
|
|
2877
|
-
//
|
|
2878
|
-
//
|
|
2879
|
-
//
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
const hasLeft = effectivePinning.left.length > 0;
|
|
2885
|
-
const hasRight = effectivePinning.right.length > 0;
|
|
2886
|
-
if ((!hasLeft && !hasRight) || window.length === 0)
|
|
2887
|
-
return window;
|
|
2888
|
-
const firstIdx = window[0].index;
|
|
2889
|
-
const lastIdx = window[window.length - 1].index;
|
|
2890
|
-
const startIndex = hasLeft ? 0 : firstIdx;
|
|
2891
|
-
const endIndex = hasRight ? allColumns.length - 1 : lastIdx;
|
|
2892
|
-
if (startIndex === firstIdx && endIndex === lastIdx)
|
|
3033
|
+
// cell is in the DOM. allColumns is ordered [pinnedLeft, unpinned,
|
|
3034
|
+
// pinnedRight], so the rendered set is three runs: the pinned-left prefix,
|
|
3035
|
+
// the virtualizer's window, and the pinned-right suffix. Each run keeps its
|
|
3036
|
+
// real offsets, and the templates draw a spacer cell for the columns
|
|
3037
|
+
// between two runs (`gap` on `renderedColumns`), so the centre stays
|
|
3038
|
+
// virtualized however many columns sit between the pinned edges.
|
|
3039
|
+
const window = virtualColumns.length > 0 ? virtualColumns : preMeasureColumnItems();
|
|
3040
|
+
const leftCount = effectivePinning.left.length;
|
|
3041
|
+
const rightCount = effectivePinning.right.length;
|
|
3042
|
+
if ((leftCount === 0 && rightCount === 0) || window.length === 0)
|
|
2893
3043
|
return window;
|
|
3044
|
+
const total = allColumns.length;
|
|
2894
3045
|
const items = [];
|
|
3046
|
+
// Pinned-left prefix: offsets from 0.
|
|
2895
3047
|
let offset = 0;
|
|
2896
|
-
for (let i = 0; i <
|
|
2897
|
-
offset += getColumnWidth(allColumns[i].id);
|
|
2898
|
-
for (let i = startIndex; i <= endIndex; i += 1) {
|
|
3048
|
+
for (let i = 0; i < Math.min(leftCount, total); i += 1) {
|
|
2899
3049
|
const size = getColumnWidth(allColumns[i].id);
|
|
2900
3050
|
items.push({ index: i, key: i, size, start: offset, end: offset + size });
|
|
2901
3051
|
offset += size;
|
|
2902
3052
|
}
|
|
3053
|
+
// The window, minus any pinned column it already covers.
|
|
3054
|
+
const firstRight = total - rightCount;
|
|
3055
|
+
for (const item of window) {
|
|
3056
|
+
if (item.index < leftCount || item.index >= firstRight)
|
|
3057
|
+
continue;
|
|
3058
|
+
items.push(item);
|
|
3059
|
+
}
|
|
3060
|
+
// Pinned-right suffix: offsets measured back from the virtualizer's total,
|
|
3061
|
+
// the same total the right spacer is computed against.
|
|
3062
|
+
const right = [];
|
|
3063
|
+
let end = virtualColumnTotalSize;
|
|
3064
|
+
if (end <= 0) {
|
|
3065
|
+
// Not measured yet: the virtualizer has no total until its effect runs.
|
|
3066
|
+
for (const column of allColumns)
|
|
3067
|
+
end += getColumnWidth(column.id);
|
|
3068
|
+
}
|
|
3069
|
+
for (let i = total - 1; i >= Math.max(firstRight, leftCount); i -= 1) {
|
|
3070
|
+
const size = getColumnWidth(allColumns[i].id);
|
|
3071
|
+
right.push({ index: i, key: i, size, start: end - size, end });
|
|
3072
|
+
end -= size;
|
|
3073
|
+
}
|
|
3074
|
+
for (let i = right.length - 1; i >= 0; i -= 1)
|
|
3075
|
+
items.push(right[i]);
|
|
2903
3076
|
return items;
|
|
2904
3077
|
});
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3078
|
+
/**
|
|
3079
|
+
* The rendered columns, each with `gap`: the width of the columns NOT
|
|
3080
|
+
* rendered between it and the previous rendered column. Non-zero only at the
|
|
3081
|
+
* seams of the pinned/centre runs above; every row template draws a spacer
|
|
3082
|
+
* cell of that width before the column, so all rows keep the same slots.
|
|
3083
|
+
*/
|
|
3084
|
+
// The entries handed out last time, by column id. A horizontal scroll moves
|
|
3085
|
+
// the window by a column or two per frame; an entry whose column, offset,
|
|
3086
|
+
// width and gap are unchanged is handed out again as the SAME object, so
|
|
3087
|
+
// the keyed `{#each}` in every row leaves those cells alone and only the
|
|
3088
|
+
// columns that entered the window are rendered. A fresh object per column
|
|
3089
|
+
// per frame re-ran every visible cell's effects on every scroll frame.
|
|
3090
|
+
let renderedColumnsPrev = new Map();
|
|
3091
|
+
// The body cells' {#each} key. A virtualized window column is keyed by a
|
|
3092
|
+
// slot, not by its id: a column keeps its slot (and so its <td>) while it
|
|
3093
|
+
// stays in the window, and a column entering the window takes a slot a
|
|
3094
|
+
// leaving column freed. A one-column scroll then hands one cell per row to
|
|
3095
|
+
// the new column, and a thumb drag that jumps to columns far away updates
|
|
3096
|
+
// every cell in place instead of destroying the window's cells and creating
|
|
3097
|
+
// new ones (~800 at a time on a wide grid). There are as many slots as the
|
|
3098
|
+
// widest window so far. Pinned columns, and every column without column
|
|
3099
|
+
// virtualization, keep their id.
|
|
3100
|
+
let columnSlots = new Map();
|
|
3101
|
+
let columnSlotOrder = [];
|
|
3102
|
+
let columnSlotCount = 0;
|
|
3103
|
+
const renderedColumns = $derived.by(() => {
|
|
3104
|
+
const out = [];
|
|
3105
|
+
const next = new Map();
|
|
3106
|
+
const total = allColumns.length;
|
|
3107
|
+
const leftCount = effectivePinning.left.length;
|
|
3108
|
+
const firstRight = total - effectivePinning.right.length;
|
|
3109
|
+
const inWindow = (index) => columnVirtualizationEnabled && index >= leftCount && index < firstRight;
|
|
3110
|
+
// Window columns already holding a slot keep it; the others take the
|
|
3111
|
+
// slots nobody kept, in the order those cells sat in the row, then new
|
|
3112
|
+
// ones. In that order a jump to an unrelated window keeps the cells where
|
|
3113
|
+
// they are, and a step moves only the cells that changed column: Svelte
|
|
3114
|
+
// reorders keyed cells by key order.
|
|
3115
|
+
const slots = new Map();
|
|
3116
|
+
const taken = new Set();
|
|
3117
|
+
for (const item of renderedColumnItems) {
|
|
3118
|
+
const id = allColumns[item.index]?.id;
|
|
3119
|
+
if (id === undefined || !inWindow(item.index))
|
|
3120
|
+
continue;
|
|
3121
|
+
const slot = columnSlots.get(id);
|
|
3122
|
+
if (slot !== undefined) {
|
|
3123
|
+
slots.set(id, slot);
|
|
3124
|
+
taken.add(slot);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
const free = columnSlotOrder.filter((slot) => !taken.has(slot));
|
|
3128
|
+
let nextFree = 0;
|
|
3129
|
+
const order = [];
|
|
3130
|
+
for (const item of renderedColumnItems) {
|
|
3131
|
+
const id = allColumns[item.index]?.id;
|
|
3132
|
+
if (id === undefined || !inWindow(item.index))
|
|
3133
|
+
continue;
|
|
3134
|
+
let slot = slots.get(id);
|
|
3135
|
+
if (slot === undefined) {
|
|
3136
|
+
slot = nextFree < free.length ? free[nextFree++] : columnSlotCount++;
|
|
3137
|
+
slots.set(id, slot);
|
|
3138
|
+
}
|
|
3139
|
+
order.push(slot);
|
|
3140
|
+
}
|
|
3141
|
+
// Slots nobody took this time stay at the end, for the next window.
|
|
3142
|
+
for (let i = nextFree; i < free.length; i += 1)
|
|
3143
|
+
order.push(free[i]);
|
|
3144
|
+
columnSlots = slots;
|
|
3145
|
+
columnSlotOrder = order;
|
|
3146
|
+
// What isColumnPinned answers, once per column here instead of once per
|
|
3147
|
+
// cell in the template.
|
|
3148
|
+
const pinnedLeft = new Set(effectivePinning.left);
|
|
3149
|
+
const pinnedRight = new Set(effectivePinning.right);
|
|
3150
|
+
let prevEnd = null;
|
|
3151
|
+
for (const item of renderedColumnItems) {
|
|
3152
|
+
const column = allColumns[item.index];
|
|
3153
|
+
if (column === undefined)
|
|
3154
|
+
continue;
|
|
3155
|
+
let gap = 0;
|
|
3156
|
+
if (prevEnd !== null && item.start - prevEnd >= 0.5)
|
|
3157
|
+
gap = item.start - prevEnd;
|
|
3158
|
+
prevEnd = item.end;
|
|
3159
|
+
const slot = slots.get(column.id);
|
|
3160
|
+
const key = slot !== undefined ? `s:${slot}` : `c:${column.id}`;
|
|
3161
|
+
const pinned = pinnedLeft.has(column.id) ? "left" : pinnedRight.has(column.id) ? "right" : null;
|
|
3162
|
+
const prev = renderedColumnsPrev.get(column.id);
|
|
3163
|
+
const entry = prev &&
|
|
3164
|
+
prev.column === column &&
|
|
3165
|
+
prev.gap === gap &&
|
|
3166
|
+
prev.key === key &&
|
|
3167
|
+
prev.pinned === pinned &&
|
|
3168
|
+
prev.item.index === item.index &&
|
|
3169
|
+
prev.item.start === item.start &&
|
|
3170
|
+
prev.item.size === item.size
|
|
3171
|
+
? prev
|
|
3172
|
+
: { item, column, gap, key, pinned };
|
|
3173
|
+
next.set(column.id, entry);
|
|
3174
|
+
out.push(entry);
|
|
3175
|
+
}
|
|
3176
|
+
renderedColumnsPrev = next;
|
|
3177
|
+
return out;
|
|
3178
|
+
});
|
|
3179
|
+
/**
|
|
3180
|
+
* The contiguous runs of rendered column indices, for whoever must not span
|
|
3181
|
+
* across a seam: a merged cell or a column-group header is clipped to the
|
|
3182
|
+
* run it starts in. One run unless pinned columns split the window.
|
|
3183
|
+
*/
|
|
3184
|
+
const renderedColumnRuns = $derived.by(function renderedColumnRuns_d() {
|
|
3185
|
+
const runs = [];
|
|
3186
|
+
for (const rendered of renderedColumns) {
|
|
3187
|
+
const i = rendered.item.index;
|
|
3188
|
+
const run = runs[runs.length - 1];
|
|
3189
|
+
if (run && i === run.last + 1 && rendered.gap === 0)
|
|
3190
|
+
run.last = i;
|
|
3191
|
+
else
|
|
3192
|
+
runs.push({ first: i, last: i, gap: runs.length === 0 ? 0 : rendered.gap });
|
|
3193
|
+
}
|
|
3194
|
+
return runs;
|
|
3195
|
+
});
|
|
3196
|
+
/** The run holding column `colIndex`, or the whole window if none does. */
|
|
3197
|
+
function columnRunOf(colIndex) {
|
|
3198
|
+
const runs = renderedColumnRuns;
|
|
3199
|
+
for (const run of runs) {
|
|
3200
|
+
if (colIndex >= run.first && colIndex <= run.last)
|
|
3201
|
+
return { firstCol: run.first, lastCol: run.last };
|
|
3202
|
+
}
|
|
3203
|
+
return { firstCol: runs[0]?.first ?? 0, lastCol: runs[runs.length - 1]?.last ?? -1 };
|
|
3204
|
+
}
|
|
2908
3205
|
const totalColumnWidth = $derived.by(function totalColumnWidth_d() {
|
|
2909
3206
|
if (columnVirtualizationEnabled)
|
|
2910
3207
|
return virtualColumnTotalSize;
|
|
@@ -2950,25 +3247,37 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2950
3247
|
const base = groupHeaderRows;
|
|
2951
3248
|
if (!columnVirtualizationEnabled || base.length === 0)
|
|
2952
3249
|
return base;
|
|
2953
|
-
const
|
|
2954
|
-
if (
|
|
3250
|
+
const runs = renderedColumnRuns;
|
|
3251
|
+
if (runs.length === 0)
|
|
2955
3252
|
return base;
|
|
2956
|
-
|
|
2957
|
-
|
|
3253
|
+
// Clip each cell to every run it overlaps. With pinned columns the window
|
|
3254
|
+
// is several runs, so a group spanning a seam becomes one cell per run,
|
|
3255
|
+
// and the first cell of each later run carries the seam's gap.
|
|
2958
3256
|
return base.map((row) => {
|
|
2959
3257
|
const cells = [];
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
widthPx
|
|
2970
|
-
|
|
2971
|
-
|
|
3258
|
+
runs.forEach((run, runIndex) => {
|
|
3259
|
+
let first = true;
|
|
3260
|
+
for (const cell of row.cells) {
|
|
3261
|
+
const start = cell.firstLeafIndex;
|
|
3262
|
+
const end = start + cell.colSpan; // exclusive
|
|
3263
|
+
const from = Math.max(start, run.first);
|
|
3264
|
+
const to = Math.min(end, run.last + 1);
|
|
3265
|
+
if (to <= from)
|
|
3266
|
+
continue; // cell is entirely outside this run
|
|
3267
|
+
let widthPx = 0;
|
|
3268
|
+
for (let i = from; i < to; i += 1)
|
|
3269
|
+
widthPx += getColumnWidth(allColumns[i].id);
|
|
3270
|
+
cells.push({
|
|
3271
|
+
...cell,
|
|
3272
|
+
key: runIndex === 0 ? cell.key : `${cell.key}@${runIndex}`,
|
|
3273
|
+
colSpan: to - from,
|
|
3274
|
+
widthPx,
|
|
3275
|
+
firstLeafIndex: from,
|
|
3276
|
+
gapBefore: first && runIndex > 0 ? run.gap : 0,
|
|
3277
|
+
});
|
|
3278
|
+
first = false;
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
2972
3281
|
return { ...row, cells };
|
|
2973
3282
|
});
|
|
2974
3283
|
});
|
|
@@ -3354,9 +3663,13 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3354
3663
|
// instead of `scrollVersion` so this effect does NOT re-run on every
|
|
3355
3664
|
// scroll event - which would otherwise re-call setOptions hundreds of
|
|
3356
3665
|
// times during a drag.
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3666
|
+
// The measured height comes from the `viewportHeight` derived, which
|
|
3667
|
+
// reads clientHeight only when viewportVersion moves. Reading it here
|
|
3668
|
+
// directly did so on every run of this effect - each filter, sort or
|
|
3669
|
+
// data change - in the middle of the DOM update, forcing a layout the
|
|
3670
|
+
// frame then repeated (6 ms of a 100k-row filter).
|
|
3671
|
+
const measuredHeight = typeof props.containerHeight === "string"
|
|
3672
|
+
? (scrollContainer ? viewportHeight : 520)
|
|
3360
3673
|
: (props.containerHeight ?? 520);
|
|
3361
3674
|
const rh = props.rowHeight;
|
|
3362
3675
|
// Auto height: read measured sizes, falling back to the fixed rowHeight as
|
|
@@ -3384,7 +3697,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3384
3697
|
count: allRows.length,
|
|
3385
3698
|
estimateSize,
|
|
3386
3699
|
overscan: props.overscan ?? 8,
|
|
3387
|
-
viewportHeight,
|
|
3700
|
+
viewportHeight: measuredHeight,
|
|
3388
3701
|
});
|
|
3389
3702
|
});
|
|
3390
3703
|
// Track size changes of the shell so the virtualizer's viewport, the
|
|
@@ -3433,28 +3746,76 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3433
3746
|
});
|
|
3434
3747
|
$effect(() => {
|
|
3435
3748
|
// Reading columnWidths here registers it as a reactive dependency so
|
|
3436
|
-
// the effect re-runs when the user resizes a column.
|
|
3437
|
-
//
|
|
3438
|
-
//
|
|
3439
|
-
|
|
3440
|
-
|
|
3749
|
+
// the effect re-runs when the user resizes a column. The widths are read
|
|
3750
|
+
// once into an array and the size function reads that: passing a size
|
|
3751
|
+
// function makes the virtualizer rebuild every column's offset, and
|
|
3752
|
+
// resolving each width through getColumnWidth inside that rebuild cost
|
|
3753
|
+
// several reactive reads per column - 10,000 times on a wide grid.
|
|
3754
|
+
// getColumnWidth's rules, with the maps read once: collapsed is 0, a
|
|
3755
|
+
// fitted width wins unless the user resized the column, then the user's
|
|
3756
|
+
// width, and otherwise getColumnBaseWidth (the definition or the default,
|
|
3757
|
+
// exactly as the cells are sized).
|
|
3758
|
+
const resized = columnWidths;
|
|
3759
|
+
const collapsed = collapsedColumns;
|
|
3760
|
+
const fitted = fittedColumnWidths;
|
|
3761
|
+
const widths = new Float64Array(allColumns.length);
|
|
3762
|
+
for (let i = 0; i < widths.length; i++) {
|
|
3763
|
+
const id = allColumns[i].id;
|
|
3764
|
+
const own = resized[id];
|
|
3765
|
+
widths[i] = collapsed[id]
|
|
3766
|
+
? 0
|
|
3767
|
+
: own !== undefined
|
|
3768
|
+
? own
|
|
3769
|
+
: (fitted?.[id] ?? getColumnBaseWidth(id));
|
|
3770
|
+
}
|
|
3771
|
+
// `columnOverscan` columns stay rendered ahead of a scroll at all times.
|
|
3772
|
+
// The window does not move on every column boundary: it stays put while
|
|
3773
|
+
// that many columns are left ahead, then moves COLUMN_WINDOW_STEP
|
|
3774
|
+
// columns at once (overscanMin). A move costs about the same whatever
|
|
3775
|
+
// the number of columns entering, in every rendered row, so moving four
|
|
3776
|
+
// at a time did the same scroll for about half the work (1,000 columns,
|
|
3777
|
+
// 120 px per frame, interleaved A/B), and the frames in between change
|
|
3778
|
+
// nothing. Three, not four: a move is one longer frame, and three gave
|
|
3779
|
+
// the shortest longest frame of a wheel scroll (production build: 8.0 ms
|
|
3780
|
+
// against 10.2 at four and 9.4 at two) for about the same total work.
|
|
3781
|
+
const minAhead = Math.max(props.columnOverscan ?? COLUMN_OVERSCAN, 0);
|
|
3441
3782
|
columnVirtualizer.setOptions({
|
|
3442
|
-
count:
|
|
3443
|
-
estimateSize: (index) =>
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3783
|
+
count: widths.length,
|
|
3784
|
+
estimateSize: (index) => widths[index] ?? (props.columnWidth ?? 140),
|
|
3785
|
+
overscan: minAhead + COLUMN_WINDOW_STEP - 1,
|
|
3786
|
+
// One column behind the scroll: the columns the scroll is leaving were
|
|
3787
|
+
// just passed, and a reversal needs one at most for the frame before
|
|
3788
|
+
// the window turns around.
|
|
3789
|
+
overscanBehind: Math.min(1, minAhead),
|
|
3790
|
+
overscanMin: minAhead,
|
|
3449
3791
|
});
|
|
3450
3792
|
});
|
|
3793
|
+
// The viewport on its own: a width change re-windows the columns without
|
|
3794
|
+
// rebuilding their offsets.
|
|
3451
3795
|
$effect(() => {
|
|
3452
|
-
|
|
3796
|
+
columnVirtualizer.setViewportWidth(viewportWidth);
|
|
3797
|
+
});
|
|
3798
|
+
// Re-sync the virtualizers to the scroll position. This effect re-runs on
|
|
3799
|
+
// anything domToLogicalRowOffset reads (the row count among them), and
|
|
3800
|
+
// reading scrollTop / scrollLeft in the middle of that update forced a
|
|
3801
|
+
// layout the frame then did again (5 ms of a 100k-row filter). The live
|
|
3802
|
+
// position is read once per container; after that the last position the
|
|
3803
|
+
// scroll handler or a programmatic scroll recorded (pendingScrollTop /
|
|
3804
|
+
// pendingScrollLeft) is the same number without the layout.
|
|
3805
|
+
let scrollSyncedContainer = null;
|
|
3806
|
+
$effect(() => {
|
|
3807
|
+
const el = scrollContainer;
|
|
3808
|
+
if (!el)
|
|
3453
3809
|
return;
|
|
3810
|
+
if (el !== scrollSyncedContainer) {
|
|
3811
|
+
scrollSyncedContainer = el;
|
|
3812
|
+
pendingScrollTop = el.scrollTop;
|
|
3813
|
+
pendingScrollLeft = el.scrollLeft;
|
|
3814
|
+
}
|
|
3454
3815
|
if (rowVirtualizationEnabled)
|
|
3455
|
-
virtualizer.setScrollOffset(domToLogicalRowOffset(
|
|
3816
|
+
virtualizer.setScrollOffset(domToLogicalRowOffset(pendingScrollTop));
|
|
3456
3817
|
if (columnVirtualizationEnabled)
|
|
3457
|
-
columnVirtualizer.setHorizontalOffset(
|
|
3818
|
+
columnVirtualizer.setHorizontalOffset(pendingScrollLeft);
|
|
3458
3819
|
});
|
|
3459
3820
|
// Re-arms once the user scrolls away from the bottom, so a long lazy-load
|
|
3460
3821
|
// run only fires `onScrollBottomReached` once per arrival at the end.
|
|
@@ -3982,6 +4343,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3982
4343
|
get columnWidths() { return columnWidths; },
|
|
3983
4344
|
get collapsedColumns() { return collapsedColumns; },
|
|
3984
4345
|
get mergeIndex() { return mergeIndex; },
|
|
4346
|
+
get flexRows() { return flexRows; },
|
|
4347
|
+
get flexLayoutActive() { return flexLayoutActive; },
|
|
3985
4348
|
get setColumnCollapsed() { return setColumnCollapsed; },
|
|
3986
4349
|
set columnWidths(v) { columnWidths = v; },
|
|
3987
4350
|
get MIN_COLUMN_WIDTH() { return MIN_COLUMN_WIDTH; },
|
|
@@ -4453,6 +4816,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
4453
4816
|
get renderedColumnItems() { return renderedColumnItems; },
|
|
4454
4817
|
get hasRenderedColumn() { return hasRenderedColumn; },
|
|
4455
4818
|
get renderedColumns() { return renderedColumns; },
|
|
4819
|
+
get renderedColumnRuns() { return renderedColumnRuns; },
|
|
4820
|
+
get columnRunOf() { return columnRunOf; },
|
|
4456
4821
|
get totalColumnWidth() { return totalColumnWidth; },
|
|
4457
4822
|
get hasHorizontalOverflow() { return hasHorizontalOverflow; },
|
|
4458
4823
|
get columnWindowStart() { return columnWindowStart; },
|