@svgrid/grid 3.0.7 → 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/README.md +1 -1
- package/dist/GridMenus.svelte +3 -2
- package/dist/SvGrid.controller.svelte.d.ts +60 -8
- package/dist/SvGrid.controller.svelte.js +490 -114
- package/dist/SvGrid.css +186 -3
- package/dist/SvGrid.svelte +285 -164
- package/dist/SvGrid.types.d.ts +24 -7
- package/dist/cdn/{GridMenus-CcwU8I4t.js → GridMenus-B1sipj6y.js} +165 -165
- package/dist/cdn/{GridMenus-BgUS-W3o.js → GridMenus-DlAzfHLk.js} +201 -201
- package/dist/cdn/server-block-cache-Dj-KPqEQ.js +1289 -0
- package/dist/cdn/{src-kE9B98r6.js → src-BYsa66I0.js} +9747 -10178
- package/dist/cdn/{src-25JmJ_7g.js → src-pA4RroLV.js} +10709 -11140
- package/dist/cdn/svgrid.js +27 -27
- package/dist/cdn/svgrid.svelte-external.js +27 -27
- package/dist/cdn/validate-XuMe4_KR.js +80 -0
- 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 -47
- package/dist/filtering/excel-filters.js +41 -6
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/keyboard-handlers.js +5 -1
- package/dist/selection.d.ts +2 -2
- package/dist/selection.js +46 -12
- 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/{subscribe.js → subscribe.svelte.js} +5 -1
- package/dist/validate.js +13 -14
- 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 +483 -122
- package/src/SvGrid.css +186 -3
- package/src/SvGrid.svelte +285 -164
- 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 -49
- package/src/counting-cell.test.svelte +11 -0
- package/src/filtering/excel-filters.ts +39 -6
- package/src/headless.reactivity.svelte.test.ts +89 -0
- package/src/headless.ts +1 -1
- package/src/index.ts +7 -1
- package/src/keyboard-handlers.ts +5 -1
- package/src/new-features.test.ts +24 -0
- package/src/selection.test.ts +51 -0
- package/src/selection.ts +47 -14
- 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/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
- package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
- package/src/svgrid.behavior.test.ts +58 -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 +20 -4
- package/src/validate.ts +13 -16
- 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
- package/dist/cdn/validate-AZoD8BoR.js +0 -80
- /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
|
@@ -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
|
|
@@ -1386,11 +1464,22 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1386
1464
|
return rawRows;
|
|
1387
1465
|
let rows = rawRows;
|
|
1388
1466
|
if (globalFilter.trim()) {
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1467
|
+
const locale = props.filterLocale ?? props.localization?.locale;
|
|
1468
|
+
const needle = normalizeForFilter(globalFilter, locale);
|
|
1469
|
+
// Read the values, not the cells. `getAllCells()` built a cell object
|
|
1470
|
+
// with two closures per column on EVERY row and kept them on the row:
|
|
1471
|
+
// ten million objects for one keystroke over a million rows, and a
|
|
1472
|
+
// search that froze the page for seconds. `cell.getValue()` and
|
|
1473
|
+
// `getCellValueByColumnId` read the same per-row values array, so the
|
|
1474
|
+
// answer is the same. The columns are the ones the rows were built with.
|
|
1475
|
+
const columnIds = grid.getAllColumns().map((c) => c.id);
|
|
1476
|
+
rows = denseRows(rows).filter((row) => {
|
|
1477
|
+
for (const id of columnIds) {
|
|
1478
|
+
if (normalizeForFilter(String(row.getCellValueByColumnId(id) ?? ""), locale).includes(needle))
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
return false;
|
|
1482
|
+
});
|
|
1394
1483
|
}
|
|
1395
1484
|
rows = applyColumnFilters(rows);
|
|
1396
1485
|
// --- Advanced filter (Pro) ---------------------------------------------
|
|
@@ -1647,11 +1736,17 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1647
1736
|
});
|
|
1648
1737
|
/** Whether the bar has anything to show: the model's count when it has one, else the loaded ticks. */
|
|
1649
1738
|
const selectionBarVisible = $derived(selectionBarOn && selectionBarCount > 0);
|
|
1650
|
-
// Forward selection changes to the consumer.
|
|
1651
|
-
//
|
|
1652
|
-
|
|
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;
|
|
1653
1744
|
$effect(() => {
|
|
1654
1745
|
const serialized = JSON.stringify(rowSelectionState);
|
|
1746
|
+
if (lastSelectionSerialized === null) {
|
|
1747
|
+
lastSelectionSerialized = serialized;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1655
1750
|
if (serialized === lastSelectionSerialized)
|
|
1656
1751
|
return;
|
|
1657
1752
|
lastSelectionSerialized = serialized;
|
|
@@ -1673,16 +1768,23 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1673
1768
|
// Forward cell-selection rectangle changes to the consumer. Same
|
|
1674
1769
|
// dedupe pattern - fires only when the serialized rectangle changes
|
|
1675
1770
|
// so consumers don't see spurious callbacks during re-renders.
|
|
1676
|
-
|
|
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;
|
|
1677
1779
|
$effect(() => {
|
|
1678
|
-
//
|
|
1679
|
-
// active one last, the order getSelectionRects gives: a consumer's
|
|
1680
|
-
// status bar and shading cover the whole selection, not only its last
|
|
1681
|
-
// range. Depend on selectionRanges so a change to the committed set
|
|
1682
|
-
// re-runs this too.
|
|
1780
|
+
// Depend on selectionRanges so a change to the committed set re-runs this too.
|
|
1683
1781
|
void selectionRanges;
|
|
1684
|
-
const ranges =
|
|
1782
|
+
const ranges = currentCellRanges();
|
|
1685
1783
|
const serialized = JSON.stringify(ranges);
|
|
1784
|
+
if (lastCellRangeSerialized === null) {
|
|
1785
|
+
lastCellRangeSerialized = serialized;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1686
1788
|
if (serialized === lastCellRangeSerialized)
|
|
1687
1789
|
return;
|
|
1688
1790
|
lastCellRangeSerialized = serialized;
|
|
@@ -2487,25 +2589,39 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2487
2589
|
}
|
|
2488
2590
|
// Forward sort-clause changes to the consumer. Same dedupe pattern as the
|
|
2489
2591
|
// selection callback above - fires only when the serialized clauses change.
|
|
2490
|
-
|
|
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));
|
|
2491
2598
|
$effect(() => {
|
|
2492
2599
|
gridStateVersion;
|
|
2493
|
-
const sorting = (
|
|
2600
|
+
const sorting = currentSorting();
|
|
2494
2601
|
const serialized = JSON.stringify(sorting);
|
|
2495
2602
|
if (serialized === lastSortingSerialized)
|
|
2496
2603
|
return;
|
|
2497
2604
|
lastSortingSerialized = serialized;
|
|
2498
2605
|
props.onSortingChange?.(sorting);
|
|
2499
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
|
+
});
|
|
2500
2619
|
// Forward filter-state changes to the consumer. Consolidates the three
|
|
2501
2620
|
// wrapper-managed filter stores (global text, per-column operator filters,
|
|
2502
2621
|
// facet checklists) into one shape so server-side consumers can build a
|
|
2503
2622
|
// single query. Skipped entirely when no callback is registered to avoid
|
|
2504
2623
|
// serializing on every keystroke.
|
|
2505
|
-
|
|
2506
|
-
$effect(() => {
|
|
2507
|
-
if (!props.onFiltersChange)
|
|
2508
|
-
return;
|
|
2624
|
+
function currentFilters() {
|
|
2509
2625
|
const menuEntries = Object.entries(filterMenuValues)
|
|
2510
2626
|
.filter(([, f]) => {
|
|
2511
2627
|
if (f.operator === "isBlank" || f.operator === "isNotBlank")
|
|
@@ -2538,10 +2654,17 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2538
2654
|
? { ...existing, selectedValues: entry.selectedValues }
|
|
2539
2655
|
: entry);
|
|
2540
2656
|
}
|
|
2541
|
-
|
|
2657
|
+
return {
|
|
2542
2658
|
global: globalFilter,
|
|
2543
2659
|
columns: Array.from(merged.values()),
|
|
2544
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();
|
|
2545
2668
|
const serialized = JSON.stringify(payload);
|
|
2546
2669
|
if (serialized === lastFiltersSerialized)
|
|
2547
2670
|
return;
|
|
@@ -2701,6 +2824,33 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2701
2824
|
const frozenRowList = $derived(frozenRowCount ? allRows.slice(0, frozenRowCount) : []);
|
|
2702
2825
|
/** The merged cells, indexed; null (the usual case) costs nothing. */
|
|
2703
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);
|
|
2704
2854
|
const frozenBandHeight = $derived.by(() => {
|
|
2705
2855
|
if (!frozenRowCount)
|
|
2706
2856
|
return 0;
|
|
@@ -2848,52 +2998,210 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2848
2998
|
return item;
|
|
2849
2999
|
});
|
|
2850
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
|
+
};
|
|
2851
3022
|
const renderedColumnItems = $derived.by(function renderedColumnItems_d() {
|
|
2852
3023
|
if (!columnVirtualizationEnabled)
|
|
2853
3024
|
return allColumnItems();
|
|
2854
3025
|
// Column virtualization is ON by default and its virtualizer, like the row
|
|
2855
3026
|
// one, only learns `count` from an effect - which never runs on a server.
|
|
2856
|
-
// Without this the server emitted rows containing no data cells.
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
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
|
+
}
|
|
2862
3032
|
// Pinned columns are position:sticky, so they only stay pinned while their
|
|
2863
|
-
// cell is in the DOM.
|
|
2864
|
-
//
|
|
2865
|
-
//
|
|
2866
|
-
//
|
|
2867
|
-
//
|
|
2868
|
-
//
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
const hasLeft = effectivePinning.left.length > 0;
|
|
2874
|
-
const hasRight = effectivePinning.right.length > 0;
|
|
2875
|
-
if ((!hasLeft && !hasRight) || window.length === 0)
|
|
2876
|
-
return window;
|
|
2877
|
-
const firstIdx = window[0].index;
|
|
2878
|
-
const lastIdx = window[window.length - 1].index;
|
|
2879
|
-
const startIndex = hasLeft ? 0 : firstIdx;
|
|
2880
|
-
const endIndex = hasRight ? allColumns.length - 1 : lastIdx;
|
|
2881
|
-
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)
|
|
2882
3043
|
return window;
|
|
3044
|
+
const total = allColumns.length;
|
|
2883
3045
|
const items = [];
|
|
3046
|
+
// Pinned-left prefix: offsets from 0.
|
|
2884
3047
|
let offset = 0;
|
|
2885
|
-
for (let i = 0; i <
|
|
2886
|
-
offset += getColumnWidth(allColumns[i].id);
|
|
2887
|
-
for (let i = startIndex; i <= endIndex; i += 1) {
|
|
3048
|
+
for (let i = 0; i < Math.min(leftCount, total); i += 1) {
|
|
2888
3049
|
const size = getColumnWidth(allColumns[i].id);
|
|
2889
3050
|
items.push({ index: i, key: i, size, start: offset, end: offset + size });
|
|
2890
3051
|
offset += size;
|
|
2891
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]);
|
|
2892
3076
|
return items;
|
|
2893
3077
|
});
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
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
|
+
}
|
|
2897
3205
|
const totalColumnWidth = $derived.by(function totalColumnWidth_d() {
|
|
2898
3206
|
if (columnVirtualizationEnabled)
|
|
2899
3207
|
return virtualColumnTotalSize;
|
|
@@ -2939,25 +3247,37 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2939
3247
|
const base = groupHeaderRows;
|
|
2940
3248
|
if (!columnVirtualizationEnabled || base.length === 0)
|
|
2941
3249
|
return base;
|
|
2942
|
-
const
|
|
2943
|
-
if (
|
|
3250
|
+
const runs = renderedColumnRuns;
|
|
3251
|
+
if (runs.length === 0)
|
|
2944
3252
|
return base;
|
|
2945
|
-
|
|
2946
|
-
|
|
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.
|
|
2947
3256
|
return base.map((row) => {
|
|
2948
3257
|
const cells = [];
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
const
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
widthPx
|
|
2959
|
-
|
|
2960
|
-
|
|
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
|
+
});
|
|
2961
3281
|
return { ...row, cells };
|
|
2962
3282
|
});
|
|
2963
3283
|
});
|
|
@@ -3343,9 +3663,13 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3343
3663
|
// instead of `scrollVersion` so this effect does NOT re-run on every
|
|
3344
3664
|
// scroll event - which would otherwise re-call setOptions hundreds of
|
|
3345
3665
|
// times during a drag.
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
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)
|
|
3349
3673
|
: (props.containerHeight ?? 520);
|
|
3350
3674
|
const rh = props.rowHeight;
|
|
3351
3675
|
// Auto height: read measured sizes, falling back to the fixed rowHeight as
|
|
@@ -3373,7 +3697,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3373
3697
|
count: allRows.length,
|
|
3374
3698
|
estimateSize,
|
|
3375
3699
|
overscan: props.overscan ?? 8,
|
|
3376
|
-
viewportHeight,
|
|
3700
|
+
viewportHeight: measuredHeight,
|
|
3377
3701
|
});
|
|
3378
3702
|
});
|
|
3379
3703
|
// Track size changes of the shell so the virtualizer's viewport, the
|
|
@@ -3422,28 +3746,76 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3422
3746
|
});
|
|
3423
3747
|
$effect(() => {
|
|
3424
3748
|
// Reading columnWidths here registers it as a reactive dependency so
|
|
3425
|
-
// the effect re-runs when the user resizes a column.
|
|
3426
|
-
//
|
|
3427
|
-
//
|
|
3428
|
-
|
|
3429
|
-
|
|
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);
|
|
3430
3782
|
columnVirtualizer.setOptions({
|
|
3431
|
-
count:
|
|
3432
|
-
estimateSize: (index) =>
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
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,
|
|
3438
3791
|
});
|
|
3439
3792
|
});
|
|
3793
|
+
// The viewport on its own: a width change re-windows the columns without
|
|
3794
|
+
// rebuilding their offsets.
|
|
3440
3795
|
$effect(() => {
|
|
3441
|
-
|
|
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)
|
|
3442
3809
|
return;
|
|
3810
|
+
if (el !== scrollSyncedContainer) {
|
|
3811
|
+
scrollSyncedContainer = el;
|
|
3812
|
+
pendingScrollTop = el.scrollTop;
|
|
3813
|
+
pendingScrollLeft = el.scrollLeft;
|
|
3814
|
+
}
|
|
3443
3815
|
if (rowVirtualizationEnabled)
|
|
3444
|
-
virtualizer.setScrollOffset(domToLogicalRowOffset(
|
|
3816
|
+
virtualizer.setScrollOffset(domToLogicalRowOffset(pendingScrollTop));
|
|
3445
3817
|
if (columnVirtualizationEnabled)
|
|
3446
|
-
columnVirtualizer.setHorizontalOffset(
|
|
3818
|
+
columnVirtualizer.setHorizontalOffset(pendingScrollLeft);
|
|
3447
3819
|
});
|
|
3448
3820
|
// Re-arms once the user scrolls away from the bottom, so a long lazy-load
|
|
3449
3821
|
// run only fires `onScrollBottomReached` once per arrival at the end.
|
|
@@ -3971,6 +4343,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3971
4343
|
get columnWidths() { return columnWidths; },
|
|
3972
4344
|
get collapsedColumns() { return collapsedColumns; },
|
|
3973
4345
|
get mergeIndex() { return mergeIndex; },
|
|
4346
|
+
get flexRows() { return flexRows; },
|
|
4347
|
+
get flexLayoutActive() { return flexLayoutActive; },
|
|
3974
4348
|
get setColumnCollapsed() { return setColumnCollapsed; },
|
|
3975
4349
|
set columnWidths(v) { columnWidths = v; },
|
|
3976
4350
|
get MIN_COLUMN_WIDTH() { return MIN_COLUMN_WIDTH; },
|
|
@@ -4442,6 +4816,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
4442
4816
|
get renderedColumnItems() { return renderedColumnItems; },
|
|
4443
4817
|
get hasRenderedColumn() { return hasRenderedColumn; },
|
|
4444
4818
|
get renderedColumns() { return renderedColumns; },
|
|
4819
|
+
get renderedColumnRuns() { return renderedColumnRuns; },
|
|
4820
|
+
get columnRunOf() { return columnRunOf; },
|
|
4445
4821
|
get totalColumnWidth() { return totalColumnWidth; },
|
|
4446
4822
|
get hasHorizontalOverflow() { return hasHorizontalOverflow; },
|
|
4447
4823
|
get columnWindowStart() { return columnWindowStart; },
|