@humanspeak/svelte-virtual-list 0.2.4 → 0.2.6-beta.0
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/LICENSE +1 -1
- package/README.md +48 -0
- package/dist/SvelteVirtualList.svelte +420 -121
- package/dist/SvelteVirtualList.svelte.d.ts +186 -32
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +70 -16
- package/dist/types.js +8 -1
- package/dist/utils/heightCalculation.d.ts +77 -0
- package/dist/utils/heightCalculation.js +91 -0
- package/dist/utils/raf.d.ts +29 -5
- package/dist/utils/raf.js +45 -19
- package/dist/utils/virtualList.d.ts +43 -13
- package/dist/utils/virtualList.js +85 -13
- package/package.json +45 -36
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SvelteVirtualListMode } from '../types.js';
|
|
1
|
+
import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
|
|
2
2
|
import type { VirtualListSetters, VirtualListState } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Calculates the maximum scroll position for a virtual list.
|
|
@@ -26,12 +26,9 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
|
|
|
26
26
|
* @param {number} totalItems - Total number of items in the list
|
|
27
27
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
28
28
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
29
|
-
* @returns {
|
|
29
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
30
30
|
*/
|
|
31
|
-
export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) =>
|
|
32
|
-
start: number;
|
|
33
|
-
end: number;
|
|
34
|
-
};
|
|
31
|
+
export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => SvelteVirtualListPreviousVisibleRange;
|
|
35
32
|
/**
|
|
36
33
|
* Calculates the CSS transform value for positioning the virtual list items.
|
|
37
34
|
*
|
|
@@ -64,20 +61,19 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
64
61
|
* Calculates the average height of visible items in a virtual list.
|
|
65
62
|
*
|
|
66
63
|
* This function optimizes performance by:
|
|
67
|
-
* 1. Using a height cache to store measured item heights
|
|
64
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
68
65
|
* 2. Only measuring new items not in the cache
|
|
69
66
|
* 3. Calculating a running average of all measured heights
|
|
70
67
|
*
|
|
71
68
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
72
69
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
73
|
-
* @param {
|
|
74
|
-
* @param {number} lastMeasuredIndex - Index of the last measured item
|
|
70
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
75
71
|
* @param {number} currentItemHeight - Current average item height being used
|
|
76
72
|
*
|
|
77
73
|
* @returns {{
|
|
78
74
|
* newHeight: number,
|
|
79
75
|
* newLastMeasuredIndex: number,
|
|
80
|
-
* updatedHeightCache:
|
|
76
|
+
* updatedHeightCache: HeightCache
|
|
81
77
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
82
78
|
*
|
|
83
79
|
* @example
|
|
@@ -85,13 +81,12 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
85
81
|
* itemElements,
|
|
86
82
|
* { start: 0 },
|
|
87
83
|
* {},
|
|
88
|
-
* -1,
|
|
89
84
|
* 40
|
|
90
85
|
* )
|
|
91
86
|
*/
|
|
92
87
|
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
93
88
|
start: number;
|
|
94
|
-
}, heightCache: Record<number, number>,
|
|
89
|
+
}, heightCache: Record<number, number>, currentItemHeight: number) => {
|
|
95
90
|
newHeight: number;
|
|
96
91
|
newLastMeasuredIndex: number;
|
|
97
92
|
updatedHeightCache: Record<number, number>;
|
|
@@ -120,4 +115,39 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
|
|
|
120
115
|
* () => console.log('All items processed')
|
|
121
116
|
* )
|
|
122
117
|
*/
|
|
123
|
-
export declare const processChunked: (items: any[],
|
|
118
|
+
export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
119
|
+
chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
|
|
120
|
+
onComplete: () => void) => Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
123
|
+
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
124
|
+
*
|
|
125
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
126
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
127
|
+
* @param {number} totalItems - Total number of items in the list
|
|
128
|
+
* @param {number} blockSize - Number of items per block
|
|
129
|
+
* @returns {number[]} Array of prefix sums at each block boundary
|
|
130
|
+
*/
|
|
131
|
+
export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
|
|
132
|
+
/**
|
|
133
|
+
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
134
|
+
*
|
|
135
|
+
* Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
|
|
136
|
+
* For very large lists, this avoids O(n) iteration for every scroll.
|
|
137
|
+
*
|
|
138
|
+
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
139
|
+
* - For small indices, falls back to the original logic.
|
|
140
|
+
*
|
|
141
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
142
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
143
|
+
* @param {number} idx - The index to scroll to (exclusive)
|
|
144
|
+
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
|
145
|
+
* @param {number} [blockSize=1000] - Block size for memoization
|
|
146
|
+
* @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // For best performance with repeated queries:
|
|
150
|
+
* const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
|
|
151
|
+
* const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
|
|
152
|
+
*/
|
|
153
|
+
export declare const getScrollOffsetForIndex: (heightCache: Record<number, number>, calculatedItemHeight: number, idx: number, blockSums?: number[], blockSize?: number) => number;
|
|
@@ -29,7 +29,7 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
|
|
|
29
29
|
* @param {number} totalItems - Total number of items in the list
|
|
30
30
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
31
31
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
32
|
-
* @returns {
|
|
32
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
33
33
|
*/
|
|
34
34
|
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
|
|
35
35
|
if (mode === 'bottomToTop') {
|
|
@@ -101,20 +101,19 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
101
101
|
* Calculates the average height of visible items in a virtual list.
|
|
102
102
|
*
|
|
103
103
|
* This function optimizes performance by:
|
|
104
|
-
* 1. Using a height cache to store measured item heights
|
|
104
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
105
105
|
* 2. Only measuring new items not in the cache
|
|
106
106
|
* 3. Calculating a running average of all measured heights
|
|
107
107
|
*
|
|
108
108
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
109
109
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
110
|
-
* @param {
|
|
111
|
-
* @param {number} lastMeasuredIndex - Index of the last measured item
|
|
110
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
112
111
|
* @param {number} currentItemHeight - Current average item height being used
|
|
113
112
|
*
|
|
114
113
|
* @returns {{
|
|
115
114
|
* newHeight: number,
|
|
116
115
|
* newLastMeasuredIndex: number,
|
|
117
|
-
* updatedHeightCache:
|
|
116
|
+
* updatedHeightCache: HeightCache
|
|
118
117
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
119
118
|
*
|
|
120
119
|
* @example
|
|
@@ -122,16 +121,15 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
122
121
|
* itemElements,
|
|
123
122
|
* { start: 0 },
|
|
124
123
|
* {},
|
|
125
|
-
* -1,
|
|
126
124
|
* 40
|
|
127
125
|
* )
|
|
128
126
|
*/
|
|
129
|
-
export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
|
|
127
|
+
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
|
|
130
128
|
const validElements = itemElements.filter((el) => el);
|
|
131
129
|
if (validElements.length === 0) {
|
|
132
130
|
return {
|
|
133
131
|
newHeight: currentItemHeight,
|
|
134
|
-
newLastMeasuredIndex:
|
|
132
|
+
newLastMeasuredIndex: visibleRange.start,
|
|
135
133
|
updatedHeightCache: heightCache
|
|
136
134
|
};
|
|
137
135
|
}
|
|
@@ -140,14 +138,23 @@ export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
|
|
|
140
138
|
validElements.forEach((el, i) => {
|
|
141
139
|
const itemIndex = visibleRange.start + i;
|
|
142
140
|
if (!newHeightCache[itemIndex]) {
|
|
143
|
-
|
|
141
|
+
try {
|
|
142
|
+
const height = el.getBoundingClientRect().height;
|
|
143
|
+
if (Number.isFinite(height) && height > 0) {
|
|
144
|
+
newHeightCache[itemIndex] = height;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Skip invalid measurements
|
|
149
|
+
}
|
|
144
150
|
}
|
|
145
151
|
});
|
|
146
|
-
// Calculate average from cached heights
|
|
147
|
-
const
|
|
148
|
-
const averageHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length;
|
|
152
|
+
// Calculate average from valid cached heights
|
|
153
|
+
const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
|
|
149
154
|
return {
|
|
150
|
-
newHeight:
|
|
155
|
+
newHeight: validHeights.length > 0
|
|
156
|
+
? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
|
|
157
|
+
: currentItemHeight,
|
|
151
158
|
newLastMeasuredIndex: visibleRange.start,
|
|
152
159
|
updatedHeightCache: newHeightCache
|
|
153
160
|
};
|
|
@@ -195,3 +202,68 @@ onComplete) => {
|
|
|
195
202
|
};
|
|
196
203
|
await processChunk(0);
|
|
197
204
|
};
|
|
205
|
+
/**
|
|
206
|
+
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
207
|
+
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
208
|
+
*
|
|
209
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
210
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
211
|
+
* @param {number} totalItems - Total number of items in the list
|
|
212
|
+
* @param {number} blockSize - Number of items per block
|
|
213
|
+
* @returns {number[]} Array of prefix sums at each block boundary
|
|
214
|
+
*/
|
|
215
|
+
export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, blockSize = 1000) => {
|
|
216
|
+
const blockSums = [];
|
|
217
|
+
let sum = 0;
|
|
218
|
+
for (let i = 0; i < totalItems; i++) {
|
|
219
|
+
sum += heightCache[i] ?? calculatedItemHeight;
|
|
220
|
+
if ((i + 1) % blockSize === 0) {
|
|
221
|
+
blockSums.push(sum);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Push the last partial block if needed
|
|
225
|
+
if (totalItems % blockSize !== 0) {
|
|
226
|
+
blockSums.push(sum);
|
|
227
|
+
}
|
|
228
|
+
return blockSums;
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
232
|
+
*
|
|
233
|
+
* Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
|
|
234
|
+
* For very large lists, this avoids O(n) iteration for every scroll.
|
|
235
|
+
*
|
|
236
|
+
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
237
|
+
* - For small indices, falls back to the original logic.
|
|
238
|
+
*
|
|
239
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
240
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
241
|
+
* @param {number} idx - The index to scroll to (exclusive)
|
|
242
|
+
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
|
243
|
+
* @param {number} [blockSize=1000] - Block size for memoization
|
|
244
|
+
* @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* // For best performance with repeated queries:
|
|
248
|
+
* const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
|
|
249
|
+
* const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
|
|
250
|
+
*/
|
|
251
|
+
export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx, blockSums, blockSize = 1000) => {
|
|
252
|
+
if (idx <= 0)
|
|
253
|
+
return 0;
|
|
254
|
+
if (!blockSums) {
|
|
255
|
+
// Fallback: O(n) for a single query
|
|
256
|
+
let offset = 0;
|
|
257
|
+
for (let i = 0; i < idx; i++) {
|
|
258
|
+
offset += heightCache[i] ?? calculatedItemHeight;
|
|
259
|
+
}
|
|
260
|
+
return offset;
|
|
261
|
+
}
|
|
262
|
+
const blockIdx = Math.floor(idx / blockSize);
|
|
263
|
+
let offset = blockIdx > 0 ? blockSums[blockIdx - 1] : 0;
|
|
264
|
+
const start = blockIdx * blockSize;
|
|
265
|
+
for (let i = start; i < idx; i++) {
|
|
266
|
+
offset += heightCache[i] ?? calculatedItemHeight;
|
|
267
|
+
}
|
|
268
|
+
return offset;
|
|
269
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6-beta.0",
|
|
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",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"files": [
|
|
44
44
|
"dist",
|
|
45
45
|
"!dist/**/*.test.*",
|
|
46
|
-
"!dist/**/*.spec.*"
|
|
46
|
+
"!dist/**/*.spec.*",
|
|
47
|
+
"!dist/test/**/*"
|
|
47
48
|
],
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "vite build && npm run package",
|
|
@@ -65,52 +66,60 @@
|
|
|
65
66
|
"test:only": "vitest run",
|
|
66
67
|
"test:watch": "vitest"
|
|
67
68
|
},
|
|
69
|
+
"overrides": {
|
|
70
|
+
"@sveltejs/kit": {
|
|
71
|
+
"cookie": "^0.7.0"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
68
74
|
"dependencies": {
|
|
69
|
-
"esm-env": "^1.2.2"
|
|
75
|
+
"esm-env": "^1.2.2",
|
|
76
|
+
"runed": "^0.31.1"
|
|
70
77
|
},
|
|
71
78
|
"devDependencies": {
|
|
72
|
-
"@eslint/
|
|
73
|
-
"@
|
|
74
|
-
"@
|
|
75
|
-
"@
|
|
76
|
-
"@sveltejs/
|
|
77
|
-
"@sveltejs/
|
|
78
|
-
"@
|
|
79
|
-
"@
|
|
80
|
-
"@testing-library/
|
|
81
|
-
"@
|
|
82
|
-
"@
|
|
83
|
-
"@
|
|
84
|
-
"@
|
|
85
|
-
"eslint": "^
|
|
86
|
-
"
|
|
87
|
-
"eslint
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
79
|
+
"@eslint/compat": "^1.3.1",
|
|
80
|
+
"@eslint/js": "^9.32.0",
|
|
81
|
+
"@faker-js/faker": "^9.9.0",
|
|
82
|
+
"@playwright/test": "^1.54.1",
|
|
83
|
+
"@sveltejs/adapter-auto": "^6.0.1",
|
|
84
|
+
"@sveltejs/kit": "^2.26.1",
|
|
85
|
+
"@sveltejs/package": "^2.4.0",
|
|
86
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
|
87
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
88
|
+
"@testing-library/svelte": "^5.2.8",
|
|
89
|
+
"@testing-library/user-event": "^14.6.1",
|
|
90
|
+
"@types/node": "^24.1.0",
|
|
91
|
+
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
92
|
+
"@typescript-eslint/parser": "^8.38.0",
|
|
93
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
94
|
+
"eslint": "^9.32.0",
|
|
95
|
+
"eslint-config-prettier": "^10.1.8",
|
|
96
|
+
"eslint-plugin-import": "^2.32.0",
|
|
97
|
+
"eslint-plugin-svelte": "^3.11.0",
|
|
98
|
+
"eslint-plugin-unused-imports": "^4.1.4",
|
|
99
|
+
"globals": "^16.3.0",
|
|
100
|
+
"jsdom": "^26.1.0",
|
|
101
|
+
"prettier": "^3.6.2",
|
|
102
|
+
"prettier-plugin-organize-imports": "^4.2.0",
|
|
103
|
+
"prettier-plugin-sort-json": "^4.1.1",
|
|
104
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
105
|
+
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
106
|
+
"publint": "^0.3.12",
|
|
107
|
+
"svelte": "^5.37.1",
|
|
108
|
+
"svelte-check": "^4.3.0",
|
|
109
|
+
"typescript": "^5.8.3",
|
|
110
|
+
"typescript-eslint": "^8.38.0",
|
|
111
|
+
"vite": "^7.0.6",
|
|
112
|
+
"vitest": "^3.2.4"
|
|
99
113
|
},
|
|
100
114
|
"peerDependencies": {
|
|
101
115
|
"svelte": "^5.0.0"
|
|
102
116
|
},
|
|
103
117
|
"volta": {
|
|
104
|
-
"node": "22.
|
|
118
|
+
"node": "22.16.0"
|
|
105
119
|
},
|
|
106
120
|
"publishConfig": {
|
|
107
121
|
"access": "public"
|
|
108
122
|
},
|
|
109
|
-
"overrides": {
|
|
110
|
-
"@sveltejs/kit": {
|
|
111
|
-
"cookie": "^0.7.0"
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
123
|
"tags": [
|
|
115
124
|
"svelte",
|
|
116
125
|
"virtual-list",
|