@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
|
@@ -151,6 +151,41 @@ function scanInTokens(value) {
|
|
|
151
151
|
// pull in SvGrid.svelte, which is a runtime cost (and a bundler hazard) for
|
|
152
152
|
// anything that only wants operator semantics.
|
|
153
153
|
export { ALL_FILTER_OPERATORS, SET_OPERATOR_IDS, VALUELESS_OPERATOR_IDS, RANGE_OPERATOR_IDS, isSetOperator, isValuelessOperator, isRangeOperator, } from './filter-operator-catalogue.js';
|
|
154
|
+
/**
|
|
155
|
+
* A text test, remembered per distinct cell text.
|
|
156
|
+
*
|
|
157
|
+
* The text operators fold every cell (lowercase, and for non-ASCII text NFD
|
|
158
|
+
* plus diacritic stripping) before comparing, and a column the user filters
|
|
159
|
+
* is usually a category - region, status, owner - with a handful of distinct
|
|
160
|
+
* values over every row. Folding each row again was most of a 1M-row filter
|
|
161
|
+
* (417 ms). The answer depends only on the cell's text, so it is kept per
|
|
162
|
+
* text. A column that turns out mostly unique (more than half of the first
|
|
163
|
+
* 2,048 lookups missed) stops remembering and tests directly, and the memo
|
|
164
|
+
* never grows past MEMO_CAP entries.
|
|
165
|
+
*/
|
|
166
|
+
const MEMO_CAP = 16384;
|
|
167
|
+
function memoText(test) {
|
|
168
|
+
let memo = new Map();
|
|
169
|
+
let lookups = 0;
|
|
170
|
+
let hits = 0;
|
|
171
|
+
return (cellValue) => {
|
|
172
|
+
const s = typeof cellValue === 'string' ? cellValue : String(cellValue ?? '');
|
|
173
|
+
if (memo === null)
|
|
174
|
+
return test(s);
|
|
175
|
+
lookups++;
|
|
176
|
+
const known = memo.get(s);
|
|
177
|
+
if (known !== undefined) {
|
|
178
|
+
hits++;
|
|
179
|
+
return known;
|
|
180
|
+
}
|
|
181
|
+
const result = test(s);
|
|
182
|
+
if (memo.size < MEMO_CAP)
|
|
183
|
+
memo.set(s, result);
|
|
184
|
+
if (lookups >= 2048 && hits * 2 < lookups)
|
|
185
|
+
memo = null;
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
154
189
|
/**
|
|
155
190
|
* Compile a filter once, then test many rows against it.
|
|
156
191
|
*
|
|
@@ -168,23 +203,23 @@ export function compileExcelFilter(filter, options) {
|
|
|
168
203
|
const needle = fold(filter.value);
|
|
169
204
|
switch (filter.operator) {
|
|
170
205
|
case 'contains':
|
|
171
|
-
return (
|
|
206
|
+
return memoText((s) => fold(s).includes(needle));
|
|
172
207
|
case 'notContains':
|
|
173
208
|
// An empty needle is no constraint (mirrors `contains` returning true),
|
|
174
209
|
// so nothing is excluded until the user types something.
|
|
175
210
|
if (needle === '')
|
|
176
211
|
return () => true;
|
|
177
|
-
return (
|
|
212
|
+
return memoText((s) => !fold(s).includes(needle));
|
|
178
213
|
case 'equals':
|
|
179
|
-
return (
|
|
214
|
+
return memoText((s) => fold(s) === needle);
|
|
180
215
|
case 'notEquals':
|
|
181
216
|
if (needle === '')
|
|
182
217
|
return () => true;
|
|
183
|
-
return (
|
|
218
|
+
return memoText((s) => fold(s) !== needle);
|
|
184
219
|
case 'startsWith':
|
|
185
|
-
return (
|
|
220
|
+
return memoText((s) => fold(s).startsWith(needle));
|
|
186
221
|
case 'endsWith':
|
|
187
|
-
return (
|
|
222
|
+
return memoText((s) => fold(s).endsWith(needle));
|
|
188
223
|
case 'regex': {
|
|
189
224
|
// Case-insensitive by default (matches the accent/case-folded feel of
|
|
190
225
|
// the other text operators). An invalid pattern matches nothing rather
|
package/dist/index.d.ts
CHANGED
|
@@ -215,6 +215,7 @@ export { createHyperFormulaSheet, type HyperFormulaSheet, type HyperFormulaSheet
|
|
|
215
215
|
export { createCollaboration, broadcastChannelTransport, type CollabUser, type CollabCell, type CollabPresence, type CollabMessage, type CollabTransport, type Collaboration, } from './collaboration.js';
|
|
216
216
|
export { createServerDataSource, type ServerDataSource, type ServerSelectionRule, type ServerRequest, type ServerResult, type ServerController, type ServerControllerOptions, type ServerState, type ServerSortModel, type ServerFilterModel, type ServerAggFn, type ServerAggregation, type ServerGroupRow, type ServerLeafRow, type ServerMoreRow, type ServerFooterRow, type ServerSkeletonRow, type ServerGrandTotalRow, type ServerPlaceholderRow, type ServerDisplayRow, type ServerDetailRow, } from './server-data-source.js';
|
|
217
217
|
export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
|
|
218
|
+
export { createWindowedData, windowedSourceOf, isWindowedData, type WindowedSource, } from './windowed-data.js';
|
|
218
219
|
export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } from './row-model.js';
|
|
219
220
|
export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, type SavedView, type ViewStorage, type NamedViews, type AutoSavedViewOptions, } from './named-views.js';
|
|
220
221
|
export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, type ConditionalFormat, type ConditionalFormatSpec, type ColorScaleFormat, type ColorScaleStop, type ScaleBounds, type DataBarFormat, type IconSetFormat, type RuleFormat, type ResolvedCellFormat, } from './conditional-formatting.js';
|
package/dist/index.js
CHANGED
|
@@ -255,6 +255,7 @@ export { createHyperFormulaSheet, } from './hyperformula-adapter.js';
|
|
|
255
255
|
export { createCollaboration, broadcastChannelTransport, } from './collaboration.js';
|
|
256
256
|
export { createServerDataSource, } from './server-data-source.js';
|
|
257
257
|
export { createBlockCache, createRowPlaceholder, rowPlaceholderState, } from './server-block-cache.js';
|
|
258
|
+
export { createWindowedData, windowedSourceOf, isWindowedData, } from './windowed-data.js';
|
|
258
259
|
export { toServerFilterColumns, } from './row-model.js';
|
|
259
260
|
export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, } from './named-views.js';
|
|
260
261
|
export { resolveCellFormat, computeColumnStat, lerpColor, rgba, contrastText, formatNeedsStats, formatsNeedingStats, } from './conditional-formatting.js';
|
package/dist/selection.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type RowData, type TableFeatures } from "./index.js";
|
|
2
2
|
import "./sv-grid-scrollbar.js";
|
|
3
3
|
export declare function createSelection<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(ctx: any): {
|
|
4
|
-
isRowSelected: (rowId: string) => any;
|
|
5
|
-
toggleRowSelectionById: (rowId: string) => void;
|
|
4
|
+
isRowSelected: (rowId: string, original?: unknown) => any;
|
|
5
|
+
toggleRowSelectionById: (rowId: string, original?: unknown) => void;
|
|
6
6
|
toggleSelectAllRows: () => void;
|
|
7
7
|
setSelectAllRows: (select: boolean) => void;
|
|
8
8
|
setActiveCell: (rowIndex: number, colIndex: number) => void;
|
package/dist/selection.js
CHANGED
|
@@ -6,21 +6,33 @@ import "./sv-grid-scrollbar.js";
|
|
|
6
6
|
import { getColumnBaseValue, isGroupRow, } from "./cell-values.js";
|
|
7
7
|
import { expandRectToMerges, originOf, endOf } from "./merges.js";
|
|
8
8
|
export function createSelection(ctx) {
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A selection model needs the row's data. The body passes the row it is
|
|
11
|
+
* drawing as `original`; without it the row is looked up by id, a scan of
|
|
12
|
+
* every row - on a server model's windowed million rows, once per drawn
|
|
13
|
+
* row, that scan held the page for seconds after a sort.
|
|
14
|
+
*/
|
|
15
|
+
function rowOriginal(rowId) {
|
|
16
|
+
const row = ctx.allRows.find((r) => r.id === rowId);
|
|
17
|
+
return row ? { found: true, original: row.original } : { found: false };
|
|
18
|
+
}
|
|
19
|
+
function isRowSelected(rowId, original) {
|
|
10
20
|
const model = ctx.props.rowSelectionModel;
|
|
11
21
|
if (model) {
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
if (original !== undefined)
|
|
23
|
+
return model.isSelected(rowId, original);
|
|
24
|
+
const hit = rowOriginal(rowId);
|
|
25
|
+
return hit.found ? model.isSelected(rowId, hit.original) : false;
|
|
14
26
|
}
|
|
15
27
|
return Boolean(ctx.rowSelectionState[rowId]);
|
|
16
28
|
}
|
|
17
|
-
function toggleRowSelectionById(rowId) {
|
|
29
|
+
function toggleRowSelectionById(rowId, original) {
|
|
18
30
|
const model = ctx.props.rowSelectionModel;
|
|
19
31
|
if (model) {
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
32
|
+
const hit = original !== undefined ? { found: true, original } : rowOriginal(rowId);
|
|
33
|
+
if (!hit.found)
|
|
22
34
|
return;
|
|
23
|
-
model.toggle(rowId,
|
|
35
|
+
model.toggle(rowId, hit.original, !model.isSelected(rowId, hit.original));
|
|
24
36
|
return;
|
|
25
37
|
}
|
|
26
38
|
ctx.grid.setRowSelection((prev) => ({ ...prev, [rowId]: !prev[rowId] }));
|
|
@@ -446,8 +458,20 @@ export function createSelection(ctx) {
|
|
|
446
458
|
}
|
|
447
459
|
/** Look up a column by id without depending on `buildApi`'s private
|
|
448
460
|
* closure (those helpers don't exist at this scope). */
|
|
461
|
+
// id -> column, rebuilt when the column list changes. Called per header
|
|
462
|
+
// cell as columns scroll into view, so a linear `find` made every scroll
|
|
463
|
+
// frame cost more the more columns the grid has.
|
|
464
|
+
let columnsById = new Map();
|
|
465
|
+
let columnsByIdFor = null;
|
|
449
466
|
function findColumnById(columnId) {
|
|
450
|
-
|
|
467
|
+
const all = ctx.allColumns;
|
|
468
|
+
if (all !== columnsByIdFor) {
|
|
469
|
+
columnsById = new Map();
|
|
470
|
+
for (const c of all)
|
|
471
|
+
columnsById.set(c.id, c);
|
|
472
|
+
columnsByIdFor = all;
|
|
473
|
+
}
|
|
474
|
+
return columnsById.get(columnId);
|
|
451
475
|
}
|
|
452
476
|
function onCellPointerDown(rowIndex, colIndex, event) {
|
|
453
477
|
if (event.button !== 0)
|
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Block cache for server-backed rows - the engine behind infinite scrolling.
|
|
3
|
-
*
|
|
4
|
-
* Paging asks for "page 3". Infinite scrolling asks a different question: the
|
|
5
|
-
* user is looking at rows 4,000 to 4,020 of a table with a million rows, so
|
|
6
|
-
* fetch the block that covers them, keep a bounded number of recently-seen
|
|
7
|
-
* blocks, and render something sensible for every row nobody has fetched yet.
|
|
8
|
-
* That is all this module does, in plain TypeScript with no Svelte and no DOM.
|
|
9
|
-
*
|
|
10
|
-
* The shape of the problem (and most of the vocabulary) is the same one every
|
|
11
|
-
* grid with a server row model solves, so the options here are deliberately
|
|
12
|
-
* recognisable: `blockSize`, `maxBlocksInCache`, `maxConcurrentRequests`,
|
|
13
|
-
* `blockLoadDebounceMs`, `initialRowCount`, `overflowRows`.
|
|
14
|
-
*
|
|
15
|
-
* Three things it takes seriously:
|
|
16
|
-
*
|
|
17
|
-
* - **Unloaded rows still have to render.** `rows()` always returns a dense
|
|
18
|
-
* array, with a shared sentinel object in every slot that has no data. The
|
|
19
|
-
* grid recognises those (see {@link rowPlaceholderState}) and draws a
|
|
20
|
-
* skeleton or a retry affordance instead of an empty row, so the scrollbar
|
|
21
|
-
* never lies and scrolling never leaves a hole.
|
|
22
|
-
* - **The row count may be unknown.** A backend that cannot cheaply count
|
|
23
|
-
* says so (omit `rowCount`, or send `-1`) and the list grows by a block at
|
|
24
|
-
* a time until a short block proves where the end is.
|
|
25
|
-
* - **Requests are not free.** In-flight blocks are deduplicated, at most
|
|
26
|
-
* `maxConcurrentRequests` are open at once, a fast scroll debounces rather
|
|
27
|
-
* than firing a request per frame, and blocks abandoned by eviction or
|
|
28
|
-
* purge are aborted.
|
|
29
|
-
*
|
|
30
|
-
* Enterprise's server row model runs one of these per group level; the free
|
|
31
|
-
* flat controller runs exactly one.
|
|
32
|
-
*/
|
|
33
1
|
/**
|
|
34
2
|
* The mark that says "this row is not data yet".
|
|
35
3
|
*
|
|
@@ -155,6 +123,13 @@ export type BlockCache<TData> = {
|
|
|
155
123
|
setViewport(startIndex: number, endIndex: number): void;
|
|
156
124
|
/** The dense row array to hand the grid. Placeholders fill unloaded slots. */
|
|
157
125
|
rows(): ReadonlyArray<TData>;
|
|
126
|
+
/**
|
|
127
|
+
* The same rows as windowed data (see windowed-data.ts): `length` is the
|
|
128
|
+
* row count and each entry is read through `getRow` when the grid asks for
|
|
129
|
+
* it, so handing it over costs O(1) whatever the count. A new array per
|
|
130
|
+
* change, like `rows()`.
|
|
131
|
+
*/
|
|
132
|
+
windowedRows(): ReadonlyArray<TData>;
|
|
158
133
|
/** One row, without building the array. Returns a placeholder when unloaded. */
|
|
159
134
|
getRow(index: number): TData | typeof LOADING_ROW | typeof FAILED_ROW;
|
|
160
135
|
rowCount(): number | null;
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* Enterprise's server row model runs one of these per group level; the free
|
|
31
31
|
* flat controller runs exactly one.
|
|
32
32
|
*/
|
|
33
|
+
import { createWindowedData } from './windowed-data.js';
|
|
33
34
|
/**
|
|
34
35
|
* The mark that says "this row is not data yet".
|
|
35
36
|
*
|
|
@@ -99,6 +100,7 @@ export function createBlockCache(options) {
|
|
|
99
100
|
let debounceTimer = null;
|
|
100
101
|
let emitScheduled = false;
|
|
101
102
|
let rowsCache = null;
|
|
103
|
+
let windowedCache = null;
|
|
102
104
|
const blockOf = (rowIndex) => Math.floor(rowIndex / blockSize);
|
|
103
105
|
const startOf = (blockIndex) => blockIndex * blockSize;
|
|
104
106
|
/**
|
|
@@ -156,7 +158,7 @@ export function createBlockCache(options) {
|
|
|
156
158
|
* mutations notifies once. Every mutating path ends here.
|
|
157
159
|
*/
|
|
158
160
|
function emit() {
|
|
159
|
-
rowsCache = null; // rebuilt lazily, on the next read
|
|
161
|
+
rowsCache = windowedCache = null; // rebuilt lazily, on the next read
|
|
160
162
|
if (disposed || emitScheduled)
|
|
161
163
|
return;
|
|
162
164
|
emitScheduled = true;
|
|
@@ -281,6 +283,15 @@ export function createBlockCache(options) {
|
|
|
281
283
|
ensureBlock(i);
|
|
282
284
|
}
|
|
283
285
|
}
|
|
286
|
+
function rowAt(index) {
|
|
287
|
+
const block = blocks.get(blockOf(index));
|
|
288
|
+
if (!block)
|
|
289
|
+
return LOADING_ROW;
|
|
290
|
+
if (block.status === 'failed')
|
|
291
|
+
return FAILED_ROW;
|
|
292
|
+
const row = block.rows[index - startOf(blockOf(index))];
|
|
293
|
+
return row ?? LOADING_ROW;
|
|
294
|
+
}
|
|
284
295
|
function abortAll() {
|
|
285
296
|
for (const block of blocks.values())
|
|
286
297
|
block.controller?.abort();
|
|
@@ -330,15 +341,13 @@ export function createBlockCache(options) {
|
|
|
330
341
|
rowsCache = out;
|
|
331
342
|
return out;
|
|
332
343
|
},
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return FAILED_ROW;
|
|
339
|
-
const row = block.rows[index - startOf(blockOf(index))];
|
|
340
|
-
return row ?? LOADING_ROW;
|
|
344
|
+
windowedRows() {
|
|
345
|
+
if (windowedCache)
|
|
346
|
+
return windowedCache;
|
|
347
|
+
windowedCache = createWindowedData(currentLength(), (i) => rowAt(i));
|
|
348
|
+
return windowedCache;
|
|
341
349
|
},
|
|
350
|
+
getRow: rowAt,
|
|
342
351
|
rowCount: () => (countKnown ? count : null),
|
|
343
352
|
lastRowKnown: () => countKnown,
|
|
344
353
|
retryFailed() {
|
|
@@ -376,7 +385,7 @@ export function createBlockCache(options) {
|
|
|
376
385
|
blocks.clear();
|
|
377
386
|
count = null;
|
|
378
387
|
countKnown = false;
|
|
379
|
-
rowsCache = null;
|
|
388
|
+
rowsCache = windowedCache = null;
|
|
380
389
|
fetchViewport();
|
|
381
390
|
emit();
|
|
382
391
|
},
|
|
@@ -524,7 +533,7 @@ export function createBlockCache(options) {
|
|
|
524
533
|
controller: null,
|
|
525
534
|
});
|
|
526
535
|
}
|
|
527
|
-
rowsCache = null;
|
|
536
|
+
rowsCache = windowedCache = null;
|
|
528
537
|
}
|
|
529
538
|
/** Forget every block from `fromBlock` on, aborting any in flight. */
|
|
530
539
|
function dropLoadedFrom(fromBlock) {
|
|
@@ -534,6 +543,6 @@ export function createBlockCache(options) {
|
|
|
534
543
|
block.controller?.abort();
|
|
535
544
|
blocks.delete(index);
|
|
536
545
|
}
|
|
537
|
-
rowsCache = null;
|
|
546
|
+
rowsCache = windowedCache = null;
|
|
538
547
|
}
|
|
539
548
|
}
|
|
@@ -53,7 +53,9 @@ export function createServerDataSource(source, options) {
|
|
|
53
53
|
function emitFromCache() {
|
|
54
54
|
if (!cache || disposed)
|
|
55
55
|
return;
|
|
56
|
-
|
|
56
|
+
// Windowed: the grid reads only the rows it shows, so a block landing
|
|
57
|
+
// costs the same at 100M rows as at 10K.
|
|
58
|
+
const rows = cache.windowedRows();
|
|
57
59
|
state.rows = rows;
|
|
58
60
|
state.rowCount = cache.rowCount();
|
|
59
61
|
state.lastRowKnown = cache.lastRowKnown();
|
package/dist/server.d.ts
CHANGED
|
@@ -10,4 +10,5 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { createServerDataSource, type ServerDataSource, type ServerSelectionRule, type ServerRequest, type ServerResult, type ServerController, type ServerControllerOptions, type ServerState, type ServerSortModel, type ServerFilterModel, type ServerAggFn, type ServerAggregation, type ServerGroupRow, type ServerLeafRow, type ServerMoreRow, type ServerFooterRow, type ServerSkeletonRow, type ServerGrandTotalRow, type ServerPlaceholderRow, type ServerDisplayRow, type ServerDetailRow, } from './server-data-source.js';
|
|
12
12
|
export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
|
|
13
|
+
export { createWindowedData, windowedSourceOf, isWindowedData, type WindowedSource, } from './windowed-data.js';
|
|
13
14
|
export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } from './row-model.js';
|
package/dist/server.js
CHANGED
|
@@ -10,4 +10,5 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { createServerDataSource, } from './server-data-source.js';
|
|
12
12
|
export { createBlockCache, createRowPlaceholder, rowPlaceholderState, } from './server-block-cache.js';
|
|
13
|
+
export { createWindowedData, windowedSourceOf, isWindowedData, } from './windowed-data.js';
|
|
13
14
|
export { toServerFilterColumns, } from './row-model.js';
|
package/dist/spreadsheet.js
CHANGED
|
@@ -88,6 +88,13 @@ const OVERLAY_CLASS = 'sv-cell-border-overlay';
|
|
|
88
88
|
* every relevant DOM mutation inside the grid, and whenever the action
|
|
89
89
|
* options update. */
|
|
90
90
|
function apply(root, opts) {
|
|
91
|
+
// Merges here are colspan / rowspan on the rendered cells, which only the
|
|
92
|
+
// table algorithm honours: keep this grid's rows in table layout (the
|
|
93
|
+
// flex-row rules in SvGrid.css skip a table marked this way).
|
|
94
|
+
for (const table of root.querySelectorAll('table.sv-grid-table')) {
|
|
95
|
+
if (table.getAttribute('data-row-layout') !== 'table')
|
|
96
|
+
table.setAttribute('data-row-layout', 'table');
|
|
97
|
+
}
|
|
91
98
|
// ---- 1. Clean up anything the LAST run added -----------------------
|
|
92
99
|
for (const td of root.querySelectorAll(`td[${MARK}]`)) {
|
|
93
100
|
td.removeAttribute('colspan');
|
package/dist/validate.js
CHANGED
|
@@ -155,17 +155,9 @@ export function validateGridConfig(input) {
|
|
|
155
155
|
'so summary bars will under-report. Remove `treeData` and set ' +
|
|
156
156
|
'`gantt.parentField` instead.');
|
|
157
157
|
}
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// which means the natural way to write this silently does nothing.
|
|
162
|
-
const pinned = (input.initialColumnPinning?.left?.length ?? 0) +
|
|
163
|
-
(input.initialColumnPinning?.right?.length ?? 0);
|
|
164
|
-
if (pinned > 0 && input.columnVirtualization !== false) {
|
|
165
|
-
messages.push('[svgrid] `initialColumnPinning` is set while column virtualization is on ' +
|
|
166
|
-
'(its default), so the pinned columns will not stick - the virtualizer ' +
|
|
167
|
-
'recycles column nodes. Add `columnVirtualization={false}`.');
|
|
168
|
-
}
|
|
158
|
+
// (7. was "pinning versus column virtualization". Pinned columns now render
|
|
159
|
+
// as their own runs beside the virtual window, so the pair is supported and
|
|
160
|
+
// there is nothing to warn about.)
|
|
169
161
|
// ---- 8. Server-mode contracts left half-wired -----------------------------
|
|
170
162
|
// Each of these makes the grid hand control to the consumer. Miss the other
|
|
171
163
|
// half and the feature looks broken rather than unconfigured.
|
|
@@ -4,6 +4,10 @@ export declare function createColumnVirtualizer(input: {
|
|
|
4
4
|
viewportWidth: number;
|
|
5
5
|
scrollOffset?: number;
|
|
6
6
|
overscan?: number;
|
|
7
|
+
/** Columns kept behind the scroll direction; defaults to `overscan`. */
|
|
8
|
+
overscanBehind?: number;
|
|
9
|
+
/** Fewest columns left ahead of a scroll before the window moves. */
|
|
10
|
+
overscanMin?: number;
|
|
7
11
|
/** Either a uniform size (number) or a per-column size function. */
|
|
8
12
|
estimateSize?: number | ColumnSizeEstimator;
|
|
9
13
|
}): {
|
|
@@ -6,6 +6,8 @@ export function createColumnVirtualizer(input) {
|
|
|
6
6
|
viewportHeight: input.viewportWidth,
|
|
7
7
|
scrollOffset: input.scrollOffset ?? 0,
|
|
8
8
|
overscan: input.overscan ?? 4,
|
|
9
|
+
overscanBehind: input.overscanBehind,
|
|
10
|
+
overscanMin: input.overscanMin,
|
|
9
11
|
});
|
|
10
12
|
return {
|
|
11
13
|
...virtualizer,
|
|
@@ -15,8 +15,28 @@ export type VirtualizerOptions = {
|
|
|
15
15
|
*/
|
|
16
16
|
estimateSize: number | ((index: number) => number);
|
|
17
17
|
overscan?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Items kept on the side the scroll is moving away from (`overscan` is kept
|
|
20
|
+
* ahead of it). Defaults to `overscan`, a symmetric window. Before the
|
|
21
|
+
* first scroll there is no direction and both sides get `overscan`.
|
|
22
|
+
*/
|
|
23
|
+
overscanBehind?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Keep the rendered window while a scroll leaves at least this many items
|
|
26
|
+
* rendered ahead of it, and move the window only when the scroll runs past
|
|
27
|
+
* that: the window is then rebuilt with the full `overscan` ahead, so it
|
|
28
|
+
* moves `overscan - overscanMin + 1` items at a time instead of one. Each
|
|
29
|
+
* move costs about the same whether one item enters or several, so fewer,
|
|
30
|
+
* larger moves are less work, and the scroll frames in between change
|
|
31
|
+
* nothing. A window built at rest, or after a jump to items outside the
|
|
32
|
+
* rendered window, carries `overscanMin` instead of `overscan`. Undefined
|
|
33
|
+
* moves the window on every item boundary, always with `overscan`.
|
|
34
|
+
*/
|
|
35
|
+
overscanMin?: number;
|
|
18
36
|
viewportHeight: number;
|
|
19
37
|
scrollOffset?: number;
|
|
38
|
+
/** Internal: the sign of the last scroll movement (-1, 0, 1). */
|
|
39
|
+
scrollDirection?: number;
|
|
20
40
|
};
|
|
21
41
|
export type VirtualizerState = {
|
|
22
42
|
items: Array<VirtualItem>;
|
|
@@ -5,6 +5,8 @@ function sameOptions(a, b) {
|
|
|
5
5
|
return (a.count === b.count &&
|
|
6
6
|
a.estimateSize === b.estimateSize &&
|
|
7
7
|
(a.overscan ?? 6) === (b.overscan ?? 6) &&
|
|
8
|
+
a.overscanBehind === b.overscanBehind &&
|
|
9
|
+
a.overscanMin === b.overscanMin &&
|
|
8
10
|
a.viewportHeight === b.viewportHeight &&
|
|
9
11
|
(a.scrollOffset ?? 0) === (b.scrollOffset ?? 0));
|
|
10
12
|
}
|
|
@@ -107,11 +109,12 @@ function buildVariableItems(startIndex, endIndex, offsets) {
|
|
|
107
109
|
}
|
|
108
110
|
return items;
|
|
109
111
|
}
|
|
110
|
-
|
|
112
|
+
/** The items under the viewport at the current scroll offset, before any
|
|
113
|
+
* overscan. */
|
|
114
|
+
function visibleRange(options,
|
|
111
115
|
/** Pre-built cumulative offsets for the variable-size path. */
|
|
112
116
|
offsets) {
|
|
113
117
|
const count = Math.max(options.count, 0);
|
|
114
|
-
const overscan = Math.max(options.overscan ?? 6, 0);
|
|
115
118
|
const viewportHeight = Math.max(options.viewportHeight, 0);
|
|
116
119
|
if (typeof options.estimateSize === 'function' && offsets) {
|
|
117
120
|
const totalSize = offsets[count] ?? 0;
|
|
@@ -140,29 +143,47 @@ offsets) {
|
|
|
140
143
|
hi = mid;
|
|
141
144
|
}
|
|
142
145
|
const visibleEnd = Math.max(lo - 1, visibleStart);
|
|
143
|
-
|
|
144
|
-
const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1);
|
|
145
|
-
return {
|
|
146
|
-
items: endIndex >= startIndex ? buildVariableItems(startIndex, endIndex, offsets) : [],
|
|
147
|
-
totalSize,
|
|
148
|
-
startIndex,
|
|
149
|
-
endIndex,
|
|
150
|
-
scrollOffset,
|
|
151
|
-
viewportHeight,
|
|
152
|
-
};
|
|
146
|
+
return { count, viewportHeight, totalSize, scrollOffset, visibleStart, visibleEnd, uniformSize: 0 };
|
|
153
147
|
}
|
|
154
148
|
// Uniform-size fast path (original behavior).
|
|
155
|
-
const
|
|
156
|
-
const totalSize = count *
|
|
149
|
+
const uniformSize = Math.max(typeof options.estimateSize === 'number' ? options.estimateSize : 1, 1);
|
|
150
|
+
const totalSize = count * uniformSize;
|
|
157
151
|
const maxOffset = Math.max(totalSize - viewportHeight, 0);
|
|
158
152
|
const scrollOffset = clamp(options.scrollOffset ?? 0, 0, maxOffset);
|
|
159
|
-
const visibleStart = Math.floor(scrollOffset /
|
|
160
|
-
const visibleCount = Math.ceil(viewportHeight /
|
|
153
|
+
const visibleStart = Math.floor(scrollOffset / uniformSize);
|
|
154
|
+
const visibleCount = Math.ceil(viewportHeight / uniformSize);
|
|
161
155
|
const visibleEnd = Math.min(visibleStart + visibleCount, Math.max(count - 1, 0));
|
|
162
|
-
|
|
163
|
-
|
|
156
|
+
return { count, viewportHeight, totalSize, scrollOffset, visibleStart, visibleEnd, uniformSize };
|
|
157
|
+
}
|
|
158
|
+
function createState(options,
|
|
159
|
+
/** Pre-built cumulative offsets for the variable-size path. */
|
|
160
|
+
offsets,
|
|
161
|
+
/** The scroll moved on from the window already rendered. With
|
|
162
|
+
* `overscanMin`, only then does the full `overscan` go ahead; a window
|
|
163
|
+
* built at rest or after a jump carries `overscanMin`, so a thumb drag
|
|
164
|
+
* that jumps every frame renders no more than it needs. */
|
|
165
|
+
scrolling = false) {
|
|
166
|
+
const full = Math.max(options.overscan ?? 6, 0);
|
|
167
|
+
const overscan = options.overscanMin === undefined || scrolling ? full : clamp(options.overscanMin, 0, full);
|
|
168
|
+
// The overscan goes ahead of the scroll and `overscanBehind` behind it:
|
|
169
|
+
// items behind the movement were just scrolled past, and a horizontal
|
|
170
|
+
// scroll that kept three columns on each side drew ~40% more cells than it
|
|
171
|
+
// needed to (350 vs 275 in the wide benchmark).
|
|
172
|
+
const behind = Math.min(Math.max(options.overscanBehind ?? full, 0), overscan);
|
|
173
|
+
const direction = options.scrollDirection ?? 0;
|
|
174
|
+
const overscanBefore = direction > 0 ? behind : overscan;
|
|
175
|
+
const overscanAfter = direction < 0 ? behind : overscan;
|
|
176
|
+
const { count, viewportHeight, totalSize, scrollOffset, visibleStart, visibleEnd, uniformSize } = visibleRange(options, offsets);
|
|
177
|
+
const startIndex = count === 0 ? 0 : clamp(visibleStart - overscanBefore, 0, count - 1);
|
|
178
|
+
const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscanAfter, 0, count - 1);
|
|
179
|
+
let items = [];
|
|
180
|
+
if (endIndex >= startIndex) {
|
|
181
|
+
items = uniformSize > 0
|
|
182
|
+
? buildUniformItems(startIndex, endIndex, uniformSize)
|
|
183
|
+
: buildVariableItems(startIndex, endIndex, offsets);
|
|
184
|
+
}
|
|
164
185
|
return {
|
|
165
|
-
items
|
|
186
|
+
items,
|
|
166
187
|
totalSize,
|
|
167
188
|
startIndex,
|
|
168
189
|
endIndex,
|
|
@@ -200,13 +221,54 @@ export function createVirtualizer(initial) {
|
|
|
200
221
|
function emit() {
|
|
201
222
|
listeners.forEach((listener) => listener());
|
|
202
223
|
}
|
|
203
|
-
function recalc() {
|
|
204
|
-
const next = createState(options, getOffsets());
|
|
224
|
+
function recalc(scrolling = false) {
|
|
225
|
+
const next = createState(options, getOffsets(), scrolling);
|
|
205
226
|
if (sameState(state, next))
|
|
206
227
|
return;
|
|
228
|
+
// Hand back the previous item object for a row whose index, offset, size
|
|
229
|
+
// and slot did not change. The grid's row {#each} is keyed by slot, so a
|
|
230
|
+
// row that stays in the window keeps its <tr>, but a new item object
|
|
231
|
+
// still invalidated everything that row and its cells derive from it:
|
|
232
|
+
// every scroll frame re-checked ~300 cells to render the two that came
|
|
233
|
+
// into view, and Svelte's dependency marking was most of the frame's
|
|
234
|
+
// script time (measured 4.8 of 8.3 ms on a 100k-row grid).
|
|
235
|
+
if (state.items.length > 0 && next.items.length > 0) {
|
|
236
|
+
const first = state.items[0].index;
|
|
237
|
+
const items = next.items;
|
|
238
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
239
|
+
const item = items[i];
|
|
240
|
+
const prev = state.items[item.index - first];
|
|
241
|
+
if (prev && prev.index === item.index && prev.start === item.start && prev.size === item.size && prev.key === item.key) {
|
|
242
|
+
items[i] = prev;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
207
246
|
state = next;
|
|
208
247
|
emit();
|
|
209
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* What a scroll does to the window already rendered, with `overscanMin`:
|
|
251
|
+
* 'hold' while that window still covers the visible items plus
|
|
252
|
+
* `overscanMin` ahead of the scroll, 'move' once the scroll runs past that
|
|
253
|
+
* (the window moves on with the full overscan ahead), and 'jump' when the
|
|
254
|
+
* visible items are nowhere in it. Without `overscanMin` every scroll moves
|
|
255
|
+
* the window.
|
|
256
|
+
*/
|
|
257
|
+
function scrollStep() {
|
|
258
|
+
const min = options.overscanMin;
|
|
259
|
+
if (min === undefined || state.items.length === 0)
|
|
260
|
+
return 'move';
|
|
261
|
+
const ahead = clamp(min, 0, Math.max(options.overscan ?? 6, 0));
|
|
262
|
+
const { count, viewportHeight, totalSize, visibleStart, visibleEnd } = visibleRange(options, getOffsets());
|
|
263
|
+
if (count === 0 || viewportHeight !== state.viewportHeight || totalSize !== state.totalSize)
|
|
264
|
+
return 'jump';
|
|
265
|
+
if (visibleStart > state.endIndex || visibleEnd < state.startIndex)
|
|
266
|
+
return 'jump';
|
|
267
|
+
const direction = options.scrollDirection ?? 0;
|
|
268
|
+
const needStart = Math.max(visibleStart - (direction < 0 ? ahead : 0), 0);
|
|
269
|
+
const needEnd = Math.min(visibleEnd + (direction > 0 ? ahead : 0), count - 1);
|
|
270
|
+
return state.startIndex <= needStart && state.endIndex >= needEnd ? 'hold' : 'move';
|
|
271
|
+
}
|
|
210
272
|
return {
|
|
211
273
|
setOptions(next) {
|
|
212
274
|
const merged = { ...options, ...next };
|
|
@@ -229,10 +291,14 @@ export function createVirtualizer(initial) {
|
|
|
229
291
|
recalc();
|
|
230
292
|
},
|
|
231
293
|
setScrollOffset(scrollOffset) {
|
|
232
|
-
|
|
294
|
+
const previous = options.scrollOffset ?? 0;
|
|
295
|
+
if (previous === scrollOffset)
|
|
233
296
|
return;
|
|
234
|
-
options = { ...options, scrollOffset };
|
|
235
|
-
|
|
297
|
+
options = { ...options, scrollOffset, scrollDirection: scrollOffset > previous ? 1 : -1 };
|
|
298
|
+
const step = scrollStep();
|
|
299
|
+
if (step === 'hold')
|
|
300
|
+
return;
|
|
301
|
+
recalc(step === 'move');
|
|
236
302
|
},
|
|
237
303
|
setViewportHeight(viewportHeight) {
|
|
238
304
|
if (options.viewportHeight === viewportHeight)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What marks an array as windowed data, and how to tell (windowed-data.ts has
|
|
3
|
+
* the full story). Kept apart from createWindowedData so code that only asks
|
|
4
|
+
* "is this windowed?" - the controller, the core's per-update paths - does not
|
|
5
|
+
* pull in the windowed row model that createWindowedData installs.
|
|
6
|
+
*/
|
|
7
|
+
export type WindowedSource<T> = {
|
|
8
|
+
/** The number of rows, loaded or not. */
|
|
9
|
+
readonly length: number;
|
|
10
|
+
/** The entry at `index`: a loaded row, a placeholder, or undefined. */
|
|
11
|
+
at(index: number): T | undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* An array of `length` entries read through `at(i)`: a real `Array` (a Proxy
|
|
15
|
+
* over an empty one) carrying the windowed mark. Each call returns a new
|
|
16
|
+
* array identity, which is how a source tells the grid its rows changed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function makeWindowedArray<T>(length: number, at: (index: number) => T | undefined): T[];
|
|
19
|
+
/** The source behind windowed data, or null for an ordinary array. */
|
|
20
|
+
export declare function windowedSourceOf<T>(data: unknown): WindowedSource<T> | null;
|
|
21
|
+
/** True for windowed data. */
|
|
22
|
+
export declare function isWindowedData(data: unknown): boolean;
|