@humanspeak/svelte-virtual-list 0.4.3 → 0.4.5
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/SvelteVirtualList.svelte +13 -9
- package/dist/reactive-list-manager/ReactiveListManager.svelte.d.ts +0 -5
- package/dist/reactive-list-manager/ReactiveListManager.svelte.js +2 -20
- package/dist/utils/virtualList.d.ts +5 -27
- package/dist/utils/virtualList.js +29 -51
- package/package.json +21 -20
|
@@ -178,8 +178,11 @@
|
|
|
178
178
|
import { onMount, tick, untrack } from 'svelte'
|
|
179
179
|
|
|
180
180
|
const rafSchedule = createRafScheduler()
|
|
181
|
-
//
|
|
182
|
-
const
|
|
181
|
+
// Timing constants
|
|
182
|
+
const GLOBAL_CORRECTION_COOLDOWN_MS = 16
|
|
183
|
+
const SCROLL_IDLE_DELAY_MS = 250
|
|
184
|
+
const SUPPRESSION_WINDOW_MS = 450
|
|
185
|
+
const HEIGHT_DEBOUNCE_MS = 100
|
|
183
186
|
const lastCorrectionTimestampByViewport = new WeakMap<HTMLElement, number>()
|
|
184
187
|
// Package-specific debug flag - safe for library distribution
|
|
185
188
|
// Enable with: PUBLIC_SVELTE_VIRTUAL_LIST_DEBUG=true (preferred) or SVELTE_VIRTUAL_LIST_DEBUG=true
|
|
@@ -494,7 +497,7 @@
|
|
|
494
497
|
const now = performance.now()
|
|
495
498
|
const viewportEl = heightManager.viewport
|
|
496
499
|
const lastCorrectionMs = lastCorrectionTimestampByViewport.get(viewportEl) ?? 0
|
|
497
|
-
if (now - lastCorrectionMs <
|
|
500
|
+
if (now - lastCorrectionMs < GLOBAL_CORRECTION_COOLDOWN_MS) {
|
|
498
501
|
suppressBottomAnchoringUntilMs = now + 50
|
|
499
502
|
return
|
|
500
503
|
}
|
|
@@ -524,11 +527,12 @@
|
|
|
524
527
|
// Note: `container: 'nearest'` option could replace this once browser support improves
|
|
525
528
|
const currentScrollTop = heightManager.viewport.scrollTop
|
|
526
529
|
const offset = itemRect.bottom - contRect.bottom
|
|
527
|
-
|
|
530
|
+
syncScrollTop(currentScrollTop + offset)
|
|
528
531
|
log('[SVL] b2t-correction-manual', { offset })
|
|
532
|
+
} else {
|
|
533
|
+
// Sync our internal scroll state with actual DOM position
|
|
534
|
+
heightManager.scrollTop = heightManager.viewport.scrollTop
|
|
529
535
|
}
|
|
530
|
-
// Sync our internal scroll state with actual DOM position
|
|
531
|
-
heightManager.scrollTop = heightManager.viewport.scrollTop
|
|
532
536
|
// After peer correction, delay further corrections briefly
|
|
533
537
|
suppressBottomAnchoringUntilMs = performance.now() + 200
|
|
534
538
|
}
|
|
@@ -688,7 +692,7 @@
|
|
|
688
692
|
})
|
|
689
693
|
heightManager.endDynamicUpdate()
|
|
690
694
|
},
|
|
691
|
-
lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 :
|
|
695
|
+
lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 : HEIGHT_DEBOUNCE_MS,
|
|
692
696
|
dirtyItems, // Pass dirty items for processing
|
|
693
697
|
0, // Don't pass ReactiveListManager state - let each system manage its own totals
|
|
694
698
|
0, // Don't pass ReactiveListManager state - let each system manage its own totals
|
|
@@ -1124,7 +1128,7 @@
|
|
|
1124
1128
|
if (idleCorrectionsOnly || anchorModeEnabled) {
|
|
1125
1129
|
reconcileToAnchorIfEnabled()
|
|
1126
1130
|
}
|
|
1127
|
-
},
|
|
1131
|
+
}, SCROLL_IDLE_DELAY_MS)
|
|
1128
1132
|
|
|
1129
1133
|
rafSchedule(() => {
|
|
1130
1134
|
const current = heightManager.viewport.scrollTop
|
|
@@ -1132,7 +1136,7 @@
|
|
|
1132
1136
|
const delta = lastScrollTopSnapshot - current
|
|
1133
1137
|
if (delta > 0.5) {
|
|
1134
1138
|
// Widen suppression to avoid fighting peer instance corrections
|
|
1135
|
-
suppressBottomAnchoringUntilMs = performance.now() +
|
|
1139
|
+
suppressBottomAnchoringUntilMs = performance.now() + SUPPRESSION_WINDOW_MS
|
|
1136
1140
|
userHasScrolledAway = true
|
|
1137
1141
|
}
|
|
1138
1142
|
}
|
|
@@ -183,11 +183,6 @@ export declare class ReactiveListManager {
|
|
|
183
183
|
* @returns Array of cumulative block sums
|
|
184
184
|
*/
|
|
185
185
|
getBlockSums(): number[];
|
|
186
|
-
/**
|
|
187
|
-
* Build block prefix sums for efficient offset calculations.
|
|
188
|
-
* Uses the same algorithm as the utility function but leverages internal state.
|
|
189
|
-
*/
|
|
190
|
-
private buildBlockSums;
|
|
191
186
|
/**
|
|
192
187
|
* Create a new ReactiveListManager instance
|
|
193
188
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildBlockSums } from '../utils/virtualList.js';
|
|
1
2
|
import { RecomputeScheduler } from './RecomputeScheduler.js';
|
|
2
3
|
/**
|
|
3
4
|
* ReactiveListManager - A standalone reactive height calculation system
|
|
@@ -378,30 +379,11 @@ export class ReactiveListManager {
|
|
|
378
379
|
*/
|
|
379
380
|
getBlockSums() {
|
|
380
381
|
if (!this._blockSumsValid || this._blockSums.length === 0) {
|
|
381
|
-
this._blockSums = this.
|
|
382
|
+
this._blockSums = buildBlockSums(this._heightCache, this._averageHeight, this._itemLength, this._blockSize);
|
|
382
383
|
this._blockSumsValid = true;
|
|
383
384
|
}
|
|
384
385
|
return this._blockSums;
|
|
385
386
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Build block prefix sums for efficient offset calculations.
|
|
388
|
-
* Uses the same algorithm as the utility function but leverages internal state.
|
|
389
|
-
*/
|
|
390
|
-
buildBlockSums() {
|
|
391
|
-
const blocks = Math.ceil(this._itemLength / this._blockSize);
|
|
392
|
-
const sums = new Array(Math.max(0, blocks - 1));
|
|
393
|
-
let running = 0;
|
|
394
|
-
for (let b = 0; b < blocks - 1; b++) {
|
|
395
|
-
const start = b * this._blockSize;
|
|
396
|
-
const end = start + this._blockSize;
|
|
397
|
-
for (let i = start; i < end; i++) {
|
|
398
|
-
const height = this._heightCache[i];
|
|
399
|
-
running += Number.isFinite(height) && height > 0 ? height : this._averageHeight;
|
|
400
|
-
}
|
|
401
|
-
sums[b] = running;
|
|
402
|
-
}
|
|
403
|
-
return sums;
|
|
404
|
-
}
|
|
405
387
|
/**
|
|
406
388
|
* Create a new ReactiveListManager instance
|
|
407
389
|
*
|
|
@@ -62,6 +62,11 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
|
|
|
62
62
|
* @param {number} totalItems - Total number of items in the list
|
|
63
63
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
64
64
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
65
|
+
* @param {boolean} atBottom - Whether the list is scrolled to the bottom (unused, legacy parameter)
|
|
66
|
+
* @param {boolean} wasAtBottomBeforeHeightChange - Whether the list was at bottom before a height change (unused, legacy parameter)
|
|
67
|
+
* @param {SvelteVirtualListPreviousVisibleRange | null} lastVisibleRange - Previous visible range (unused, legacy parameter)
|
|
68
|
+
* @param {number} [totalContentHeight] - Pre-calculated total content height; defaults to totalItems * itemHeight
|
|
69
|
+
* @param {Record<number, number>} [heightCache] - Cache of measured item heights keyed by index, used in topToBottom mode to walk actual heights instead of dividing by average
|
|
65
70
|
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
66
71
|
*/
|
|
67
72
|
export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode, atBottom: boolean, wasAtBottomBeforeHeightChange: boolean, lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null, totalContentHeight?: number, heightCache?: Record<number, number>) => SvelteVirtualListPreviousVisibleRange;
|
|
@@ -138,33 +143,6 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
|
|
|
138
143
|
delta: number;
|
|
139
144
|
}>;
|
|
140
145
|
};
|
|
141
|
-
/**
|
|
142
|
-
* Processes large arrays in chunks to prevent UI blocking.
|
|
143
|
-
*
|
|
144
|
-
* This function implements a progressive processing strategy that:
|
|
145
|
-
* 1. Breaks down large arrays into manageable chunks
|
|
146
|
-
* 2. Processes each chunk asynchronously
|
|
147
|
-
* 3. Reports progress after each chunk
|
|
148
|
-
* 4. Yields to the main thread between chunks
|
|
149
|
-
*
|
|
150
|
-
* @param {any[]} items - Array of items to process
|
|
151
|
-
* @param {number} chunkSize - Number of items to process in each chunk
|
|
152
|
-
* @param {(processed: number) => void} onProgress - Callback for progress updates
|
|
153
|
-
* @param {() => void} onComplete - Callback when all processing is complete
|
|
154
|
-
*
|
|
155
|
-
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* await processChunked(
|
|
159
|
-
* largeArray,
|
|
160
|
-
* 50,
|
|
161
|
-
* (processed) => console.log(`Processed ${processed} items`),
|
|
162
|
-
* () => console.log('All items processed')
|
|
163
|
-
* )
|
|
164
|
-
*/
|
|
165
|
-
export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
166
|
-
chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
|
|
167
|
-
onComplete: () => void) => Promise<void>;
|
|
168
146
|
/**
|
|
169
147
|
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
170
148
|
*
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
export const getValidHeight = (height, fallback) => Number.isFinite(height) && height > 0 ? height : fallback;
|
|
19
|
+
const BOTTOM_TOLERANCE_FACTOR = 0.25;
|
|
19
20
|
/**
|
|
20
21
|
* Clamps a numeric value to be within a specified range.
|
|
21
22
|
*
|
|
@@ -65,6 +66,11 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
|
|
|
65
66
|
* @param {number} totalItems - Total number of items in the list
|
|
66
67
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
67
68
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
69
|
+
* @param {boolean} atBottom - Whether the list is scrolled to the bottom (unused, legacy parameter)
|
|
70
|
+
* @param {boolean} wasAtBottomBeforeHeightChange - Whether the list was at bottom before a height change (unused, legacy parameter)
|
|
71
|
+
* @param {SvelteVirtualListPreviousVisibleRange | null} lastVisibleRange - Previous visible range (unused, legacy parameter)
|
|
72
|
+
* @param {number} [totalContentHeight] - Pre-calculated total content height; defaults to totalItems * itemHeight
|
|
73
|
+
* @param {Record<number, number>} [heightCache] - Cache of measured item heights keyed by index, used in topToBottom mode to walk actual heights instead of dividing by average
|
|
68
74
|
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
69
75
|
*/
|
|
70
76
|
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode, atBottom, wasAtBottomBeforeHeightChange, lastVisibleRange, totalContentHeight, heightCache) => {
|
|
@@ -91,23 +97,38 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
91
97
|
return { start, end };
|
|
92
98
|
}
|
|
93
99
|
else {
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
// Walk forward through measured heights to find the correct start index
|
|
101
|
+
// instead of dividing by average height (which is wrong for variable-height items).
|
|
102
|
+
let start = 0;
|
|
103
|
+
let acc = 0;
|
|
104
|
+
while (start < totalItems) {
|
|
105
|
+
const h = getValidHeight(heightCache?.[start], itemHeight);
|
|
106
|
+
if (acc + h > scrollTop)
|
|
107
|
+
break;
|
|
108
|
+
acc += h;
|
|
109
|
+
start++;
|
|
110
|
+
}
|
|
111
|
+
// Walk forward from start to find end
|
|
112
|
+
let end = start;
|
|
113
|
+
let viewAcc = 0;
|
|
114
|
+
while (end < totalItems && viewAcc < viewportHeight) {
|
|
115
|
+
viewAcc += getValidHeight(heightCache?.[end], itemHeight);
|
|
116
|
+
end++;
|
|
117
|
+
}
|
|
118
|
+
end = Math.min(totalItems, end + 1); // +1 to ensure partial items are visible
|
|
96
119
|
// Safeguard for topToBottom: ensure last item is fully visible when at max scroll
|
|
97
120
|
const totalHeight = totalContentHeight ?? totalItems * itemHeight;
|
|
98
121
|
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
99
122
|
// Use strict tolerance to avoid premature bottom anchoring that leaves a visible gap
|
|
100
|
-
const tolerance = Math.max(1, Math.floor(itemHeight *
|
|
123
|
+
const tolerance = Math.max(1, Math.floor(itemHeight * BOTTOM_TOLERANCE_FACTOR));
|
|
101
124
|
const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
|
|
102
125
|
if (isAtBottom) {
|
|
103
126
|
// Pack from the end using measured heights when available: walk backward until viewport filled
|
|
104
127
|
const adjustedEnd = totalItems;
|
|
105
128
|
let startCore = adjustedEnd;
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const h = getH(startCore - 1);
|
|
110
|
-
acc += h;
|
|
129
|
+
let backAcc = 0;
|
|
130
|
+
while (startCore > 0 && backAcc < viewportHeight) {
|
|
131
|
+
backAcc += getValidHeight(heightCache?.[startCore - 1], itemHeight);
|
|
111
132
|
startCore -= 1;
|
|
112
133
|
}
|
|
113
134
|
return {
|
|
@@ -335,49 +356,6 @@ export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
|
|
|
335
356
|
heightChanges
|
|
336
357
|
};
|
|
337
358
|
};
|
|
338
|
-
/**
|
|
339
|
-
* Processes large arrays in chunks to prevent UI blocking.
|
|
340
|
-
*
|
|
341
|
-
* This function implements a progressive processing strategy that:
|
|
342
|
-
* 1. Breaks down large arrays into manageable chunks
|
|
343
|
-
* 2. Processes each chunk asynchronously
|
|
344
|
-
* 3. Reports progress after each chunk
|
|
345
|
-
* 4. Yields to the main thread between chunks
|
|
346
|
-
*
|
|
347
|
-
* @param {any[]} items - Array of items to process
|
|
348
|
-
* @param {number} chunkSize - Number of items to process in each chunk
|
|
349
|
-
* @param {(processed: number) => void} onProgress - Callback for progress updates
|
|
350
|
-
* @param {() => void} onComplete - Callback when all processing is complete
|
|
351
|
-
*
|
|
352
|
-
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
353
|
-
*
|
|
354
|
-
* @example
|
|
355
|
-
* await processChunked(
|
|
356
|
-
* largeArray,
|
|
357
|
-
* 50,
|
|
358
|
-
* (processed) => console.log(`Processed ${processed} items`),
|
|
359
|
-
* () => console.log('All items processed')
|
|
360
|
-
* )
|
|
361
|
-
*/
|
|
362
|
-
export const processChunked = async (items, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
363
|
-
chunkSize, onProgress, // eslint-disable-line no-unused-vars
|
|
364
|
-
onComplete) => {
|
|
365
|
-
if (!items.length) {
|
|
366
|
-
onComplete();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const processChunk = async (startIdx) => {
|
|
370
|
-
const endIdx = Math.min(startIdx + chunkSize, items.length);
|
|
371
|
-
onProgress(endIdx);
|
|
372
|
-
if (endIdx < items.length) {
|
|
373
|
-
setTimeout(() => processChunk(endIdx), 0);
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
onComplete();
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
await processChunk(0);
|
|
380
|
-
};
|
|
381
359
|
/**
|
|
382
360
|
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
383
361
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -59,45 +59,45 @@
|
|
|
59
59
|
"esm-env": "^1.2.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@eslint/compat": "^2.0.
|
|
62
|
+
"@eslint/compat": "^2.0.3",
|
|
63
63
|
"@eslint/js": "^10.0.1",
|
|
64
|
-
"@faker-js/faker": "^10.
|
|
64
|
+
"@faker-js/faker": "^10.4.0",
|
|
65
65
|
"@playwright/cli": "^0.1.1",
|
|
66
66
|
"@playwright/test": "^1.58.2",
|
|
67
67
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
68
|
-
"@sveltejs/kit": "^2.
|
|
68
|
+
"@sveltejs/kit": "^2.55.0",
|
|
69
69
|
"@sveltejs/package": "^2.5.7",
|
|
70
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
71
|
-
"@tailwindcss/vite": "^4.2.
|
|
70
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
71
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
72
72
|
"@testing-library/jest-dom": "^6.9.1",
|
|
73
73
|
"@testing-library/svelte": "^5.3.1",
|
|
74
74
|
"@testing-library/user-event": "^14.6.1",
|
|
75
|
-
"@types/node": "^25.
|
|
76
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
77
|
-
"@typescript-eslint/parser": "^8.
|
|
78
|
-
"@vitest/coverage-v8": "^4.
|
|
79
|
-
"eslint": "^10.0
|
|
75
|
+
"@types/node": "^25.5.0",
|
|
76
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
77
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
78
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
79
|
+
"eslint": "^10.1.0",
|
|
80
80
|
"eslint-config-prettier": "^10.1.8",
|
|
81
81
|
"eslint-plugin-import": "^2.32.0",
|
|
82
|
-
"eslint-plugin-svelte": "^3.
|
|
82
|
+
"eslint-plugin-svelte": "^3.16.0",
|
|
83
83
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
84
84
|
"globals": "^17.4.0",
|
|
85
85
|
"husky": "^9.1.7",
|
|
86
|
-
"jsdom": "^
|
|
87
|
-
"mprocs": "^0.
|
|
86
|
+
"jsdom": "^29.0.1",
|
|
87
|
+
"mprocs": "^0.9.2",
|
|
88
88
|
"prettier": "^3.8.1",
|
|
89
89
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
90
90
|
"prettier-plugin-svelte": "^3.5.1",
|
|
91
91
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
92
92
|
"publint": "^0.3.18",
|
|
93
|
-
"svelte": "^5.
|
|
94
|
-
"svelte-check": "^4.4.
|
|
95
|
-
"tailwindcss": "^4.2.
|
|
93
|
+
"svelte": "^5.55.0",
|
|
94
|
+
"svelte-check": "^4.4.5",
|
|
95
|
+
"tailwindcss": "^4.2.2",
|
|
96
96
|
"tw-animate-css": "^1.4.0",
|
|
97
97
|
"typescript": "^5.9.3",
|
|
98
|
-
"typescript-eslint": "^8.
|
|
99
|
-
"vite": "^
|
|
100
|
-
"vitest": "^4.
|
|
98
|
+
"typescript-eslint": "^8.57.2",
|
|
99
|
+
"vite": "^8.0.3",
|
|
100
|
+
"vitest": "^4.1.2"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
103
|
"svelte": "^5.0.0"
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
],
|
|
121
121
|
"scripts": {
|
|
122
122
|
"build": "vite build && pnpm run package",
|
|
123
|
+
"cf-typegen": "pnpm --filter docs cf-typegen",
|
|
123
124
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
124
125
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
125
126
|
"dev": "vite dev",
|