@humanspeak/svelte-virtual-list 0.3.13 → 0.4.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/dist/SvelteVirtualList.svelte +52 -52
- package/dist/reactive-list-manager/INTEGRATION_EXAMPLE.md +2 -2
- package/dist/reactive-list-manager/README.md +9 -9
- package/dist/reactive-list-manager/ReactiveListManager.svelte.d.ts +1 -1
- package/dist/reactive-list-manager/ReactiveListManager.svelte.js +1 -1
- package/dist/reactive-list-manager/index.d.ts +1 -1
- package/dist/reactive-list-manager/index.js +1 -1
- package/dist/utils/heightCalculation.d.ts +3 -3
- package/dist/utils/heightCalculation.js +5 -6
- package/package.json +1 -1
|
@@ -251,11 +251,11 @@
|
|
|
251
251
|
|
|
252
252
|
const captureAnchor = () => {
|
|
253
253
|
if (!heightManager.viewportElement) return
|
|
254
|
-
const vr = visibleItems
|
|
254
|
+
const vr = visibleItems
|
|
255
255
|
const anchorIndex = Math.max(0, vr.start)
|
|
256
256
|
const cache = heightManager.getHeightCache()
|
|
257
257
|
const est = heightManager.averageHeight
|
|
258
|
-
const maxScrollTop = Math.max(0, totalHeight
|
|
258
|
+
const maxScrollTop = Math.max(0, totalHeight - (height || 0))
|
|
259
259
|
// Offset from start to anchored item
|
|
260
260
|
const blockSums = buildBlockSums(cache, est, items.length)
|
|
261
261
|
const offsetToIndex = getScrollOffsetForIndex(cache, est, anchorIndex, blockSums)
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
Math.max(0, lastAnchorIndex),
|
|
291
291
|
blockSums
|
|
292
292
|
)
|
|
293
|
-
const maxScrollTop = clampValue(totalHeight
|
|
293
|
+
const maxScrollTop = clampValue(totalHeight - (height || 0), 0, Infinity)
|
|
294
294
|
let targetTop: number
|
|
295
295
|
if (mode === 'bottomToTop') {
|
|
296
296
|
const distanceFromStart = clampValue(offsetToIndex + lastAnchorOffset, 0, Infinity)
|
|
@@ -402,23 +402,6 @@
|
|
|
402
402
|
// Dynamic update coordination to avoid UA scroll anchoring interference
|
|
403
403
|
let suppressBottomAnchoringUntilMs = $state(0)
|
|
404
404
|
|
|
405
|
-
const displayItems = $derived(() => {
|
|
406
|
-
const visibleRange = visibleItems()
|
|
407
|
-
const slice =
|
|
408
|
-
mode === 'bottomToTop'
|
|
409
|
-
? items.slice(visibleRange.start, visibleRange.end).reverse()
|
|
410
|
-
: items.slice(visibleRange.start, visibleRange.end)
|
|
411
|
-
|
|
412
|
-
return slice.map((item, sliceIndex) => ({
|
|
413
|
-
item,
|
|
414
|
-
originalIndex:
|
|
415
|
-
mode === 'bottomToTop'
|
|
416
|
-
? visibleRange.end - 1 - sliceIndex
|
|
417
|
-
: visibleRange.start + sliceIndex,
|
|
418
|
-
sliceIndex
|
|
419
|
-
}))
|
|
420
|
-
})
|
|
421
|
-
|
|
422
405
|
/**
|
|
423
406
|
* Handles scroll position corrections when item heights change, ensuring proper positioning
|
|
424
407
|
* relative to the user's scroll context. This function calculates the cumulative impact of
|
|
@@ -442,7 +425,7 @@
|
|
|
442
425
|
if (isScrolling) {
|
|
443
426
|
// Accumulate net change above viewport and defer application
|
|
444
427
|
let pending = 0
|
|
445
|
-
const currentVisibleRange = visibleItems
|
|
428
|
+
const currentVisibleRange = visibleItems
|
|
446
429
|
for (const change of heightChanges) {
|
|
447
430
|
if (change.index < currentVisibleRange.start) pending += change.delta
|
|
448
431
|
}
|
|
@@ -488,7 +471,7 @@
|
|
|
488
471
|
*
|
|
489
472
|
* Dependencies:
|
|
490
473
|
* - wasAtBottomBeforeHeightChange: Set to true when first item marked dirty, prevents cascading corrections
|
|
491
|
-
* - totalHeight
|
|
474
|
+
* - totalHeight: Uses actual heightCache measurements instead of skewed averages
|
|
492
475
|
* - Aggressive scroll correction: Blocked when wasAtBottomBeforeHeightChange=true
|
|
493
476
|
*
|
|
494
477
|
* ⚠️ DO NOT MODIFY WITHOUT EXTENSIVE TESTING ⚠️
|
|
@@ -518,7 +501,7 @@
|
|
|
518
501
|
lastCorrectionTimestampByViewport.set(viewportEl, now)
|
|
519
502
|
|
|
520
503
|
// Step 1: Scroll to approximate position to ensure Item 0 gets rendered in virtual viewport
|
|
521
|
-
const approximateScrollTop = Math.max(0, totalHeight
|
|
504
|
+
const approximateScrollTop = Math.max(0, totalHeight - height)
|
|
522
505
|
log('[SVL] b2t-correction-approx', { approximateScrollTop })
|
|
523
506
|
syncScrollTop(approximateScrollTop)
|
|
524
507
|
|
|
@@ -555,11 +538,11 @@
|
|
|
555
538
|
}
|
|
556
539
|
|
|
557
540
|
const currentScrollTop = heightManager.viewport.scrollTop
|
|
558
|
-
const maxScrollTop = Math.max(0, totalHeight
|
|
541
|
+
const maxScrollTop = Math.max(0, totalHeight - height)
|
|
559
542
|
|
|
560
543
|
// Calculate total height change impact above current visible area
|
|
561
544
|
let heightChangeAboveViewport = 0
|
|
562
|
-
const currentVisibleRange = visibleItems
|
|
545
|
+
const currentVisibleRange = visibleItems
|
|
563
546
|
|
|
564
547
|
for (const change of heightChanges) {
|
|
565
548
|
// Only consider items that are above the current visible range
|
|
@@ -623,7 +606,7 @@
|
|
|
623
606
|
// Skip loading during bottomToTop initialization (init path renders all items artificially)
|
|
624
607
|
if (mode === 'bottomToTop' && !bottomToTopScrollComplete) return
|
|
625
608
|
|
|
626
|
-
const range = visibleItems
|
|
609
|
+
const range = visibleItems
|
|
627
610
|
const atLoadingEdge = range.end >= items.length - loadMoreThreshold
|
|
628
611
|
const insufficientItems = items.length < loadMoreThreshold && heightManager.initialized
|
|
629
612
|
|
|
@@ -755,9 +738,9 @@
|
|
|
755
738
|
* This getter is reactive and updates whenever heightManager's internal state changes.
|
|
756
739
|
* Used by: atBottom calculation, scroll corrections, maxScrollTop calculations
|
|
757
740
|
*/
|
|
758
|
-
const totalHeight = $derived(
|
|
741
|
+
const totalHeight = $derived(heightManager.totalHeight)
|
|
759
742
|
|
|
760
|
-
const atBottom = $derived(heightManager.scrollTop >= totalHeight
|
|
743
|
+
const atBottom = $derived(heightManager.scrollTop >= totalHeight - height - 1)
|
|
761
744
|
let wasAtBottomBeforeHeightChange = false
|
|
762
745
|
let lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null = null
|
|
763
746
|
|
|
@@ -787,7 +770,7 @@
|
|
|
787
770
|
mode === 'bottomToTop' &&
|
|
788
771
|
heightManager.viewportElement
|
|
789
772
|
) {
|
|
790
|
-
const targetScrollTop = Math.max(0, totalHeight
|
|
773
|
+
const targetScrollTop = Math.max(0, totalHeight - height)
|
|
791
774
|
const currentScrollTop = heightManager.viewport.scrollTop
|
|
792
775
|
const scrollDifference = Math.abs(currentScrollTop - targetScrollTop)
|
|
793
776
|
|
|
@@ -797,7 +780,7 @@
|
|
|
797
780
|
// 3. We're significantly off target
|
|
798
781
|
// 4. We're not at the bottom (where height changes should be handled more carefully)
|
|
799
782
|
const heightChanged = Math.abs(heightManager.averageHeight - lastCalculatedHeight) > 1
|
|
800
|
-
const maxScrollTop = Math.max(0, totalHeight
|
|
783
|
+
const maxScrollTop = Math.max(0, totalHeight - height)
|
|
801
784
|
|
|
802
785
|
// In bottomToTop mode, we're "at bottom" when scroll is at max position
|
|
803
786
|
const isAtBottom =
|
|
@@ -845,7 +828,7 @@
|
|
|
845
828
|
const currentScrollTop = heightManager.viewport.scrollTop
|
|
846
829
|
const currentCalculatedItemHeight = heightManager.averageHeight
|
|
847
830
|
const currentHeight = height
|
|
848
|
-
const currentTotalHeight = totalHeight
|
|
831
|
+
const currentTotalHeight = totalHeight
|
|
849
832
|
const prevTotalHeight =
|
|
850
833
|
lastTotalHeightObserved ||
|
|
851
834
|
currentTotalHeight - itemsAdded * currentCalculatedItemHeight
|
|
@@ -891,7 +874,7 @@
|
|
|
891
874
|
// Reconcile on next frame in case measured heights adjust totals
|
|
892
875
|
requestAnimationFrame(() => {
|
|
893
876
|
const beforeReconcileScrollTop = heightManager.viewport.scrollTop
|
|
894
|
-
const reconciledNextMax = clampValue(totalHeight
|
|
877
|
+
const reconciledNextMax = clampValue(totalHeight - height, 0, Infinity)
|
|
895
878
|
const reconciledDeltaMaxChange = reconciledNextMax - nextMaxScrollTop
|
|
896
879
|
// Desired position is to maintain distance-from-end; equivalently keep (max - scrollTop) constant.
|
|
897
880
|
const desiredScrollTop = clampValue(
|
|
@@ -934,7 +917,7 @@
|
|
|
934
917
|
|
|
935
918
|
lastItemsLength = currentItemsLength
|
|
936
919
|
// Update last observed total height at the end of the effect
|
|
937
|
-
lastTotalHeightObserved = totalHeight
|
|
920
|
+
lastTotalHeightObserved = totalHeight
|
|
938
921
|
})
|
|
939
922
|
|
|
940
923
|
// Update container height continuously to reflect layout changes that
|
|
@@ -970,13 +953,13 @@
|
|
|
970
953
|
*
|
|
971
954
|
* @example
|
|
972
955
|
* ```typescript
|
|
973
|
-
* const range = visibleItems
|
|
956
|
+
* const range = visibleItems
|
|
974
957
|
* console.info(`Rendering items from ${range.start} to ${range.end}`)
|
|
975
958
|
* ```
|
|
976
959
|
*
|
|
977
960
|
* @returns {SvelteVirtualListPreviousVisibleRange} Object containing start and end indices of visible items
|
|
978
961
|
*/
|
|
979
|
-
const visibleItems = $derived((): SvelteVirtualListPreviousVisibleRange => {
|
|
962
|
+
const visibleItems = $derived.by((): SvelteVirtualListPreviousVisibleRange => {
|
|
980
963
|
if (!items.length) return { start: 0, end: 0 } as SvelteVirtualListPreviousVisibleRange
|
|
981
964
|
const viewportHeight = height || 0
|
|
982
965
|
|
|
@@ -1021,7 +1004,7 @@
|
|
|
1021
1004
|
atBottom,
|
|
1022
1005
|
wasAtBottomBeforeHeightChange,
|
|
1023
1006
|
lastVisibleRange,
|
|
1024
|
-
totalHeight
|
|
1007
|
+
totalHeight,
|
|
1025
1008
|
heightManager.getHeightCache()
|
|
1026
1009
|
)
|
|
1027
1010
|
|
|
@@ -1033,15 +1016,15 @@
|
|
|
1033
1016
|
* Uses the maximum of container height and total content height to ensure
|
|
1034
1017
|
* proper scrolling behavior.
|
|
1035
1018
|
*/
|
|
1036
|
-
const contentHeight = $derived(
|
|
1019
|
+
const contentHeight = $derived(Math.max(height, totalHeight))
|
|
1037
1020
|
|
|
1038
1021
|
/**
|
|
1039
1022
|
* Computed transform Y value for positioning the visible items.
|
|
1040
1023
|
* Extracted from inline IIFE for better performance and readability.
|
|
1041
1024
|
*/
|
|
1042
|
-
const transformY = $derived(() => {
|
|
1025
|
+
const transformY = $derived.by(() => {
|
|
1043
1026
|
const viewportHeight = height || measuredFallbackHeight || 0
|
|
1044
|
-
const visibleRange = visibleItems
|
|
1027
|
+
const visibleRange = visibleItems
|
|
1045
1028
|
|
|
1046
1029
|
// Avoid synchronous DOM reads here; fall back once if height is 0
|
|
1047
1030
|
const effectiveHeight = viewportHeight === 0 ? 400 : viewportHeight
|
|
@@ -1055,13 +1038,30 @@
|
|
|
1055
1038
|
visibleRange.start,
|
|
1056
1039
|
heightManager.averageHeight,
|
|
1057
1040
|
effectiveHeight,
|
|
1058
|
-
totalHeight
|
|
1041
|
+
totalHeight,
|
|
1059
1042
|
heightManager.getHeightCache(),
|
|
1060
1043
|
measuredFallbackHeight
|
|
1061
1044
|
)
|
|
1062
1045
|
)
|
|
1063
1046
|
})
|
|
1064
1047
|
|
|
1048
|
+
const displayItems = $derived.by(() => {
|
|
1049
|
+
const visibleRange = visibleItems
|
|
1050
|
+
const slice =
|
|
1051
|
+
mode === 'bottomToTop'
|
|
1052
|
+
? items.slice(visibleRange.start, visibleRange.end).reverse()
|
|
1053
|
+
: items.slice(visibleRange.start, visibleRange.end)
|
|
1054
|
+
|
|
1055
|
+
return slice.map((item, sliceIndex) => ({
|
|
1056
|
+
item,
|
|
1057
|
+
originalIndex:
|
|
1058
|
+
mode === 'bottomToTop'
|
|
1059
|
+
? visibleRange.end - 1 - sliceIndex
|
|
1060
|
+
: visibleRange.start + sliceIndex,
|
|
1061
|
+
sliceIndex
|
|
1062
|
+
}))
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
1065
|
/**
|
|
1066
1066
|
* Handles scroll events in the viewport using requestAnimationFrame for performance.
|
|
1067
1067
|
*
|
|
@@ -1124,12 +1124,12 @@
|
|
|
1124
1124
|
captureAnchor()
|
|
1125
1125
|
}
|
|
1126
1126
|
if (INTERNAL_DEBUG) {
|
|
1127
|
-
const vr = visibleItems
|
|
1127
|
+
const vr = visibleItems
|
|
1128
1128
|
log('[SVL] scroll', {
|
|
1129
1129
|
mode,
|
|
1130
1130
|
scrollTop: heightManager.scrollTop,
|
|
1131
1131
|
height,
|
|
1132
|
-
totalHeight: totalHeight
|
|
1132
|
+
totalHeight: totalHeight,
|
|
1133
1133
|
averageItemHeight: heightManager.averageHeight,
|
|
1134
1134
|
visibleRange: vr
|
|
1135
1135
|
})
|
|
@@ -1160,7 +1160,7 @@
|
|
|
1160
1160
|
})
|
|
1161
1161
|
if (!heightManager.initialized && mode === 'bottomToTop') {
|
|
1162
1162
|
// bottomToTop initialization: use scrollIntoView on Item 0 for precise positioning
|
|
1163
|
-
// visibleItems
|
|
1163
|
+
// visibleItems guarantees Item 0 is rendered during initialization
|
|
1164
1164
|
tick().then(() => {
|
|
1165
1165
|
requestAnimationFrame(() => {
|
|
1166
1166
|
requestAnimationFrame(() => {
|
|
@@ -1181,7 +1181,7 @@
|
|
|
1181
1181
|
|
|
1182
1182
|
setTimeout(() => {
|
|
1183
1183
|
// Step 1: Set initialized (for other purposes like scroll event handling)
|
|
1184
|
-
// The init path in visibleItems
|
|
1184
|
+
// The init path in visibleItems stays active until bottomToTopScrollComplete
|
|
1185
1185
|
if (!heightManager.initialized) {
|
|
1186
1186
|
heightManager.initialized = true
|
|
1187
1187
|
}
|
|
@@ -1265,7 +1265,7 @@
|
|
|
1265
1265
|
rafSchedule(() => {
|
|
1266
1266
|
log('item-resize-observer', { entries: entries.length })
|
|
1267
1267
|
let shouldRecalculate = false
|
|
1268
|
-
void visibleItems
|
|
1268
|
+
void visibleItems // Cache once to avoid reactive loops
|
|
1269
1269
|
|
|
1270
1270
|
for (const entry of entries) {
|
|
1271
1271
|
const element = entry.target as HTMLElement
|
|
@@ -1349,7 +1349,7 @@
|
|
|
1349
1349
|
// Add the effect in the script section
|
|
1350
1350
|
$effect(() => {
|
|
1351
1351
|
if (INTERNAL_DEBUG) {
|
|
1352
|
-
prevVisibleRange = visibleItems
|
|
1352
|
+
prevVisibleRange = visibleItems
|
|
1353
1353
|
prevHeight = heightManager.averageHeight
|
|
1354
1354
|
}
|
|
1355
1355
|
})
|
|
@@ -1358,7 +1358,7 @@
|
|
|
1358
1358
|
// the callback writes to $state (which is forbidden during render effects)
|
|
1359
1359
|
$effect(() => {
|
|
1360
1360
|
if (!debug) return
|
|
1361
|
-
const currentVisibleRange = visibleItems
|
|
1361
|
+
const currentVisibleRange = visibleItems
|
|
1362
1362
|
if (
|
|
1363
1363
|
!shouldShowDebugInfo(
|
|
1364
1364
|
prevVisibleRange,
|
|
@@ -1376,7 +1376,7 @@
|
|
|
1376
1376
|
heightManager.averageHeight,
|
|
1377
1377
|
heightManager.scrollTop,
|
|
1378
1378
|
height || 0,
|
|
1379
|
-
totalHeight
|
|
1379
|
+
totalHeight
|
|
1380
1380
|
)
|
|
1381
1381
|
|
|
1382
1382
|
if (debugFunction) {
|
|
@@ -1484,7 +1484,7 @@
|
|
|
1484
1484
|
}
|
|
1485
1485
|
}
|
|
1486
1486
|
|
|
1487
|
-
const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems
|
|
1487
|
+
const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems
|
|
1488
1488
|
|
|
1489
1489
|
// Use extracted scroll calculation utility
|
|
1490
1490
|
const scrollTarget = calculateScrollTarget({
|
|
@@ -1630,16 +1630,16 @@
|
|
|
1630
1630
|
id="virtual-list-content"
|
|
1631
1631
|
{...testId ? { 'data-testid': `${testId}-content` } : {}}
|
|
1632
1632
|
class={contentClass ?? 'virtual-list-content'}
|
|
1633
|
-
style:height="{contentHeight
|
|
1633
|
+
style:height="{contentHeight}px"
|
|
1634
1634
|
>
|
|
1635
1635
|
<!-- Items container is translated to show correct items -->
|
|
1636
1636
|
<div
|
|
1637
1637
|
id="virtual-list-items"
|
|
1638
1638
|
{...testId ? { 'data-testid': `${testId}-items` } : {}}
|
|
1639
1639
|
class={itemsClass ?? 'virtual-list-items'}
|
|
1640
|
-
style:transform="translateY({transformY
|
|
1640
|
+
style:transform="translateY({transformY}px)"
|
|
1641
1641
|
>
|
|
1642
|
-
{#each displayItems
|
|
1642
|
+
{#each displayItems as currentItemWithIndex, _i (currentItemWithIndex.originalIndex)}
|
|
1643
1643
|
<!-- Render each visible item -->
|
|
1644
1644
|
<div
|
|
1645
1645
|
bind:this={itemElements[currentItemWithIndex.sliceIndex]}
|
|
@@ -71,7 +71,7 @@ const updateHeight = () => {
|
|
|
71
71
|
|
|
72
72
|
```typescript
|
|
73
73
|
// OLD: O(n) calculation every time
|
|
74
|
-
// let totalHeight = $derived(() => {
|
|
74
|
+
// let totalHeight = $derived.by(() => {
|
|
75
75
|
// let total = 0
|
|
76
76
|
// for (let i = 0; i < items.length; i++) {
|
|
77
77
|
// total += heightCache[i] || calculatedItemHeight
|
|
@@ -80,7 +80,7 @@ const updateHeight = () => {
|
|
|
80
80
|
// })
|
|
81
81
|
|
|
82
82
|
// NEW: O(1) reactive calculation 🚀
|
|
83
|
-
let totalHeight = $derived(
|
|
83
|
+
let totalHeight = $derived(heightManager.totalHeight)
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
## Performance Benefits
|
|
@@ -32,7 +32,7 @@ ReactiveListManager processes only **dirty/changed items**:
|
|
|
32
32
|
```typescript
|
|
33
33
|
// ✅ O(dirty items) - Fast and reactive
|
|
34
34
|
manager.processDirtyHeights(changedItems)
|
|
35
|
-
const totalHeight = manager.
|
|
35
|
+
const totalHeight = manager.totalHeight
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## 📦 Installation
|
|
@@ -82,7 +82,7 @@ new ReactiveListManager(config: ListManagerConfig)
|
|
|
82
82
|
**Parameters:**
|
|
83
83
|
|
|
84
84
|
- `config.itemLength` - Total number of items
|
|
85
|
-
- `config.
|
|
85
|
+
- `config.itemHeight` - Default height for unmeasured items
|
|
86
86
|
|
|
87
87
|
### Core Methods
|
|
88
88
|
|
|
@@ -154,7 +154,7 @@ Check if manager has sufficient measurement data.
|
|
|
154
154
|
// Create manager
|
|
155
155
|
const heightManager = new ReactiveListManager({
|
|
156
156
|
itemLength: items.length,
|
|
157
|
-
|
|
157
|
+
itemHeight: defaultEstimatedItemHeight
|
|
158
158
|
})
|
|
159
159
|
|
|
160
160
|
// Update on items change
|
|
@@ -182,7 +182,7 @@ $effect(() => {
|
|
|
182
182
|
})
|
|
183
183
|
|
|
184
184
|
// Reactive total height (automatically updates)
|
|
185
|
-
let totalHeight = $derived(
|
|
185
|
+
let totalHeight = $derived(heightManager.totalHeight)
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
### Standalone Usage
|
|
@@ -190,7 +190,7 @@ let totalHeight = $derived(() => heightManager.totalHeight)
|
|
|
190
190
|
```typescript
|
|
191
191
|
import { ReactiveListManager, benchmarkHeightManager } from './reactive-list-manager'
|
|
192
192
|
|
|
193
|
-
const manager = new ReactiveListManager({ itemLength: 1000,
|
|
193
|
+
const manager = new ReactiveListManager({ itemLength: 1000, itemHeight: 50 })
|
|
194
194
|
|
|
195
195
|
// Performance monitoring
|
|
196
196
|
const results = benchmarkHeightManager(10000, 1000, 100)
|
|
@@ -264,7 +264,7 @@ npm run test -- --grep "Performance Tests"
|
|
|
264
264
|
|
|
265
265
|
```text
|
|
266
266
|
|
|
267
|
-
Height Changes → processDirtyHeights() → Update State →
|
|
267
|
+
Height Changes → processDirtyHeights() → Update State → totalHeight → Reactive UI
|
|
268
268
|
|
|
269
269
|
```
|
|
270
270
|
|
|
@@ -274,7 +274,7 @@ Height Changes → processDirtyHeights() → Update State → getDerivedTotalHei
|
|
|
274
274
|
private _totalMeasuredHeight = $state(0) // Sum of all measured heights
|
|
275
275
|
private _measuredCount = $state(0) // Count of measured items
|
|
276
276
|
private _itemLength = $state(0) // Total items
|
|
277
|
-
private
|
|
277
|
+
private _itemHeight = $state(40) // Default estimate
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
## 🔧 Types
|
|
@@ -288,7 +288,7 @@ interface HeightChange {
|
|
|
288
288
|
|
|
289
289
|
interface ListManagerConfig {
|
|
290
290
|
itemLength: number
|
|
291
|
-
|
|
291
|
+
itemHeight: number
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
interface ListManagerDebugInfo {
|
|
@@ -296,7 +296,7 @@ interface ListManagerDebugInfo {
|
|
|
296
296
|
measuredCount: number
|
|
297
297
|
itemLength: number
|
|
298
298
|
coveragePercent: number
|
|
299
|
-
|
|
299
|
+
itemHeight: number
|
|
300
300
|
}
|
|
301
301
|
```
|
|
302
302
|
|
|
@@ -10,7 +10,7 @@ import type { HeightChange, ListManagerConfig, ListManagerDebugInfo } from './ty
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```typescript
|
|
13
|
-
* const manager = new ReactiveListManager({ itemLength: 10000,
|
|
13
|
+
* const manager = new ReactiveListManager({ itemLength: 10000, itemHeight: 40 })
|
|
14
14
|
*
|
|
15
15
|
* // Process height changes incrementally
|
|
16
16
|
* manager.processDirtyHeights(dirtyResults)
|
|
@@ -10,7 +10,7 @@ import { RecomputeScheduler } from './RecomputeScheduler.js';
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```typescript
|
|
13
|
-
* const manager = new ReactiveListManager({ itemLength: 10000,
|
|
13
|
+
* const manager = new ReactiveListManager({ itemLength: 10000, itemHeight: 40 })
|
|
14
14
|
*
|
|
15
15
|
* // Process height changes incrementally
|
|
16
16
|
* manager.processDirtyHeights(dirtyResults)
|
|
@@ -35,7 +35,7 @@ import type { SvelteVirtualListMode } from '../types.js';
|
|
|
35
35
|
* calculateAverageHeightDebounced(
|
|
36
36
|
* false,
|
|
37
37
|
* null,
|
|
38
|
-
*
|
|
38
|
+
* visibleRange,
|
|
39
39
|
* itemElements,
|
|
40
40
|
* heightCache,
|
|
41
41
|
* lastMeasuredIndex,
|
|
@@ -59,7 +59,7 @@ import type { SvelteVirtualListMode } from '../types.js';
|
|
|
59
59
|
*
|
|
60
60
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
61
61
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
62
|
-
* @param
|
|
62
|
+
* @param visibleItems - Current visible range
|
|
63
63
|
* @param itemElements - Array of DOM elements to measure
|
|
64
64
|
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
65
65
|
* @param lastMeasuredIndex - Index of last measured element
|
|
@@ -68,7 +68,7 @@ import type { SvelteVirtualListMode } from '../types.js';
|
|
|
68
68
|
* @param debounceTime - Time to wait between calculations (default: 200ms)
|
|
69
69
|
* @returns Timeout object or null if calculation was skipped
|
|
70
70
|
*/
|
|
71
|
-
export declare const calculateAverageHeightDebounced: (isCalculatingHeight: boolean, heightUpdateTimeout: ReturnType<typeof setTimeout> | null,
|
|
71
|
+
export declare const calculateAverageHeightDebounced: (isCalculatingHeight: boolean, heightUpdateTimeout: ReturnType<typeof setTimeout> | null, visibleItems: {
|
|
72
72
|
start: number;
|
|
73
73
|
end: number;
|
|
74
74
|
}, itemElements: HTMLElement[], heightCache: Record<number, number>, lastMeasuredIndex: number, calculatedItemHeight: number, onUpdate: (result: {
|
|
@@ -36,7 +36,7 @@ import { BROWSER } from 'esm-env';
|
|
|
36
36
|
* calculateAverageHeightDebounced(
|
|
37
37
|
* false,
|
|
38
38
|
* null,
|
|
39
|
-
*
|
|
39
|
+
* visibleRange,
|
|
40
40
|
* itemElements,
|
|
41
41
|
* heightCache,
|
|
42
42
|
* lastMeasuredIndex,
|
|
@@ -60,7 +60,7 @@ import { BROWSER } from 'esm-env';
|
|
|
60
60
|
*
|
|
61
61
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
62
62
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
63
|
-
* @param
|
|
63
|
+
* @param visibleItems - Current visible range
|
|
64
64
|
* @param itemElements - Array of DOM elements to measure
|
|
65
65
|
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
66
66
|
* @param lastMeasuredIndex - Index of last measured element
|
|
@@ -69,19 +69,18 @@ import { BROWSER } from 'esm-env';
|
|
|
69
69
|
* @param debounceTime - Time to wait between calculations (default: 200ms)
|
|
70
70
|
* @returns Timeout object or null if calculation was skipped
|
|
71
71
|
*/
|
|
72
|
-
export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout,
|
|
72
|
+
export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout, visibleItems, itemElements, heightCache, lastMeasuredIndex, calculatedItemHeight,
|
|
73
73
|
/* trunk-ignore(eslint/no-unused-vars) */
|
|
74
74
|
onUpdate, debounceTime, dirtyItems, currentTotalHeight = 0, currentValidCount = 0, mode = 'topToBottom') => {
|
|
75
75
|
if (!BROWSER || isCalculatingHeight)
|
|
76
76
|
return null;
|
|
77
|
-
const
|
|
78
|
-
const currentIndex = visibleRange.start;
|
|
77
|
+
const currentIndex = visibleItems.start;
|
|
79
78
|
if (currentIndex === lastMeasuredIndex && dirtyItems.size === 0)
|
|
80
79
|
return null;
|
|
81
80
|
if (heightUpdateTimeout)
|
|
82
81
|
clearTimeout(heightUpdateTimeout);
|
|
83
82
|
return setTimeout(() => {
|
|
84
|
-
const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems, newTotalHeight, newValidCount, heightChanges } = calculateAverageHeight(itemElements,
|
|
83
|
+
const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems, newTotalHeight, newValidCount, heightChanges } = calculateAverageHeight(itemElements, visibleItems, heightCache, calculatedItemHeight, dirtyItems, currentTotalHeight, currentValidCount, mode);
|
|
85
84
|
if (Math.abs(newHeight - calculatedItemHeight) > 1 || dirtyItems.size > 0) {
|
|
86
85
|
onUpdate({
|
|
87
86
|
newHeight,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|