@humanspeak/svelte-virtual-list 0.2.4 → 0.2.6
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 +36 -0
- package/dist/SvelteVirtualList.svelte +257 -116
- package/dist/SvelteVirtualList.svelte.d.ts +186 -32
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +58 -16
- package/dist/types.js +8 -1
- package/dist/utils/heightCalculation.d.ts +77 -0
- package/dist/utils/heightCalculation.js +90 -0
- package/dist/utils/raf.d.ts +29 -5
- package/dist/utils/raf.js +45 -19
- package/dist/utils/types.d.ts +6 -0
- package/dist/utils/virtualList.d.ts +37 -4
- package/dist/utils/virtualList.js +81 -9
- package/package.json +41 -33
|
@@ -71,7 +71,6 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
71
71
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
72
72
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
73
73
|
* @param {Record<number, number>} heightCache - Cache of previously measured item heights
|
|
74
|
-
* @param {number} lastMeasuredIndex - Index of the last measured item
|
|
75
74
|
* @param {number} currentItemHeight - Current average item height being used
|
|
76
75
|
*
|
|
77
76
|
* @returns {{
|
|
@@ -85,13 +84,12 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
85
84
|
* itemElements,
|
|
86
85
|
* { start: 0 },
|
|
87
86
|
* {},
|
|
88
|
-
* -1,
|
|
89
87
|
* 40
|
|
90
88
|
* )
|
|
91
89
|
*/
|
|
92
90
|
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
93
91
|
start: number;
|
|
94
|
-
}, heightCache: Record<number, number>,
|
|
92
|
+
}, heightCache: Record<number, number>, currentItemHeight: number) => {
|
|
95
93
|
newHeight: number;
|
|
96
94
|
newLastMeasuredIndex: number;
|
|
97
95
|
updatedHeightCache: Record<number, number>;
|
|
@@ -120,4 +118,39 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
|
|
|
120
118
|
* () => console.log('All items processed')
|
|
121
119
|
* )
|
|
122
120
|
*/
|
|
123
|
-
export declare const processChunked: (items: any[],
|
|
121
|
+
export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
122
|
+
chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
|
|
123
|
+
onComplete: () => void) => Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
126
|
+
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
127
|
+
*
|
|
128
|
+
* @param {Record<number, number>} heightCache - Map of measured item heights
|
|
129
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
130
|
+
* @param {number} totalItems - Total number of items in the list
|
|
131
|
+
* @param {number} blockSize - Number of items per block
|
|
132
|
+
* @returns {number[]} Array of prefix sums at each block boundary
|
|
133
|
+
*/
|
|
134
|
+
export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
|
|
135
|
+
/**
|
|
136
|
+
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
137
|
+
*
|
|
138
|
+
* Uses block memoization for efficient O(b) offset calculation, where b = block size (default 1000).
|
|
139
|
+
* For very large lists, this avoids O(n) iteration for every scroll.
|
|
140
|
+
*
|
|
141
|
+
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
142
|
+
* - For small indices, falls back to the original logic.
|
|
143
|
+
*
|
|
144
|
+
* @param {Record<number, number>} heightCache - Map of measured item heights
|
|
145
|
+
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
146
|
+
* @param {number} idx - The index to scroll to (exclusive)
|
|
147
|
+
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
|
148
|
+
* @param {number} [blockSize=1000] - Block size for memoization
|
|
149
|
+
* @returns {number} The total offset in pixels from the top of the list to the start of the item at idx.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* // For best performance with repeated queries:
|
|
153
|
+
* const blockSums = buildBlockSums(heightCache, calculatedItemHeight, items.length);
|
|
154
|
+
* const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
|
|
155
|
+
*/
|
|
156
|
+
export declare const getScrollOffsetForIndex: (heightCache: Record<number, number>, calculatedItemHeight: number, idx: number, blockSums?: number[], blockSize?: number) => number;
|
|
@@ -108,7 +108,6 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
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
110
|
* @param {Record<number, number>} heightCache - Cache of previously measured item heights
|
|
111
|
-
* @param {number} lastMeasuredIndex - Index of the last measured item
|
|
112
111
|
* @param {number} currentItemHeight - Current average item height being used
|
|
113
112
|
*
|
|
114
113
|
* @returns {{
|
|
@@ -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 {Record<number, number>} heightCache - Map of measured item heights
|
|
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 {Record<number, number>} heightCache - Map of measured item heights
|
|
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",
|
|
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,59 @@
|
|
|
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
75
|
"esm-env": "^1.2.2"
|
|
70
76
|
},
|
|
71
77
|
"devDependencies": {
|
|
72
|
-
"@eslint/
|
|
73
|
-
"@
|
|
74
|
-
"@
|
|
75
|
-
"@
|
|
76
|
-
"@sveltejs/
|
|
77
|
-
"@sveltejs/
|
|
78
|
+
"@eslint/compat": "^1.3.1",
|
|
79
|
+
"@eslint/js": "^9.29.0",
|
|
80
|
+
"@faker-js/faker": "^9.8.0",
|
|
81
|
+
"@playwright/test": "^1.53.1",
|
|
82
|
+
"@sveltejs/adapter-auto": "^6.0.1",
|
|
83
|
+
"@sveltejs/kit": "^2.22.2",
|
|
84
|
+
"@sveltejs/package": "^2.3.12",
|
|
85
|
+
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
|
78
86
|
"@testing-library/jest-dom": "^6.6.3",
|
|
79
|
-
"@testing-library/svelte": "^5.2.
|
|
80
|
-
"@testing-library/user-event": "^14.6.
|
|
81
|
-
"@types/node": "^
|
|
82
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
83
|
-
"@typescript-eslint/parser": "^8.
|
|
84
|
-
"@vitest/coverage-v8": "^3.
|
|
85
|
-
"eslint": "^9.
|
|
86
|
-
"eslint-config-prettier": "^10.
|
|
87
|
-
"eslint-plugin-
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
87
|
+
"@testing-library/svelte": "^5.2.8",
|
|
88
|
+
"@testing-library/user-event": "^14.6.1",
|
|
89
|
+
"@types/node": "^24.0.4",
|
|
90
|
+
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
|
91
|
+
"@typescript-eslint/parser": "^8.35.0",
|
|
92
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
93
|
+
"eslint": "^9.29.0",
|
|
94
|
+
"eslint-config-prettier": "^10.1.5",
|
|
95
|
+
"eslint-plugin-import": "^2.32.0",
|
|
96
|
+
"eslint-plugin-svelte": "^3.10.0",
|
|
97
|
+
"eslint-plugin-unused-imports": "^4.1.4",
|
|
98
|
+
"globals": "^16.2.0",
|
|
99
|
+
"jsdom": "^26.1.0",
|
|
100
|
+
"prettier": "^3.6.1",
|
|
91
101
|
"prettier-plugin-organize-imports": "^4.1.0",
|
|
92
|
-
"prettier-plugin-
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
102
|
+
"prettier-plugin-sort-json": "^4.1.1",
|
|
103
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
104
|
+
"prettier-plugin-tailwindcss": "^0.6.13",
|
|
105
|
+
"publint": "^0.3.12",
|
|
106
|
+
"svelte": "^5.34.8",
|
|
107
|
+
"svelte-check": "^4.2.2",
|
|
108
|
+
"typescript": "^5.8.3",
|
|
109
|
+
"typescript-eslint": "^8.35.0",
|
|
110
|
+
"vite": "^6.3.5",
|
|
111
|
+
"vitest": "^3.2.4"
|
|
99
112
|
},
|
|
100
113
|
"peerDependencies": {
|
|
101
114
|
"svelte": "^5.0.0"
|
|
102
115
|
},
|
|
103
116
|
"volta": {
|
|
104
|
-
"node": "22.
|
|
117
|
+
"node": "22.16.0"
|
|
105
118
|
},
|
|
106
119
|
"publishConfig": {
|
|
107
120
|
"access": "public"
|
|
108
121
|
},
|
|
109
|
-
"overrides": {
|
|
110
|
-
"@sveltejs/kit": {
|
|
111
|
-
"cookie": "^0.7.0"
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
122
|
"tags": [
|
|
115
123
|
"svelte",
|
|
116
124
|
"virtual-list",
|