@humanspeak/svelte-virtual-list 0.2.6 → 0.3.1-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/README.md +14 -2
- package/dist/SvelteVirtualList.svelte +619 -179
- package/dist/SvelteVirtualList.svelte.d.ts +156 -65
- package/dist/reactive-height-manager/INTEGRATION_EXAMPLE.md +136 -0
- package/dist/reactive-height-manager/README.md +324 -0
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.d.ts +116 -0
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.js +200 -0
- package/dist/reactive-height-manager/benchmark.d.ts +5 -0
- package/dist/reactive-height-manager/benchmark.js +25 -0
- package/dist/reactive-height-manager/index.d.ts +50 -0
- package/dist/reactive-height-manager/index.js +55 -0
- package/dist/reactive-height-manager/test/TestComponent.svelte +78 -0
- package/dist/reactive-height-manager/test/TestComponent.svelte.d.ts +23 -0
- package/dist/reactive-height-manager/types.d.ts +41 -0
- package/dist/reactive-height-manager/types.js +1 -0
- package/dist/types.d.ts +24 -5
- package/dist/utils/heightCalculation.d.ts +18 -8
- package/dist/utils/heightCalculation.js +18 -11
- package/dist/utils/heightChangeDetection.d.ts +12 -0
- package/dist/utils/heightChangeDetection.js +20 -0
- package/dist/utils/resizeObserver.d.ts +89 -0
- package/dist/utils/resizeObserver.js +119 -0
- package/dist/utils/scrollCalculation.d.ts +47 -0
- package/dist/utils/scrollCalculation.js +167 -0
- package/dist/utils/throttle.d.ts +95 -0
- package/dist/utils/throttle.js +155 -0
- package/dist/utils/types.d.ts +0 -6
- package/dist/utils/virtualList.d.ts +20 -23
- package/dist/utils/virtualList.js +153 -61
- package/dist/utils/virtualListDebug.d.ts +12 -7
- package/dist/utils/virtualListDebug.js +19 -9
- package/package.json +33 -31
|
@@ -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, atBottom: boolean, wasAtBottomBeforeHeightChange: boolean, lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null, totalContentHeight?: number) => SvelteVirtualListPreviousVisibleRange;
|
|
35
32
|
/**
|
|
36
33
|
* Calculates the CSS transform value for positioning the virtual list items.
|
|
37
34
|
*
|
|
@@ -44,9 +41,10 @@ export declare const calculateVisibleRange: (scrollTop: number, viewportHeight:
|
|
|
44
41
|
* @param {number} visibleEnd - Index of the last visible item
|
|
45
42
|
* @param {number} visibleStart - Index of the first visible item
|
|
46
43
|
* @param {number} itemHeight - Height of each list item in pixels
|
|
44
|
+
* @param {number} viewportHeight - Height of the viewport in pixels
|
|
47
45
|
* @returns {number} The calculated transform Y value in pixels
|
|
48
46
|
*/
|
|
49
|
-
export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number) => number;
|
|
47
|
+
export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number, viewportHeight: number, totalContentHeight?: number) => number;
|
|
50
48
|
/**
|
|
51
49
|
* Updates the virtual list's height and scroll position when necessary.
|
|
52
50
|
*
|
|
@@ -64,19 +62,19 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
64
62
|
* Calculates the average height of visible items in a virtual list.
|
|
65
63
|
*
|
|
66
64
|
* This function optimizes performance by:
|
|
67
|
-
* 1. Using a height cache to store measured item heights
|
|
65
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
68
66
|
* 2. Only measuring new items not in the cache
|
|
69
67
|
* 3. Calculating a running average of all measured heights
|
|
70
68
|
*
|
|
71
69
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
72
70
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
73
|
-
* @param {
|
|
71
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
74
72
|
* @param {number} currentItemHeight - Current average item height being used
|
|
75
73
|
*
|
|
76
74
|
* @returns {{
|
|
77
75
|
* newHeight: number,
|
|
78
76
|
* newLastMeasuredIndex: number,
|
|
79
|
-
* updatedHeightCache:
|
|
77
|
+
* updatedHeightCache: HeightCache
|
|
80
78
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
81
79
|
*
|
|
82
80
|
* @example
|
|
@@ -89,10 +87,20 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
89
87
|
*/
|
|
90
88
|
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
91
89
|
start: number;
|
|
92
|
-
|
|
90
|
+
end: number;
|
|
91
|
+
}, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>, currentTotalHeight?: number, currentValidCount?: number, mode?: SvelteVirtualListMode) => {
|
|
93
92
|
newHeight: number;
|
|
94
93
|
newLastMeasuredIndex: number;
|
|
95
94
|
updatedHeightCache: Record<number, number>;
|
|
95
|
+
clearedDirtyItems: Set<number>;
|
|
96
|
+
newTotalHeight: number;
|
|
97
|
+
newValidCount: number;
|
|
98
|
+
heightChanges: Array<{
|
|
99
|
+
index: number;
|
|
100
|
+
oldHeight: number;
|
|
101
|
+
newHeight: number;
|
|
102
|
+
delta: number;
|
|
103
|
+
}>;
|
|
96
104
|
};
|
|
97
105
|
/**
|
|
98
106
|
* Processes large arrays in chunks to prevent UI blocking.
|
|
@@ -121,17 +129,6 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
|
|
|
121
129
|
export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
122
130
|
chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
|
|
123
131
|
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
132
|
/**
|
|
136
133
|
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
137
134
|
*
|
|
@@ -141,7 +138,7 @@ export declare const buildBlockSums: (heightCache: Record<number, number>, calcu
|
|
|
141
138
|
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
142
139
|
* - For small indices, falls back to the original logic.
|
|
143
140
|
*
|
|
144
|
-
* @param {
|
|
141
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
145
142
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
146
143
|
* @param {number} idx - The index to scroll to (exclusive)
|
|
147
144
|
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
|
@@ -29,24 +29,56 @@ 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
|
-
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
|
|
34
|
+
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode, atBottom, wasAtBottomBeforeHeightChange, lastVisibleRange, totalContentHeight) => {
|
|
35
35
|
if (mode === 'bottomToTop') {
|
|
36
36
|
const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
|
|
37
|
-
|
|
37
|
+
// In bottomToTop mode, scrollTop represents distance from the total content end
|
|
38
|
+
// scrollTop = 0 means we're at the beginning (showing first items)
|
|
39
|
+
// scrollTop = maxScrollTop means we're at the end (showing last items)
|
|
40
|
+
const totalHeight = totalContentHeight ?? totalItems * itemHeight;
|
|
41
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
42
|
+
// Convert scrollTop to "distance from start" for bottomToTop
|
|
43
|
+
const distanceFromStart = maxScrollTop - scrollTop;
|
|
44
|
+
const startIndex = Math.floor(distanceFromStart / itemHeight);
|
|
45
|
+
// Safeguard: handle edge cases
|
|
46
|
+
if (startIndex < 0) {
|
|
47
|
+
// We're scrolled beyond the maximum (showing first items)
|
|
48
|
+
const start = 0;
|
|
49
|
+
const end = Math.min(totalItems, visibleCount + bufferSize * 2);
|
|
50
|
+
return { start, end };
|
|
51
|
+
}
|
|
38
52
|
// Add buffer to both ends
|
|
39
|
-
const start = Math.max(0,
|
|
40
|
-
const end = Math.min(totalItems,
|
|
53
|
+
const start = Math.max(0, startIndex - bufferSize);
|
|
54
|
+
const end = Math.min(totalItems, startIndex + visibleCount + bufferSize);
|
|
41
55
|
return { start, end };
|
|
42
56
|
}
|
|
43
57
|
else {
|
|
44
58
|
const start = Math.floor(scrollTop / itemHeight);
|
|
45
59
|
const end = Math.min(totalItems, start + Math.ceil(viewportHeight / itemHeight) + 1);
|
|
60
|
+
// Safeguard for topToBottom: ensure last item is fully visible when at max scroll
|
|
61
|
+
const totalHeight = totalContentHeight ?? totalItems * itemHeight;
|
|
62
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
63
|
+
// Add dynamic tolerance based on item height for browser rendering precision
|
|
64
|
+
const tolerance = Math.max(itemHeight, 10); // At least one full item height or 10px minimum
|
|
65
|
+
const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
|
|
66
|
+
if (isAtBottom) {
|
|
67
|
+
// When at the bottom, ensure we include all items up to the end
|
|
68
|
+
const adjustedEnd = totalItems;
|
|
69
|
+
const visibleItemCount = Math.ceil(viewportHeight / itemHeight) + bufferSize + 1;
|
|
70
|
+
const adjustedStart = Math.max(0, adjustedEnd - visibleItemCount);
|
|
71
|
+
return {
|
|
72
|
+
start: adjustedStart,
|
|
73
|
+
end: adjustedEnd
|
|
74
|
+
};
|
|
75
|
+
}
|
|
46
76
|
// Add buffer to both ends
|
|
77
|
+
const finalStart = Math.max(0, start - bufferSize);
|
|
78
|
+
const finalEnd = Math.min(totalItems, end + bufferSize);
|
|
47
79
|
return {
|
|
48
|
-
start:
|
|
49
|
-
end:
|
|
80
|
+
start: finalStart,
|
|
81
|
+
end: finalEnd
|
|
50
82
|
};
|
|
51
83
|
}
|
|
52
84
|
};
|
|
@@ -62,12 +94,22 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
62
94
|
* @param {number} visibleEnd - Index of the last visible item
|
|
63
95
|
* @param {number} visibleStart - Index of the first visible item
|
|
64
96
|
* @param {number} itemHeight - Height of each list item in pixels
|
|
97
|
+
* @param {number} viewportHeight - Height of the viewport in pixels
|
|
65
98
|
* @returns {number} The calculated transform Y value in pixels
|
|
66
99
|
*/
|
|
67
|
-
export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
100
|
+
export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight, viewportHeight, totalContentHeight) => {
|
|
101
|
+
if (mode === 'bottomToTop') {
|
|
102
|
+
// In bottomToTop mode, position items so they stack from bottom up
|
|
103
|
+
const actualTotalHeight = totalContentHeight ?? totalItems * itemHeight;
|
|
104
|
+
// Calculate transform to position visible items correctly
|
|
105
|
+
const basicTransform = (totalItems - visibleEnd) * itemHeight;
|
|
106
|
+
// When content is smaller than viewport, push to bottom
|
|
107
|
+
const bottomOffset = Math.max(0, viewportHeight - actualTotalHeight);
|
|
108
|
+
return basicTransform + bottomOffset;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return visibleStart * itemHeight;
|
|
112
|
+
}
|
|
71
113
|
};
|
|
72
114
|
/**
|
|
73
115
|
* Updates the virtual list's height and scroll position when necessary.
|
|
@@ -101,19 +143,19 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
101
143
|
* Calculates the average height of visible items in a virtual list.
|
|
102
144
|
*
|
|
103
145
|
* This function optimizes performance by:
|
|
104
|
-
* 1. Using a height cache to store measured item heights
|
|
146
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
105
147
|
* 2. Only measuring new items not in the cache
|
|
106
148
|
* 3. Calculating a running average of all measured heights
|
|
107
149
|
*
|
|
108
150
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
109
151
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
110
|
-
* @param {
|
|
152
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
111
153
|
* @param {number} currentItemHeight - Current average item height being used
|
|
112
154
|
*
|
|
113
155
|
* @returns {{
|
|
114
156
|
* newHeight: number,
|
|
115
157
|
* newLastMeasuredIndex: number,
|
|
116
|
-
* updatedHeightCache:
|
|
158
|
+
* updatedHeightCache: HeightCache
|
|
117
159
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
118
160
|
*
|
|
119
161
|
* @example
|
|
@@ -124,39 +166,113 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
124
166
|
* 40
|
|
125
167
|
* )
|
|
126
168
|
*/
|
|
127
|
-
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
|
|
169
|
+
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight, dirtyItems, currentTotalHeight = 0, currentValidCount = 0, mode = 'topToBottom') => {
|
|
128
170
|
const validElements = itemElements.filter((el) => el);
|
|
129
171
|
if (validElements.length === 0) {
|
|
130
172
|
return {
|
|
131
173
|
newHeight: currentItemHeight,
|
|
132
174
|
newLastMeasuredIndex: visibleRange.start,
|
|
133
|
-
updatedHeightCache: heightCache
|
|
175
|
+
updatedHeightCache: heightCache,
|
|
176
|
+
clearedDirtyItems: new Set(),
|
|
177
|
+
newTotalHeight: currentTotalHeight,
|
|
178
|
+
newValidCount: currentValidCount,
|
|
179
|
+
heightChanges: []
|
|
134
180
|
};
|
|
135
181
|
}
|
|
136
182
|
const newHeightCache = { ...heightCache };
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
183
|
+
const clearedDirtyItems = new Set();
|
|
184
|
+
const heightChanges = [];
|
|
185
|
+
// Start with current running totals (O(1) instead of O(n))
|
|
186
|
+
let totalValidHeight = currentTotalHeight;
|
|
187
|
+
let validHeightCount = currentValidCount;
|
|
188
|
+
// Process only dirty items if they exist, otherwise process all visible items
|
|
189
|
+
if (dirtyItems.size > 0) {
|
|
190
|
+
// Process only dirty items
|
|
191
|
+
dirtyItems.forEach((itemIndex) => {
|
|
192
|
+
// Map original item index to position in itemElements array
|
|
193
|
+
let elementIndex;
|
|
194
|
+
if (mode === 'bottomToTop') {
|
|
195
|
+
// In bottomToTop, itemElements is reversed relative to the visible range
|
|
196
|
+
// elementIndex should be based on position within the actual array, not theoretical end
|
|
197
|
+
elementIndex = validElements.length - 1 - (itemIndex - visibleRange.start);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// In topToBottom, itemElements is normal: [item0, item1, ..., item44, item45]
|
|
201
|
+
elementIndex = itemIndex - visibleRange.start;
|
|
202
|
+
}
|
|
203
|
+
const element = validElements[elementIndex];
|
|
204
|
+
if (element && elementIndex >= 0 && elementIndex < validElements.length) {
|
|
205
|
+
try {
|
|
206
|
+
// await tick()
|
|
207
|
+
void element.offsetHeight;
|
|
208
|
+
const height = element.getBoundingClientRect().height;
|
|
209
|
+
const oldHeight = newHeightCache[itemIndex];
|
|
210
|
+
if (Number.isFinite(height) && height > 0) {
|
|
211
|
+
// Only update if height actually changed (use smaller tolerance for precision)
|
|
212
|
+
if (!oldHeight || Math.abs(oldHeight - height) >= 0.1) {
|
|
213
|
+
// Track the height change for scroll correction
|
|
214
|
+
const actualOldHeight = oldHeight || currentItemHeight;
|
|
215
|
+
const delta = height - actualOldHeight;
|
|
216
|
+
heightChanges.push({
|
|
217
|
+
index: itemIndex,
|
|
218
|
+
oldHeight: actualOldHeight,
|
|
219
|
+
newHeight: height,
|
|
220
|
+
delta
|
|
221
|
+
});
|
|
222
|
+
// Update running totals
|
|
223
|
+
if (oldHeight && Number.isFinite(oldHeight) && oldHeight > 0) {
|
|
224
|
+
// Replace old height with new height in running total
|
|
225
|
+
totalValidHeight = totalValidHeight - oldHeight + height;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Add new height to running total
|
|
229
|
+
totalValidHeight += height;
|
|
230
|
+
validHeightCount++;
|
|
231
|
+
}
|
|
232
|
+
newHeightCache[itemIndex] = height;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
clearedDirtyItems.add(itemIndex);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Skip invalid measurements but still clear from dirty
|
|
239
|
+
clearedDirtyItems.add(itemIndex);
|
|
145
240
|
}
|
|
146
241
|
}
|
|
147
|
-
|
|
148
|
-
//
|
|
242
|
+
else {
|
|
243
|
+
clearedDirtyItems.add(itemIndex); // Still clear it from dirty items
|
|
149
244
|
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// Original behavior: process all visible items
|
|
249
|
+
validElements.forEach((el, i) => {
|
|
250
|
+
const itemIndex = visibleRange.start + i;
|
|
251
|
+
if (!newHeightCache[itemIndex]) {
|
|
252
|
+
try {
|
|
253
|
+
const height = el.getBoundingClientRect().height;
|
|
254
|
+
if (Number.isFinite(height) && height > 0) {
|
|
255
|
+
// Add new height to running totals
|
|
256
|
+
totalValidHeight += height;
|
|
257
|
+
validHeightCount++;
|
|
258
|
+
newHeightCache[itemIndex] = height;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Skip invalid measurements
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// O(1) average calculation using running totals!
|
|
154
268
|
return {
|
|
155
|
-
newHeight:
|
|
156
|
-
? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
|
|
157
|
-
: currentItemHeight,
|
|
269
|
+
newHeight: validHeightCount > 0 ? totalValidHeight / validHeightCount : currentItemHeight,
|
|
158
270
|
newLastMeasuredIndex: visibleRange.start,
|
|
159
|
-
updatedHeightCache: newHeightCache
|
|
271
|
+
updatedHeightCache: newHeightCache,
|
|
272
|
+
clearedDirtyItems,
|
|
273
|
+
newTotalHeight: totalValidHeight,
|
|
274
|
+
newValidCount: validHeightCount,
|
|
275
|
+
heightChanges
|
|
160
276
|
};
|
|
161
277
|
};
|
|
162
278
|
/**
|
|
@@ -202,31 +318,6 @@ onComplete) => {
|
|
|
202
318
|
};
|
|
203
319
|
await processChunk(0);
|
|
204
320
|
};
|
|
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
321
|
/**
|
|
231
322
|
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
232
323
|
*
|
|
@@ -236,7 +327,7 @@ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, bl
|
|
|
236
327
|
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
237
328
|
* - For small indices, falls back to the original logic.
|
|
238
329
|
*
|
|
239
|
-
* @param {
|
|
330
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
240
331
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
241
332
|
* @param {number} idx - The index to scroll to (exclusive)
|
|
242
333
|
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
|
@@ -255,7 +346,8 @@ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx,
|
|
|
255
346
|
// Fallback: O(n) for a single query
|
|
256
347
|
let offset = 0;
|
|
257
348
|
for (let i = 0; i < idx; i++) {
|
|
258
|
-
|
|
349
|
+
const height = heightCache[i] ?? calculatedItemHeight;
|
|
350
|
+
offset += height;
|
|
259
351
|
}
|
|
260
352
|
return offset;
|
|
261
353
|
}
|
|
@@ -40,9 +40,10 @@ export declare function shouldShowDebugInfo(prevRange: {
|
|
|
40
40
|
*
|
|
41
41
|
* This utility function generates a structured debug object that captures the complete
|
|
42
42
|
* state of a virtual list at any given moment. It includes critical metrics such as
|
|
43
|
-
* visible item count, viewport boundaries, total items,
|
|
44
|
-
* height calculations
|
|
45
|
-
*
|
|
43
|
+
* visible item count, viewport boundaries, total items, processed items with measured
|
|
44
|
+
* heights, height calculations, scroll position, and total content dimensions.
|
|
45
|
+
* This information is essential for performance monitoring, debugging scroll behavior,
|
|
46
|
+
* and optimizing virtual list configurations.
|
|
46
47
|
*
|
|
47
48
|
* Performance considerations:
|
|
48
49
|
* - All calculations are O(1)
|
|
@@ -51,16 +52,20 @@ export declare function shouldShowDebugInfo(prevRange: {
|
|
|
51
52
|
*
|
|
52
53
|
* @param visibleRange - Current visible range object containing start and end indices
|
|
53
54
|
* @param totalItems - Total number of items in the virtual list
|
|
54
|
-
* @param processedItems - Number of items
|
|
55
|
+
* @param processedItems - Number of items with measured heights (heightCache.length)
|
|
55
56
|
* @param averageItemHeight - Current calculated average height per item in pixels
|
|
57
|
+
* @param scrollTop - Current scroll position in pixels
|
|
58
|
+
* @param viewportHeight - Height of the viewport in pixels
|
|
56
59
|
* @returns {SvelteVirtualListDebugInfo} A structured debug information object
|
|
57
60
|
*
|
|
58
61
|
* @example
|
|
59
62
|
* const debugInfo = createDebugInfo(
|
|
60
63
|
* { start: 0, end: 10 },
|
|
61
64
|
* 1000,
|
|
62
|
-
*
|
|
63
|
-
*
|
|
65
|
+
* 50,
|
|
66
|
+
* 45,
|
|
67
|
+
* 200,
|
|
68
|
+
* 400
|
|
64
69
|
* );
|
|
65
70
|
* console.log('Virtual List State:', debugInfo);
|
|
66
71
|
*
|
|
@@ -69,4 +74,4 @@ export declare function shouldShowDebugInfo(prevRange: {
|
|
|
69
74
|
export declare function createDebugInfo(visibleRange: {
|
|
70
75
|
start: number;
|
|
71
76
|
end: number;
|
|
72
|
-
}, totalItems: number, processedItems: number, averageItemHeight: number): SvelteVirtualListDebugInfo;
|
|
77
|
+
}, totalItems: number, processedItems: number, averageItemHeight: number, scrollTop: number, viewportHeight: number, totalHeight: number): SvelteVirtualListDebugInfo;
|
|
@@ -39,9 +39,10 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
|
|
|
39
39
|
*
|
|
40
40
|
* This utility function generates a structured debug object that captures the complete
|
|
41
41
|
* state of a virtual list at any given moment. It includes critical metrics such as
|
|
42
|
-
* visible item count, viewport boundaries, total items,
|
|
43
|
-
* height calculations
|
|
44
|
-
*
|
|
42
|
+
* visible item count, viewport boundaries, total items, processed items with measured
|
|
43
|
+
* heights, height calculations, scroll position, and total content dimensions.
|
|
44
|
+
* This information is essential for performance monitoring, debugging scroll behavior,
|
|
45
|
+
* and optimizing virtual list configurations.
|
|
45
46
|
*
|
|
46
47
|
* Performance considerations:
|
|
47
48
|
* - All calculations are O(1)
|
|
@@ -50,28 +51,37 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
|
|
|
50
51
|
*
|
|
51
52
|
* @param visibleRange - Current visible range object containing start and end indices
|
|
52
53
|
* @param totalItems - Total number of items in the virtual list
|
|
53
|
-
* @param processedItems - Number of items
|
|
54
|
+
* @param processedItems - Number of items with measured heights (heightCache.length)
|
|
54
55
|
* @param averageItemHeight - Current calculated average height per item in pixels
|
|
56
|
+
* @param scrollTop - Current scroll position in pixels
|
|
57
|
+
* @param viewportHeight - Height of the viewport in pixels
|
|
55
58
|
* @returns {SvelteVirtualListDebugInfo} A structured debug information object
|
|
56
59
|
*
|
|
57
60
|
* @example
|
|
58
61
|
* const debugInfo = createDebugInfo(
|
|
59
62
|
* { start: 0, end: 10 },
|
|
60
63
|
* 1000,
|
|
61
|
-
*
|
|
62
|
-
*
|
|
64
|
+
* 50,
|
|
65
|
+
* 45,
|
|
66
|
+
* 200,
|
|
67
|
+
* 400
|
|
63
68
|
* );
|
|
64
69
|
* console.log('Virtual List State:', debugInfo);
|
|
65
70
|
*
|
|
66
71
|
* @throws {Error} Will throw if end index is less than start index in visibleRange
|
|
67
72
|
*/
|
|
68
|
-
export function createDebugInfo(visibleRange, totalItems, processedItems, averageItemHeight) {
|
|
73
|
+
export function createDebugInfo(visibleRange, totalItems, processedItems, averageItemHeight, scrollTop, viewportHeight, totalHeight) {
|
|
74
|
+
const atTop = scrollTop <= 1; // Small tolerance for floating point precision
|
|
75
|
+
const atBottom = scrollTop >= totalHeight - viewportHeight - 1; // Small tolerance
|
|
69
76
|
return {
|
|
70
77
|
visibleItemsCount: visibleRange.end - visibleRange.start,
|
|
71
78
|
startIndex: visibleRange.start,
|
|
72
79
|
endIndex: visibleRange.end,
|
|
73
80
|
totalItems,
|
|
74
|
-
processedItems,
|
|
75
|
-
averageItemHeight
|
|
81
|
+
processedItems, // Number of items with measured heights in heightCache
|
|
82
|
+
averageItemHeight,
|
|
83
|
+
atTop,
|
|
84
|
+
atBottom,
|
|
85
|
+
totalHeight
|
|
76
86
|
};
|
|
77
87
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1-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",
|
|
@@ -55,16 +55,17 @@
|
|
|
55
55
|
"lint": "prettier --check . && eslint .",
|
|
56
56
|
"lint:fix": "npm run format && eslint . --fix",
|
|
57
57
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
58
|
+
"prepare": "husky",
|
|
58
59
|
"prepublishOnly": "npm run package",
|
|
59
60
|
"preview": "vite preview",
|
|
60
|
-
"test": "vitest run --coverage",
|
|
61
|
+
"test": "vitest run --coverage --",
|
|
61
62
|
"test:all": "npm run test && npm run test:e2e",
|
|
62
|
-
"test:e2e": "playwright test",
|
|
63
|
-
"test:e2e:debug": "playwright test --debug",
|
|
63
|
+
"test:e2e": "playwright test --",
|
|
64
|
+
"test:e2e:debug": "playwright test --debug --",
|
|
64
65
|
"test:e2e:report": "playwright show-report",
|
|
65
|
-
"test:e2e:ui": "playwright test --ui",
|
|
66
|
-
"test:only": "vitest run",
|
|
67
|
-
"test:watch": "vitest"
|
|
66
|
+
"test:e2e:ui": "playwright test --ui --",
|
|
67
|
+
"test:only": "vitest run --",
|
|
68
|
+
"test:watch": "vitest --"
|
|
68
69
|
},
|
|
69
70
|
"overrides": {
|
|
70
71
|
"@sveltejs/kit": {
|
|
@@ -76,45 +77,46 @@
|
|
|
76
77
|
},
|
|
77
78
|
"devDependencies": {
|
|
78
79
|
"@eslint/compat": "^1.3.1",
|
|
79
|
-
"@eslint/js": "^9.
|
|
80
|
-
"@faker-js/faker": "^9.
|
|
81
|
-
"@playwright/test": "^1.
|
|
82
|
-
"@sveltejs/adapter-auto": "^6.0.
|
|
83
|
-
"@sveltejs/kit": "^2.
|
|
84
|
-
"@sveltejs/package": "^2.
|
|
85
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
86
|
-
"@testing-library/jest-dom": "^6.6.
|
|
80
|
+
"@eslint/js": "^9.32.0",
|
|
81
|
+
"@faker-js/faker": "^9.9.0",
|
|
82
|
+
"@playwright/test": "^1.54.2",
|
|
83
|
+
"@sveltejs/adapter-auto": "^6.0.2",
|
|
84
|
+
"@sveltejs/kit": "^2.27.3",
|
|
85
|
+
"@sveltejs/package": "^2.4.1",
|
|
86
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
|
87
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
87
88
|
"@testing-library/svelte": "^5.2.8",
|
|
88
89
|
"@testing-library/user-event": "^14.6.1",
|
|
89
|
-
"@types/node": "^24.0
|
|
90
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
91
|
-
"@typescript-eslint/parser": "^8.
|
|
90
|
+
"@types/node": "^24.2.0",
|
|
91
|
+
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
|
92
|
+
"@typescript-eslint/parser": "^8.39.0",
|
|
92
93
|
"@vitest/coverage-v8": "^3.2.4",
|
|
93
|
-
"eslint": "^9.
|
|
94
|
-
"eslint-config-prettier": "^10.1.
|
|
94
|
+
"eslint": "^9.32.0",
|
|
95
|
+
"eslint-config-prettier": "^10.1.8",
|
|
95
96
|
"eslint-plugin-import": "^2.32.0",
|
|
96
|
-
"eslint-plugin-svelte": "^3.
|
|
97
|
+
"eslint-plugin-svelte": "^3.11.0",
|
|
97
98
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
98
|
-
"globals": "^16.
|
|
99
|
+
"globals": "^16.3.0",
|
|
100
|
+
"husky": "^9.1.7",
|
|
99
101
|
"jsdom": "^26.1.0",
|
|
100
|
-
"prettier": "^3.6.
|
|
101
|
-
"prettier-plugin-organize-imports": "^4.
|
|
102
|
+
"prettier": "^3.6.2",
|
|
103
|
+
"prettier-plugin-organize-imports": "^4.2.0",
|
|
102
104
|
"prettier-plugin-sort-json": "^4.1.1",
|
|
103
105
|
"prettier-plugin-svelte": "^3.4.0",
|
|
104
|
-
"prettier-plugin-tailwindcss": "^0.6.
|
|
106
|
+
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
105
107
|
"publint": "^0.3.12",
|
|
106
|
-
"svelte": "^5.
|
|
107
|
-
"svelte-check": "^4.
|
|
108
|
-
"typescript": "^5.
|
|
109
|
-
"typescript-eslint": "^8.
|
|
110
|
-
"vite": "^
|
|
108
|
+
"svelte": "^5.38.0",
|
|
109
|
+
"svelte-check": "^4.3.1",
|
|
110
|
+
"typescript": "^5.9.2",
|
|
111
|
+
"typescript-eslint": "^8.39.0",
|
|
112
|
+
"vite": "^7.1.1",
|
|
111
113
|
"vitest": "^3.2.4"
|
|
112
114
|
},
|
|
113
115
|
"peerDependencies": {
|
|
114
116
|
"svelte": "^5.0.0"
|
|
115
117
|
},
|
|
116
118
|
"volta": {
|
|
117
|
-
"node": "22.
|
|
119
|
+
"node": "22.18.0"
|
|
118
120
|
},
|
|
119
121
|
"publishConfig": {
|
|
120
122
|
"access": "public"
|