@humanspeak/svelte-virtual-list 0.2.6-beta.0 → 0.2.6-beta.1
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.
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
import { onMount, tick } from 'svelte'
|
|
162
162
|
|
|
163
163
|
const rafSchedule = createRafScheduler()
|
|
164
|
-
|
|
164
|
+
const INTERNAL_DEBUG = true
|
|
165
165
|
/**
|
|
166
166
|
* Core configuration props with default values
|
|
167
167
|
* @type {SvelteVirtualListProps}
|
|
@@ -241,25 +241,74 @@
|
|
|
241
241
|
calculatedItemHeight = result.newHeight
|
|
242
242
|
lastMeasuredIndex = result.newLastMeasuredIndex
|
|
243
243
|
heightCache = result.updatedHeightCache
|
|
244
|
-
|
|
244
|
+
|
|
245
|
+
// Update running totals for precise height calculation (only when significant changes)
|
|
246
|
+
if (result.clearedDirtyItems.size > 10) {
|
|
247
|
+
const heights = Object.values(heightCache)
|
|
248
|
+
totalMeasuredHeight = heights.reduce((sum, h) => sum + h, 0)
|
|
249
|
+
measuredCount = heights.length
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Clear processed dirty items
|
|
253
|
+
result.clearedDirtyItems.forEach((index) => {
|
|
254
|
+
dirtyItems.delete(index)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
if (INTERNAL_DEBUG && result.clearedDirtyItems.size > 0) {
|
|
258
|
+
console.log(
|
|
259
|
+
`Cleared ${result.clearedDirtyItems.size} dirty items:`,
|
|
260
|
+
Array.from(result.clearedDirtyItems)
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
100, // debounceTime
|
|
265
|
+
dirtyItems // Pass dirty items for processing
|
|
245
266
|
)
|
|
246
267
|
}
|
|
247
268
|
|
|
248
269
|
// Add new effect to handle height changes
|
|
270
|
+
// Track if user has scrolled away from bottom to prevent snap-back
|
|
271
|
+
let userHasScrolledAway = $state(false)
|
|
272
|
+
let lastCalculatedHeight = $state(0)
|
|
273
|
+
|
|
249
274
|
$effect(() => {
|
|
250
275
|
if (BROWSER && initialized && mode === 'bottomToTop' && viewportElement) {
|
|
251
276
|
const totalHeight = Math.max(0, items.length * calculatedItemHeight)
|
|
252
277
|
const targetScrollTop = Math.max(0, totalHeight - height)
|
|
278
|
+
const currentScrollTop = viewportElement.scrollTop
|
|
279
|
+
const scrollDifference = Math.abs(currentScrollTop - targetScrollTop)
|
|
280
|
+
|
|
281
|
+
// Only correct scroll if:
|
|
282
|
+
// 1. Item height changed significantly (not just user scrolling)
|
|
283
|
+
// 2. User hasn't intentionally scrolled away from bottom
|
|
284
|
+
// 3. We're significantly off target
|
|
285
|
+
const heightChanged = Math.abs(calculatedItemHeight - lastCalculatedHeight) > 1
|
|
286
|
+
const shouldCorrect =
|
|
287
|
+
heightChanged && !userHasScrolledAway && scrollDifference > calculatedItemHeight * 3
|
|
288
|
+
|
|
289
|
+
if (shouldCorrect) {
|
|
290
|
+
if (INTERNAL_DEBUG) {
|
|
291
|
+
console.log(
|
|
292
|
+
'🔄 Correcting scroll position from',
|
|
293
|
+
currentScrollTop,
|
|
294
|
+
'to',
|
|
295
|
+
targetScrollTop,
|
|
296
|
+
'diff:',
|
|
297
|
+
scrollDifference,
|
|
298
|
+
'heightChanged:',
|
|
299
|
+
heightChanged
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
viewportElement.scrollTop = targetScrollTop
|
|
303
|
+
scrollTop = targetScrollTop
|
|
304
|
+
}
|
|
253
305
|
|
|
254
|
-
//
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
if (viewportElement) {
|
|
258
|
-
viewportElement.scrollTop = targetScrollTop
|
|
259
|
-
scrollTop = targetScrollTop
|
|
260
|
-
}
|
|
261
|
-
})
|
|
306
|
+
// Track if user has scrolled significantly away from bottom
|
|
307
|
+
if (scrollDifference > calculatedItemHeight * 5) {
|
|
308
|
+
userHasScrolledAway = true
|
|
262
309
|
}
|
|
310
|
+
|
|
311
|
+
lastCalculatedHeight = calculatedItemHeight
|
|
263
312
|
}
|
|
264
313
|
})
|
|
265
314
|
|
|
@@ -303,6 +352,23 @@
|
|
|
303
352
|
}
|
|
304
353
|
})
|
|
305
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Calculate precise item height based on actual measurements when available
|
|
357
|
+
*/
|
|
358
|
+
// Running totals for efficient precise height calculation
|
|
359
|
+
let totalMeasuredHeight = $state(0)
|
|
360
|
+
let measuredCount = $state(0)
|
|
361
|
+
const preciseItemHeight = $derived(() => {
|
|
362
|
+
if (measuredCount > 100) {
|
|
363
|
+
const avgHeight = totalMeasuredHeight / measuredCount
|
|
364
|
+
// Only use if the difference is significant (more than 0.5px)
|
|
365
|
+
if (Math.abs(avgHeight - calculatedItemHeight) > 0.5) {
|
|
366
|
+
return avgHeight
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return calculatedItemHeight
|
|
370
|
+
})
|
|
371
|
+
|
|
306
372
|
/**
|
|
307
373
|
* Calculates the range of items that should be rendered based on current scroll position.
|
|
308
374
|
*
|
|
@@ -326,7 +392,27 @@
|
|
|
326
392
|
if (!items.length) return { start: 0, end: 0 } as SvelteVirtualListPreviousVisibleRange
|
|
327
393
|
const viewportHeight = height || 0
|
|
328
394
|
|
|
329
|
-
|
|
395
|
+
// For bottomToTop mode, don't calculate visible range until properly initialized
|
|
396
|
+
// This prevents showing wrong items when scrollTop starts at 0
|
|
397
|
+
if (mode === 'bottomToTop' && !initialized && scrollTop === 0 && viewportHeight > 0) {
|
|
398
|
+
// Calculate what the correct scroll position should be
|
|
399
|
+
const totalHeight = items.length * calculatedItemHeight
|
|
400
|
+
const targetScrollTop = Math.max(0, totalHeight - viewportHeight)
|
|
401
|
+
|
|
402
|
+
// Use the target scroll position for visible range calculation
|
|
403
|
+
const result = calculateVisibleRange(
|
|
404
|
+
targetScrollTop,
|
|
405
|
+
viewportHeight,
|
|
406
|
+
calculatedItemHeight,
|
|
407
|
+
items.length,
|
|
408
|
+
bufferSize,
|
|
409
|
+
mode
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
return result
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = calculateVisibleRange(
|
|
330
416
|
scrollTop,
|
|
331
417
|
viewportHeight,
|
|
332
418
|
calculatedItemHeight,
|
|
@@ -334,6 +420,8 @@
|
|
|
334
420
|
bufferSize,
|
|
335
421
|
mode
|
|
336
422
|
)
|
|
423
|
+
|
|
424
|
+
return result
|
|
337
425
|
})
|
|
338
426
|
|
|
339
427
|
/**
|
|
@@ -495,7 +583,7 @@
|
|
|
495
583
|
itemResizeObserver = new ResizeObserver((entries) => {
|
|
496
584
|
let shouldRecalculate = false
|
|
497
585
|
|
|
498
|
-
if (
|
|
586
|
+
if (INTERNAL_DEBUG) {
|
|
499
587
|
console.log(`ResizeObserver fired for ${entries.length} entries`)
|
|
500
588
|
}
|
|
501
589
|
|
|
@@ -510,7 +598,7 @@
|
|
|
510
598
|
dirtyItems.add(actualIndex)
|
|
511
599
|
shouldRecalculate = true
|
|
512
600
|
|
|
513
|
-
if (
|
|
601
|
+
if (INTERNAL_DEBUG) {
|
|
514
602
|
console.log(
|
|
515
603
|
`Item ${actualIndex} marked dirty (resized), queue size: ${dirtyItems.size}`
|
|
516
604
|
)
|
|
@@ -556,7 +644,7 @@
|
|
|
556
644
|
|
|
557
645
|
// Add the effect in the script section
|
|
558
646
|
$effect(() => {
|
|
559
|
-
if (
|
|
647
|
+
if (INTERNAL_DEBUG) {
|
|
560
648
|
prevVisibleRange = visibleItems()
|
|
561
649
|
prevHeight = calculatedItemHeight
|
|
562
650
|
}
|
|
@@ -811,7 +899,7 @@
|
|
|
811
899
|
function autoObserveItemResize(element: HTMLElement) {
|
|
812
900
|
if (itemResizeObserver) {
|
|
813
901
|
itemResizeObserver.observe(element)
|
|
814
|
-
if (
|
|
902
|
+
if (INTERNAL_DEBUG) {
|
|
815
903
|
console.log(
|
|
816
904
|
'Started observing element:',
|
|
817
905
|
element,
|
|
@@ -819,7 +907,7 @@
|
|
|
819
907
|
element.getBoundingClientRect().height
|
|
820
908
|
)
|
|
821
909
|
}
|
|
822
|
-
} else if (
|
|
910
|
+
} else if (INTERNAL_DEBUG) {
|
|
823
911
|
console.log('itemResizeObserver not available for element:', element)
|
|
824
912
|
}
|
|
825
913
|
|
|
@@ -827,7 +915,7 @@
|
|
|
827
915
|
destroy() {
|
|
828
916
|
if (itemResizeObserver) {
|
|
829
917
|
itemResizeObserver.unobserve(element)
|
|
830
|
-
if (
|
|
918
|
+
if (INTERNAL_DEBUG) {
|
|
831
919
|
console.log('Stopped observing element:', element)
|
|
832
920
|
}
|
|
833
921
|
}
|
|
@@ -862,24 +950,36 @@
|
|
|
862
950
|
id="virtual-list-content"
|
|
863
951
|
{...testId ? { 'data-testid': `${testId}-content` } : {}}
|
|
864
952
|
class={contentClass ?? 'virtual-list-content'}
|
|
865
|
-
style:height="{
|
|
953
|
+
style:height="{(() => {
|
|
954
|
+
// Use precise height when available for better cross-browser compatibility
|
|
955
|
+
const totalActualHeight = items.length * preciseItemHeight()
|
|
956
|
+
return Math.max(height, totalActualHeight)
|
|
957
|
+
})()}px"
|
|
866
958
|
>
|
|
867
959
|
<!-- Items container is translated to show correct items -->
|
|
868
960
|
<div
|
|
869
961
|
id="virtual-list-items"
|
|
870
962
|
{...testId ? { 'data-testid': `${testId}-items` } : {}}
|
|
871
963
|
class={itemsClass ?? 'virtual-list-items'}
|
|
872
|
-
style:transform="translateY({
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
964
|
+
style:transform="translateY({(() => {
|
|
965
|
+
const transform = calculateTransformY(
|
|
966
|
+
mode,
|
|
967
|
+
items.length,
|
|
968
|
+
visibleItems().end,
|
|
969
|
+
visibleItems().start,
|
|
970
|
+
calculatedItemHeight
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
return transform
|
|
974
|
+
})()}px)"
|
|
879
975
|
>
|
|
880
|
-
{#each
|
|
881
|
-
|
|
882
|
-
|
|
976
|
+
{#each (() => {
|
|
977
|
+
const slice = mode === 'bottomToTop' ? items
|
|
978
|
+
.slice(visibleItems().start, visibleItems().end)
|
|
979
|
+
.reverse() : items.slice(visibleItems().start, visibleItems().end)
|
|
980
|
+
|
|
981
|
+
return slice
|
|
982
|
+
})() as currentItem, i (currentItem?.id ?? i)}
|
|
883
983
|
<!-- Only debug when visible range or average height changes -->
|
|
884
984
|
{#if debug && i === 0 && shouldShowDebugInfo(prevVisibleRange, visibleItems(), prevHeight, calculatedItemHeight)}
|
|
885
985
|
{@const debugInfo = createDebugInfo(
|
|
@@ -74,4 +74,5 @@ export declare const calculateAverageHeightDebounced: (isCalculatingHeight: bool
|
|
|
74
74
|
newHeight: number;
|
|
75
75
|
newLastMeasuredIndex: number;
|
|
76
76
|
updatedHeightCache: Record<number, number>;
|
|
77
|
-
|
|
77
|
+
clearedDirtyItems: Set<number>;
|
|
78
|
+
}) => void, debounceTime: number, dirtyItems: Set<number>) => NodeJS.Timeout | null;
|
|
@@ -71,20 +71,23 @@ import { BROWSER } from 'esm-env';
|
|
|
71
71
|
*/
|
|
72
72
|
export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout, visibleItemsGetter, itemElements, heightCache, lastMeasuredIndex, calculatedItemHeight,
|
|
73
73
|
/* trunk-ignore(eslint/no-unused-vars) */
|
|
74
|
-
onUpdate, debounceTime
|
|
75
|
-
if (!BROWSER || isCalculatingHeight
|
|
74
|
+
onUpdate, debounceTime, dirtyItems) => {
|
|
75
|
+
if (!BROWSER || isCalculatingHeight)
|
|
76
76
|
return null;
|
|
77
77
|
const visibleRange = visibleItemsGetter();
|
|
78
78
|
const currentIndex = visibleRange.start;
|
|
79
79
|
if (currentIndex === lastMeasuredIndex)
|
|
80
80
|
return null;
|
|
81
|
+
if (heightUpdateTimeout)
|
|
82
|
+
clearTimeout(heightUpdateTimeout);
|
|
81
83
|
return setTimeout(() => {
|
|
82
|
-
const { newHeight, newLastMeasuredIndex, updatedHeightCache } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight);
|
|
83
|
-
if (Math.abs(newHeight - calculatedItemHeight) > 1) {
|
|
84
|
+
const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight, dirtyItems);
|
|
85
|
+
if (Math.abs(newHeight - calculatedItemHeight) > 1 || dirtyItems.size > 0) {
|
|
84
86
|
onUpdate({
|
|
85
87
|
newHeight,
|
|
86
88
|
newLastMeasuredIndex,
|
|
87
|
-
updatedHeightCache
|
|
89
|
+
updatedHeightCache,
|
|
90
|
+
clearedDirtyItems
|
|
88
91
|
});
|
|
89
92
|
}
|
|
90
93
|
}, debounceTime);
|
|
@@ -86,10 +86,11 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
86
86
|
*/
|
|
87
87
|
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
88
88
|
start: number;
|
|
89
|
-
}, heightCache: Record<number, number>, currentItemHeight: number) => {
|
|
89
|
+
}, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>) => {
|
|
90
90
|
newHeight: number;
|
|
91
91
|
newLastMeasuredIndex: number;
|
|
92
92
|
updatedHeightCache: Record<number, number>;
|
|
93
|
+
clearedDirtyItems: Set<number>;
|
|
93
94
|
};
|
|
94
95
|
/**
|
|
95
96
|
* Processes large arrays in chunks to prevent UI blocking.
|
|
@@ -35,6 +35,17 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
35
35
|
if (mode === 'bottomToTop') {
|
|
36
36
|
const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
|
|
37
37
|
const bottomIndex = totalItems - Math.floor(scrollTop / itemHeight);
|
|
38
|
+
// Safeguard: if bottomIndex is negative, it means scrollTop is too large for current itemHeight
|
|
39
|
+
// This can happen when itemHeight changes but scrollTop hasn't been corrected yet
|
|
40
|
+
if (bottomIndex < 0) {
|
|
41
|
+
// Calculate what scrollTop should be and use that for visible range
|
|
42
|
+
const totalHeight = totalItems * itemHeight;
|
|
43
|
+
const correctedScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
44
|
+
const correctedBottomIndex = totalItems - Math.floor(correctedScrollTop / itemHeight);
|
|
45
|
+
const start = Math.max(0, correctedBottomIndex - visibleCount - bufferSize);
|
|
46
|
+
const end = Math.min(totalItems, correctedBottomIndex + bufferSize);
|
|
47
|
+
return { start, end };
|
|
48
|
+
}
|
|
38
49
|
// Add buffer to both ends
|
|
39
50
|
const start = Math.max(0, bottomIndex - visibleCount - bufferSize);
|
|
40
51
|
const end = Math.min(totalItems, bottomIndex + bufferSize);
|
|
@@ -43,6 +54,23 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
43
54
|
else {
|
|
44
55
|
const start = Math.floor(scrollTop / itemHeight);
|
|
45
56
|
const end = Math.min(totalItems, start + Math.ceil(viewportHeight / itemHeight) + 1);
|
|
57
|
+
// Safeguard for topToBottom: ensure last item is fully visible when at max scroll
|
|
58
|
+
const totalHeight = totalItems * itemHeight;
|
|
59
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
60
|
+
// Add dynamic tolerance based on item height for browser rendering precision
|
|
61
|
+
const tolerance = Math.max(itemHeight, 10); // At least one full item height or 10px minimum
|
|
62
|
+
const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
|
|
63
|
+
if (isAtBottom) {
|
|
64
|
+
// When at the bottom, ensure we include all items up to the end
|
|
65
|
+
const adjustedEnd = totalItems;
|
|
66
|
+
const visibleItemCount = Math.ceil(viewportHeight / itemHeight) + bufferSize + 1;
|
|
67
|
+
const adjustedStart = Math.max(0, adjustedEnd - visibleItemCount);
|
|
68
|
+
// TopToBottom safeguard is now active
|
|
69
|
+
return {
|
|
70
|
+
start: adjustedStart,
|
|
71
|
+
end: adjustedEnd
|
|
72
|
+
};
|
|
73
|
+
}
|
|
46
74
|
// Add buffer to both ends
|
|
47
75
|
return {
|
|
48
76
|
start: Math.max(0, start - bufferSize),
|
|
@@ -65,9 +93,15 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
65
93
|
* @returns {number} The calculated transform Y value in pixels
|
|
66
94
|
*/
|
|
67
95
|
export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
if (mode === 'bottomToTop') {
|
|
97
|
+
// In bottomToTop mode, we need to position the container so that
|
|
98
|
+
// the first visible item (visibleStart) aligns with its correct position
|
|
99
|
+
// from the bottom of the total content
|
|
100
|
+
return (totalItems - visibleEnd) * itemHeight;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return visibleStart * itemHeight;
|
|
104
|
+
}
|
|
71
105
|
};
|
|
72
106
|
/**
|
|
73
107
|
* Updates the virtual list's height and scroll position when necessary.
|
|
@@ -124,39 +158,89 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
124
158
|
* 40
|
|
125
159
|
* )
|
|
126
160
|
*/
|
|
127
|
-
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
|
|
161
|
+
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight, dirtyItems) => {
|
|
128
162
|
const validElements = itemElements.filter((el) => el);
|
|
129
163
|
if (validElements.length === 0) {
|
|
130
164
|
return {
|
|
131
165
|
newHeight: currentItemHeight,
|
|
132
166
|
newLastMeasuredIndex: visibleRange.start,
|
|
133
|
-
updatedHeightCache: heightCache
|
|
167
|
+
updatedHeightCache: heightCache,
|
|
168
|
+
clearedDirtyItems: new Set()
|
|
134
169
|
};
|
|
135
170
|
}
|
|
136
171
|
const newHeightCache = { ...heightCache };
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
const clearedDirtyItems = new Set();
|
|
173
|
+
// Initialize running totals for O(1) average calculation
|
|
174
|
+
let totalValidHeight = 0;
|
|
175
|
+
let validHeightCount = 0;
|
|
176
|
+
// Calculate initial totals from existing cache
|
|
177
|
+
for (const height of Object.values(heightCache)) {
|
|
178
|
+
if (Number.isFinite(height) && height > 0) {
|
|
179
|
+
totalValidHeight += height;
|
|
180
|
+
validHeightCount++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Process only dirty items if they exist, otherwise process all visible items
|
|
184
|
+
if (dirtyItems.size > 0) {
|
|
185
|
+
// Process only dirty items
|
|
186
|
+
dirtyItems.forEach((itemIndex) => {
|
|
187
|
+
const elementIndex = itemIndex - visibleRange.start;
|
|
188
|
+
const element = validElements[elementIndex];
|
|
189
|
+
if (element && elementIndex >= 0 && elementIndex < validElements.length) {
|
|
190
|
+
try {
|
|
191
|
+
const height = element.getBoundingClientRect().height;
|
|
192
|
+
if (Number.isFinite(height) && height > 0) {
|
|
193
|
+
const oldHeight = newHeightCache[itemIndex];
|
|
194
|
+
// Only update if height actually changed (use smaller tolerance for precision)
|
|
195
|
+
if (!oldHeight || Math.abs(oldHeight - height) >= 0.1) {
|
|
196
|
+
// Update running totals
|
|
197
|
+
if (oldHeight && Number.isFinite(oldHeight) && oldHeight > 0) {
|
|
198
|
+
// Replace old height with new height in running total
|
|
199
|
+
totalValidHeight = totalValidHeight - oldHeight + height;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Add new height to running total
|
|
203
|
+
totalValidHeight += height;
|
|
204
|
+
validHeightCount++;
|
|
205
|
+
}
|
|
206
|
+
newHeightCache[itemIndex] = height;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
clearedDirtyItems.add(itemIndex);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Skip invalid measurements but still clear from dirty
|
|
213
|
+
clearedDirtyItems.add(itemIndex);
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
|
-
|
|
148
|
-
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Original behavior: process all visible items
|
|
220
|
+
validElements.forEach((el, i) => {
|
|
221
|
+
const itemIndex = visibleRange.start + i;
|
|
222
|
+
if (!newHeightCache[itemIndex]) {
|
|
223
|
+
try {
|
|
224
|
+
const height = el.getBoundingClientRect().height;
|
|
225
|
+
if (Number.isFinite(height) && height > 0) {
|
|
226
|
+
// Add new height to running totals
|
|
227
|
+
totalValidHeight += height;
|
|
228
|
+
validHeightCount++;
|
|
229
|
+
newHeightCache[itemIndex] = height;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Skip invalid measurements
|
|
234
|
+
}
|
|
149
235
|
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
//
|
|
153
|
-
const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// O(1) average calculation using running totals!
|
|
154
239
|
return {
|
|
155
|
-
newHeight:
|
|
156
|
-
? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
|
|
157
|
-
: currentItemHeight,
|
|
240
|
+
newHeight: validHeightCount > 0 ? totalValidHeight / validHeightCount : currentItemHeight,
|
|
158
241
|
newLastMeasuredIndex: visibleRange.start,
|
|
159
|
-
updatedHeightCache: newHeightCache
|
|
242
|
+
updatedHeightCache: newHeightCache,
|
|
243
|
+
clearedDirtyItems
|
|
160
244
|
};
|
|
161
245
|
};
|
|
162
246
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.2.6-beta.
|
|
3
|
+
"version": "0.2.6-beta.1",
|
|
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,6 +55,7 @@
|
|
|
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
61
|
"test": "vitest run --coverage",
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
"eslint-plugin-svelte": "^3.11.0",
|
|
98
99
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
99
100
|
"globals": "^16.3.0",
|
|
101
|
+
"husky": "^9.1.7",
|
|
100
102
|
"jsdom": "^26.1.0",
|
|
101
103
|
"prettier": "^3.6.2",
|
|
102
104
|
"prettier-plugin-organize-imports": "^4.2.0",
|