@humanspeak/svelte-virtual-list 0.3.6 → 0.3.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 +51 -0
- package/dist/SvelteVirtualList.svelte +307 -118
- package/dist/SvelteVirtualList.svelte.d.ts +40 -0
- package/dist/reactive-list-manager/RecomputeScheduler.d.ts +78 -0
- package/dist/reactive-list-manager/RecomputeScheduler.js +78 -9
- package/dist/types.d.ts +15 -0
- package/dist/utils/heightChangeDetection.d.ts +32 -7
- package/dist/utils/heightChangeDetection.js +32 -7
- package/dist/utils/scrollCalculation.d.ts +35 -0
- package/dist/utils/scrollCalculation.js +99 -71
- package/dist/utils/virtualList.d.ts +64 -0
- package/dist/utils/virtualList.js +83 -12
- package/package.json +30 -30
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
|
|
2
2
|
import type { VirtualListSetters, VirtualListState } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validates a height value and returns it if valid, otherwise returns the fallback.
|
|
5
|
+
*
|
|
6
|
+
* A height is considered valid if it is a finite number greater than 0.
|
|
7
|
+
* This utility consolidates the repeated pattern of height validation
|
|
8
|
+
* found throughout the virtual list codebase.
|
|
9
|
+
*
|
|
10
|
+
* @param {unknown} height - The height value to validate
|
|
11
|
+
* @param {number} fallback - The fallback value to use if height is invalid
|
|
12
|
+
* @returns {number} The validated height or the fallback value
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const height = getValidHeight(heightCache[i], calculatedItemHeight)
|
|
17
|
+
* // Returns heightCache[i] if valid, otherwise calculatedItemHeight
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const getValidHeight: (height: unknown, fallback: number) => number;
|
|
21
|
+
/**
|
|
22
|
+
* Clamps a numeric value to be within a specified range.
|
|
23
|
+
*
|
|
24
|
+
* This utility consolidates the repeated `Math.max(min, Math.min(max, value))`
|
|
25
|
+
* pattern used throughout scroll calculations and positioning logic.
|
|
26
|
+
*
|
|
27
|
+
* @param {number} value - The value to clamp
|
|
28
|
+
* @param {number} min - The minimum allowed value
|
|
29
|
+
* @param {number} max - The maximum allowed value
|
|
30
|
+
* @returns {number} The clamped value
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const scrollTop = clampValue(targetScrollTop, 0, maxScrollTop)
|
|
35
|
+
* // Ensures scrollTop is between 0 and maxScrollTop
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const clampValue: (value: number, min: number, max: number) => number;
|
|
3
39
|
/**
|
|
4
40
|
* Calculates the maximum scroll position for a virtual list.
|
|
5
41
|
*
|
|
@@ -151,3 +187,31 @@ onComplete: () => void) => Promise<void>;
|
|
|
151
187
|
* const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
|
|
152
188
|
*/
|
|
153
189
|
export declare const getScrollOffsetForIndex: (heightCache: Record<number, number>, calculatedItemHeight: number, idx: number, blockSums?: number[], blockSize?: number) => number;
|
|
190
|
+
/**
|
|
191
|
+
* Builds block prefix sums for heightCache to accelerate offset queries.
|
|
192
|
+
*
|
|
193
|
+
* This function precomputes cumulative height sums for blocks of items, enabling
|
|
194
|
+
* O(blockSize) offset calculations instead of O(n). The returned array contains
|
|
195
|
+
* the total height of all items up to and including each completed block.
|
|
196
|
+
*
|
|
197
|
+
* For example, with blockSize=1000:
|
|
198
|
+
* - Entry 0: sum of heights for items 0-999
|
|
199
|
+
* - Entry 1: sum of heights for items 0-1999
|
|
200
|
+
* - Entry 2: sum of heights for items 0-2999
|
|
201
|
+
*
|
|
202
|
+
* @param {Record<number, number>} heightCache - Cache of measured item heights.
|
|
203
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items.
|
|
204
|
+
* @param {number} totalItems - Total number of items in the list.
|
|
205
|
+
* @param {number} [blockSize=1000] - Number of items per block for memoization.
|
|
206
|
+
* @returns {number[]} Array of cumulative height sums for each completed block.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const heightCache = { 0: 40, 1: 50, 2: 45 };
|
|
211
|
+
* const blockSums = buildBlockSums(heightCache, 40, 5000, 1000);
|
|
212
|
+
*
|
|
213
|
+
* // Use with getScrollOffsetForIndex for efficient lookups
|
|
214
|
+
* const offset = getScrollOffsetForIndex(heightCache, 40, 2500, blockSums);
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a height value and returns it if valid, otherwise returns the fallback.
|
|
3
|
+
*
|
|
4
|
+
* A height is considered valid if it is a finite number greater than 0.
|
|
5
|
+
* This utility consolidates the repeated pattern of height validation
|
|
6
|
+
* found throughout the virtual list codebase.
|
|
7
|
+
*
|
|
8
|
+
* @param {unknown} height - The height value to validate
|
|
9
|
+
* @param {number} fallback - The fallback value to use if height is invalid
|
|
10
|
+
* @returns {number} The validated height or the fallback value
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const height = getValidHeight(heightCache[i], calculatedItemHeight)
|
|
15
|
+
* // Returns heightCache[i] if valid, otherwise calculatedItemHeight
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const getValidHeight = (height, fallback) => Number.isFinite(height) && height > 0 ? height : fallback;
|
|
19
|
+
/**
|
|
20
|
+
* Clamps a numeric value to be within a specified range.
|
|
21
|
+
*
|
|
22
|
+
* This utility consolidates the repeated `Math.max(min, Math.min(max, value))`
|
|
23
|
+
* pattern used throughout scroll calculations and positioning logic.
|
|
24
|
+
*
|
|
25
|
+
* @param {number} value - The value to clamp
|
|
26
|
+
* @param {number} min - The minimum allowed value
|
|
27
|
+
* @param {number} max - The maximum allowed value
|
|
28
|
+
* @returns {number} The clamped value
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const scrollTop = clampValue(targetScrollTop, 0, maxScrollTop)
|
|
33
|
+
* // Ensures scrollTop is between 0 and maxScrollTop
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const clampValue = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
1
37
|
/**
|
|
2
38
|
* Calculates the maximum scroll position for a virtual list.
|
|
3
39
|
*
|
|
@@ -68,10 +104,7 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
68
104
|
const adjustedEnd = totalItems;
|
|
69
105
|
let startCore = adjustedEnd;
|
|
70
106
|
let acc = 0;
|
|
71
|
-
const getH = (i) =>
|
|
72
|
-
const v = heightCache ? heightCache[i] : undefined;
|
|
73
|
-
return Number.isFinite(v) && v > 0 ? v : itemHeight;
|
|
74
|
-
};
|
|
107
|
+
const getH = (i) => getValidHeight(heightCache ? heightCache[i] : undefined, itemHeight);
|
|
75
108
|
while (startCore > 0 && acc < viewportHeight) {
|
|
76
109
|
const h = getH(startCore - 1);
|
|
77
110
|
acc += h;
|
|
@@ -115,7 +148,8 @@ export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart,
|
|
|
115
148
|
const basicTransform = (totalItems - visibleEnd) * itemHeight;
|
|
116
149
|
// When content is smaller than viewport, push to bottom
|
|
117
150
|
const bottomOffset = Math.max(0, effectiveViewport - actualTotalHeight);
|
|
118
|
-
|
|
151
|
+
// Snap to integer pixels to avoid subpixel oscillation
|
|
152
|
+
return Math.round(basicTransform + bottomOffset);
|
|
119
153
|
}
|
|
120
154
|
else {
|
|
121
155
|
// For topToBottom, prefer precise offset using measured heights when available
|
|
@@ -123,7 +157,7 @@ export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart,
|
|
|
123
157
|
const offset = getScrollOffsetForIndex(heightCache, itemHeight, visibleStart);
|
|
124
158
|
return Math.max(0, Math.round(offset));
|
|
125
159
|
}
|
|
126
|
-
return visibleStart * itemHeight;
|
|
160
|
+
return Math.round(visibleStart * itemHeight);
|
|
127
161
|
}
|
|
128
162
|
};
|
|
129
163
|
/**
|
|
@@ -365,9 +399,7 @@ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx,
|
|
|
365
399
|
// Fallback: O(n) for a single query
|
|
366
400
|
let offset = 0;
|
|
367
401
|
for (let i = 0; i < safeIdx; i++) {
|
|
368
|
-
|
|
369
|
-
const height = Number.isFinite(raw) && raw > 0 ? raw : calculatedItemHeight;
|
|
370
|
-
offset += height;
|
|
402
|
+
offset += getValidHeight(heightCache[i], calculatedItemHeight);
|
|
371
403
|
}
|
|
372
404
|
return offset;
|
|
373
405
|
}
|
|
@@ -380,9 +412,48 @@ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx,
|
|
|
380
412
|
let offset = offsetBase;
|
|
381
413
|
const start = blockIdx * blockSize;
|
|
382
414
|
for (let i = start; i < safeIdx; i++) {
|
|
383
|
-
|
|
384
|
-
const height = Number.isFinite(raw) && raw > 0 ? raw : calculatedItemHeight;
|
|
385
|
-
offset += height;
|
|
415
|
+
offset += getValidHeight(heightCache[i], calculatedItemHeight);
|
|
386
416
|
}
|
|
387
417
|
return offset;
|
|
388
418
|
};
|
|
419
|
+
/**
|
|
420
|
+
* Builds block prefix sums for heightCache to accelerate offset queries.
|
|
421
|
+
*
|
|
422
|
+
* This function precomputes cumulative height sums for blocks of items, enabling
|
|
423
|
+
* O(blockSize) offset calculations instead of O(n). The returned array contains
|
|
424
|
+
* the total height of all items up to and including each completed block.
|
|
425
|
+
*
|
|
426
|
+
* For example, with blockSize=1000:
|
|
427
|
+
* - Entry 0: sum of heights for items 0-999
|
|
428
|
+
* - Entry 1: sum of heights for items 0-1999
|
|
429
|
+
* - Entry 2: sum of heights for items 0-2999
|
|
430
|
+
*
|
|
431
|
+
* @param {Record<number, number>} heightCache - Cache of measured item heights.
|
|
432
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items.
|
|
433
|
+
* @param {number} totalItems - Total number of items in the list.
|
|
434
|
+
* @param {number} [blockSize=1000] - Number of items per block for memoization.
|
|
435
|
+
* @returns {number[]} Array of cumulative height sums for each completed block.
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* const heightCache = { 0: 40, 1: 50, 2: 45 };
|
|
440
|
+
* const blockSums = buildBlockSums(heightCache, 40, 5000, 1000);
|
|
441
|
+
*
|
|
442
|
+
* // Use with getScrollOffsetForIndex for efficient lookups
|
|
443
|
+
* const offset = getScrollOffsetForIndex(heightCache, 40, 2500, blockSums);
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, blockSize = 1000) => {
|
|
447
|
+
const blocks = Math.ceil(totalItems / blockSize);
|
|
448
|
+
const sums = new Array(Math.max(0, blocks - 1));
|
|
449
|
+
let running = 0;
|
|
450
|
+
for (let b = 0; b < blocks - 1; b++) {
|
|
451
|
+
const start = b * blockSize;
|
|
452
|
+
const end = start + blockSize;
|
|
453
|
+
for (let i = start; i < end; i++) {
|
|
454
|
+
running += getValidHeight(heightCache[i], calculatedItemHeight);
|
|
455
|
+
}
|
|
456
|
+
sums[b] = running;
|
|
457
|
+
}
|
|
458
|
+
return sums;
|
|
459
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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,51 +59,51 @@
|
|
|
59
59
|
"esm-env": "^1.2.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@eslint/compat": "^
|
|
63
|
-
"@eslint/js": "^9.39.
|
|
64
|
-
"@faker-js/faker": "^10.
|
|
65
|
-
"@playwright/test": "^1.
|
|
62
|
+
"@eslint/compat": "^2.0.1",
|
|
63
|
+
"@eslint/js": "^9.39.2",
|
|
64
|
+
"@faker-js/faker": "^10.2.0",
|
|
65
|
+
"@playwright/test": "^1.58.0",
|
|
66
66
|
"@sveltejs/adapter-auto": "^7.0.0",
|
|
67
|
-
"@sveltejs/kit": "^2.
|
|
68
|
-
"@sveltejs/package": "^2.5.
|
|
69
|
-
"@sveltejs/vite-plugin-svelte": "^6.2.
|
|
70
|
-
"@tailwindcss/vite": "^4.1.
|
|
67
|
+
"@sveltejs/kit": "^2.50.1",
|
|
68
|
+
"@sveltejs/package": "^2.5.7",
|
|
69
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
70
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
71
71
|
"@testing-library/jest-dom": "^6.9.1",
|
|
72
|
-
"@testing-library/svelte": "^5.
|
|
72
|
+
"@testing-library/svelte": "^5.3.1",
|
|
73
73
|
"@testing-library/user-event": "^14.6.1",
|
|
74
|
-
"@types/node": "^
|
|
75
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
76
|
-
"@typescript-eslint/parser": "^8.
|
|
77
|
-
"@vitest/coverage-v8": "^4.0.
|
|
74
|
+
"@types/node": "^25.1.0",
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
76
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
77
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
78
78
|
"concurrently": "^9.2.1",
|
|
79
|
-
"eslint": "^9.39.
|
|
79
|
+
"eslint": "^9.39.2",
|
|
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.14.0",
|
|
83
83
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
84
|
-
"globals": "^
|
|
84
|
+
"globals": "^17.2.0",
|
|
85
85
|
"husky": "^9.1.7",
|
|
86
|
-
"jsdom": "^27.
|
|
87
|
-
"prettier": "^3.
|
|
86
|
+
"jsdom": "^27.4.0",
|
|
87
|
+
"prettier": "^3.8.1",
|
|
88
88
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
89
|
-
"prettier-plugin-sort-json": "^4.
|
|
90
|
-
"prettier-plugin-svelte": "^3.4.
|
|
91
|
-
"prettier-plugin-tailwindcss": "^0.7.
|
|
92
|
-
"publint": "^0.3.
|
|
93
|
-
"svelte": "^5.
|
|
94
|
-
"svelte-check": "^4.3.
|
|
95
|
-
"tailwindcss": "^4.1.
|
|
89
|
+
"prettier-plugin-sort-json": "^4.2.0",
|
|
90
|
+
"prettier-plugin-svelte": "^3.4.1",
|
|
91
|
+
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
92
|
+
"publint": "^0.3.17",
|
|
93
|
+
"svelte": "^5.48.5",
|
|
94
|
+
"svelte-check": "^4.3.5",
|
|
95
|
+
"tailwindcss": "^4.1.18",
|
|
96
96
|
"tw-animate-css": "^1.4.0",
|
|
97
97
|
"typescript": "^5.9.3",
|
|
98
|
-
"typescript-eslint": "^8.
|
|
99
|
-
"vite": "^7.
|
|
100
|
-
"vitest": "^4.0.
|
|
98
|
+
"typescript-eslint": "^8.54.0",
|
|
99
|
+
"vite": "^7.3.1",
|
|
100
|
+
"vitest": "^4.0.18"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
103
|
"svelte": "^5.0.0"
|
|
104
104
|
},
|
|
105
105
|
"volta": {
|
|
106
|
-
"node": "
|
|
106
|
+
"node": "24.13.0"
|
|
107
107
|
},
|
|
108
108
|
"publishConfig": {
|
|
109
109
|
"access": "public"
|