@humanspeak/svelte-virtual-list 0.2.5 โ 0.2.6-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/dist/SvelteVirtualList.svelte +349 -80
- package/dist/SvelteVirtualList.svelte.d.ts +78 -31
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +30 -0
- package/dist/types.js +8 -1
- package/dist/utils/heightCalculation.d.ts +7 -7
- package/dist/utils/heightCalculation.js +6 -5
- package/dist/utils/types.d.ts +0 -6
- package/dist/utils/virtualList.d.ts +8 -11
- package/dist/utils/virtualList.js +6 -6
- package/package.json +31 -27
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ A high-performance virtual list component for Svelte 5 applications that efficie
|
|
|
28
28
|
- ๐ง Memory-optimized for 10k+ items
|
|
29
29
|
- ๐งช Comprehensive test coverage (vitest and playwright)
|
|
30
30
|
- ๐ Progressive initialization for large datasets
|
|
31
|
-
- ๐น๏ธ Programmatic scrolling with `
|
|
31
|
+
- ๐น๏ธ Programmatic scrolling with `scroll`
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## scroll: Programmatic Scrolling
|
|
34
34
|
|
|
35
|
-
You can now programmatically scroll to any item in the list using the `
|
|
35
|
+
You can now programmatically scroll to any item in the list using the `scroll` method. This is useful for chat apps, jump-to-item navigation, and more. You can check the usage in `src/routes/tests/scroll`. Thank you for the feature request!
|
|
36
36
|
|
|
37
37
|
### Usage Example
|
|
38
38
|
|
|
@@ -43,8 +43,8 @@ You can now programmatically scroll to any item in the list using the `scrollToI
|
|
|
43
43
|
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }))
|
|
44
44
|
|
|
45
45
|
function goToItem5000() {
|
|
46
|
-
// Scroll to item 5000 with smooth scrolling
|
|
47
|
-
listRef.
|
|
46
|
+
// Scroll to item 5000 with smooth scrolling and auto alignment
|
|
47
|
+
listRef.scroll({ index: 5000, smoothScroll: true, align: 'auto' })
|
|
48
48
|
}
|
|
49
49
|
</script>
|
|
50
50
|
|
|
@@ -58,10 +58,23 @@ You can now programmatically scroll to any item in the list using the `scrollToI
|
|
|
58
58
|
|
|
59
59
|
### API
|
|
60
60
|
|
|
61
|
-
- `
|
|
61
|
+
- `scroll(options: { index: number; smoothScroll?: boolean; shouldThrowOnBounds?: boolean; align?: 'auto' | 'top' | 'bottom' | 'nearest' })`
|
|
62
62
|
- `index`: The item index to scroll to (0-based)
|
|
63
63
|
- `smoothScroll`: If true, uses smooth scrolling (default: true)
|
|
64
64
|
- `shouldThrowOnBounds`: If true, throws if index is out of bounds (default: true)
|
|
65
|
+
- `align`: Where to align the item in the viewport:
|
|
66
|
+
- `'auto'` (default): Only scroll if not visible, align to top or bottom as appropriate
|
|
67
|
+
- `'top'`: Always align to the top
|
|
68
|
+
- `'bottom'`: Always align to the bottom
|
|
69
|
+
- `'nearest'`: Scroll as little as possible to bring the item into view (like native scrollIntoView({ block: 'nearest' }))
|
|
70
|
+
|
|
71
|
+
#### Usage Examples
|
|
72
|
+
|
|
73
|
+
```svelte
|
|
74
|
+
<button on:click={() => listRef.scroll({ index: 5000, align: 'nearest' })}>
|
|
75
|
+
Scroll to item 5000 (nearest)
|
|
76
|
+
</button>
|
|
77
|
+
```
|
|
65
78
|
|
|
66
79
|
## Installation
|
|
67
80
|
|
|
@@ -1,45 +1,63 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
@component
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
@component SvelteVirtualList
|
|
3
|
+
|
|
4
|
+
A high-performance, memory-efficient virtualized list component for Svelte 5.
|
|
5
|
+
Renders only visible items plus a buffer, supporting dynamic item heights,
|
|
6
|
+
bi-directional (top-to-bottom and bottom-to-top) scrolling, and programmatic control.
|
|
7
|
+
|
|
8
|
+
=============================
|
|
9
|
+
== Key Features ==
|
|
10
|
+
=============================
|
|
11
|
+
- Dynamic item height support (no fixed height required)
|
|
12
|
+
- Top-to-bottom and bottom-to-top (chat-style) scrolling
|
|
13
|
+
- Programmatic scrolling with flexible alignment (top, bottom, auto)
|
|
14
|
+
- Smooth scrolling and buffer size configuration
|
|
15
|
+
- SSR compatible and hydration-friendly
|
|
16
|
+
- TypeScript and Svelte 5 runes/snippets support
|
|
17
|
+
- Customizable styling via class props
|
|
18
|
+
- Debug mode for development and testing
|
|
19
|
+
- Optimized for large lists (10k+ items)
|
|
20
|
+
- Comprehensive test coverage (unit and E2E)
|
|
21
|
+
|
|
22
|
+
=============================
|
|
23
|
+
== Usage Example ==
|
|
24
|
+
=============================
|
|
21
25
|
```svelte
|
|
22
26
|
<SvelteVirtualList
|
|
23
27
|
items={data}
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
mode="bottomToTop"
|
|
29
|
+
bind:this={listRef}
|
|
26
30
|
>
|
|
27
|
-
{#snippet renderItem(item
|
|
28
|
-
<div
|
|
31
|
+
{#snippet renderItem(item)}
|
|
32
|
+
<div>{item.text}</div>
|
|
29
33
|
{/snippet}
|
|
30
34
|
</SvelteVirtualList>
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
37
|
+
=============================
|
|
38
|
+
== Architecture Notes ==
|
|
39
|
+
=============================
|
|
40
|
+
- Uses a four-layer DOM structure for optimal performance
|
|
41
|
+
- Only visible items + buffer are mounted in the DOM
|
|
42
|
+
- Height caching and estimation for dynamic content
|
|
43
|
+
- Handles resize events and dynamic content changes
|
|
44
|
+
- Supports chunked initialization for very large lists
|
|
45
|
+
- All scrolling logic is centralized in the scroll() method
|
|
46
|
+
- Bi-directional support: mode="topToBottom" or "bottomToTop"
|
|
47
|
+
- Designed for extensibility and easy debugging
|
|
48
|
+
|
|
49
|
+
=============================
|
|
50
|
+
== For Contributors ==
|
|
51
|
+
=============================
|
|
52
|
+
- Please keep all scrolling logic in the scroll() method
|
|
53
|
+
- Add new features behind feature flags or as optional props
|
|
54
|
+
- Write tests for all new features (see /test and /tests/scroll)
|
|
55
|
+
- Use TypeScript and Svelte 5 runes for all new code
|
|
56
|
+
- Document all exported functions and props with JSDoc
|
|
57
|
+
- See README.md for API and usage details
|
|
58
|
+
- For questions, open an issue or discussion on GitHub
|
|
59
|
+
|
|
60
|
+
MIT License ยฉ Humanspeak, Inc.
|
|
43
61
|
-->
|
|
44
62
|
|
|
45
63
|
<script lang="ts">
|
|
@@ -122,16 +140,21 @@
|
|
|
122
140
|
* - Progressive size adjustment system
|
|
123
141
|
*/
|
|
124
142
|
|
|
125
|
-
import
|
|
143
|
+
import {
|
|
144
|
+
DEFAULT_SCROLL_OPTIONS,
|
|
145
|
+
type SvelteVirtualListPreviousVisibleRange,
|
|
146
|
+
type SvelteVirtualListProps,
|
|
147
|
+
type SvelteVirtualListScrollOptions
|
|
148
|
+
} from './types.js'
|
|
126
149
|
import { calculateAverageHeightDebounced } from './utils/heightCalculation.js'
|
|
127
150
|
import { createRafScheduler } from './utils/raf.js'
|
|
128
151
|
import {
|
|
129
152
|
calculateScrollPosition,
|
|
130
153
|
calculateTransformY,
|
|
131
154
|
calculateVisibleRange,
|
|
155
|
+
getScrollOffsetForIndex,
|
|
132
156
|
processChunked,
|
|
133
|
-
updateHeightAndScroll as utilsUpdateHeightAndScroll
|
|
134
|
-
getScrollOffsetForIndex
|
|
157
|
+
updateHeightAndScroll as utilsUpdateHeightAndScroll
|
|
135
158
|
} from './utils/virtualList.js'
|
|
136
159
|
import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
|
|
137
160
|
import { BROWSER } from 'esm-env'
|
|
@@ -185,37 +208,43 @@
|
|
|
185
208
|
*/
|
|
186
209
|
let heightUpdateTimeout: ReturnType<typeof setTimeout> | null = null // Debounce timer for height updates
|
|
187
210
|
let resizeObserver: ResizeObserver | null = null // Watches for container size changes
|
|
211
|
+
let itemResizeObserver: ResizeObserver | null = null // Watches for individual item size changes
|
|
188
212
|
|
|
189
213
|
/**
|
|
190
214
|
* Performance Optimization State
|
|
191
215
|
*/
|
|
192
216
|
let heightCache = $state<Record<number, number>>({}) // Cache of measured item heights
|
|
217
|
+
let dirtyItems = $state(new Set<number>()) // Set of item indices that need height recalculation
|
|
193
218
|
const chunkSize = $state(50) // Number of items to process in each chunk
|
|
194
219
|
let processedItems = $state(0) // Number of items processed during initialization
|
|
195
220
|
|
|
196
|
-
let prevVisibleRange = $state<
|
|
221
|
+
let prevVisibleRange = $state<SvelteVirtualListPreviousVisibleRange | null>(null)
|
|
197
222
|
let prevHeight = $state<number>(0)
|
|
198
223
|
|
|
199
224
|
// Trigger height calculation when items are rendered
|
|
200
225
|
$effect(() => {
|
|
201
226
|
if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
202
|
-
|
|
203
|
-
isCalculatingHeight,
|
|
204
|
-
heightUpdateTimeout,
|
|
205
|
-
visibleItems,
|
|
206
|
-
itemElements,
|
|
207
|
-
heightCache,
|
|
208
|
-
lastMeasuredIndex,
|
|
209
|
-
calculatedItemHeight,
|
|
210
|
-
(result) => {
|
|
211
|
-
calculatedItemHeight = result.newHeight
|
|
212
|
-
lastMeasuredIndex = result.newLastMeasuredIndex
|
|
213
|
-
heightCache = result.updatedHeightCache
|
|
214
|
-
}
|
|
215
|
-
)
|
|
227
|
+
updateHeight()
|
|
216
228
|
}
|
|
217
229
|
})
|
|
218
230
|
|
|
231
|
+
const updateHeight = () => {
|
|
232
|
+
heightUpdateTimeout = calculateAverageHeightDebounced(
|
|
233
|
+
isCalculatingHeight,
|
|
234
|
+
heightUpdateTimeout,
|
|
235
|
+
visibleItems,
|
|
236
|
+
itemElements,
|
|
237
|
+
heightCache,
|
|
238
|
+
lastMeasuredIndex,
|
|
239
|
+
calculatedItemHeight,
|
|
240
|
+
(result) => {
|
|
241
|
+
calculatedItemHeight = result.newHeight
|
|
242
|
+
lastMeasuredIndex = result.newLastMeasuredIndex
|
|
243
|
+
heightCache = result.updatedHeightCache
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
219
248
|
// Add new effect to handle height changes
|
|
220
249
|
$effect(() => {
|
|
221
250
|
if (BROWSER && initialized && mode === 'bottomToTop' && viewportElement) {
|
|
@@ -291,10 +320,10 @@
|
|
|
291
320
|
* console.log(`Rendering items from ${range.start} to ${range.end}`)
|
|
292
321
|
* ```
|
|
293
322
|
*
|
|
294
|
-
* @returns {
|
|
323
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Object containing start and end indices of visible items
|
|
295
324
|
*/
|
|
296
|
-
const visibleItems = $derived(() => {
|
|
297
|
-
if (!items.length) return { start: 0, end: 0 }
|
|
325
|
+
const visibleItems = $derived((): SvelteVirtualListPreviousVisibleRange => {
|
|
326
|
+
if (!items.length) return { start: 0, end: 0 } as SvelteVirtualListPreviousVisibleRange
|
|
298
327
|
const viewportHeight = height || 0
|
|
299
328
|
|
|
300
329
|
return calculateVisibleRange(
|
|
@@ -460,6 +489,44 @@
|
|
|
460
489
|
}
|
|
461
490
|
})
|
|
462
491
|
|
|
492
|
+
// Create itemResizeObserver immediately when in browser
|
|
493
|
+
if (BROWSER) {
|
|
494
|
+
// Watch for individual item size changes
|
|
495
|
+
itemResizeObserver = new ResizeObserver((entries) => {
|
|
496
|
+
let shouldRecalculate = false
|
|
497
|
+
|
|
498
|
+
if (debug) {
|
|
499
|
+
console.log(`ResizeObserver fired for ${entries.length} entries`)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
const element = entry.target as HTMLElement
|
|
504
|
+
const elementIndex = itemElements.indexOf(element)
|
|
505
|
+
|
|
506
|
+
if (elementIndex !== -1) {
|
|
507
|
+
const actualIndex = visibleItems().start + elementIndex
|
|
508
|
+
|
|
509
|
+
// ResizeObserver fired = element resized, so add to dirty queue
|
|
510
|
+
dirtyItems.add(actualIndex)
|
|
511
|
+
shouldRecalculate = true
|
|
512
|
+
|
|
513
|
+
if (debug) {
|
|
514
|
+
console.log(
|
|
515
|
+
`Item ${actualIndex} marked dirty (resized), queue size: ${dirtyItems.size}`
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (shouldRecalculate) {
|
|
522
|
+
// Trigger virtual list recalculation
|
|
523
|
+
rafSchedule(() => {
|
|
524
|
+
updateHeight()
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
|
|
463
530
|
// Setup and cleanup
|
|
464
531
|
onMount(() => {
|
|
465
532
|
if (BROWSER) {
|
|
@@ -480,6 +547,9 @@
|
|
|
480
547
|
if (resizeObserver) {
|
|
481
548
|
resizeObserver.disconnect()
|
|
482
549
|
}
|
|
550
|
+
if (itemResizeObserver) {
|
|
551
|
+
itemResizeObserver.disconnect()
|
|
552
|
+
}
|
|
483
553
|
}
|
|
484
554
|
}
|
|
485
555
|
})
|
|
@@ -495,6 +565,9 @@
|
|
|
495
565
|
/**
|
|
496
566
|
* Scrolls the virtual list to the item at the given index.
|
|
497
567
|
*
|
|
568
|
+
* @deprecated This function is deprecated and will be removed in a future version.
|
|
569
|
+
* Use the new scroll method from the component instance instead.
|
|
570
|
+
*
|
|
498
571
|
* @function scrollToIndex
|
|
499
572
|
* @param index The index of the item to scroll to.
|
|
500
573
|
* @param smoothScroll (default: true) Whether to use smooth scrolling.
|
|
@@ -525,49 +598,239 @@
|
|
|
525
598
|
smoothScroll = true,
|
|
526
599
|
shouldThrowOnBounds = true
|
|
527
600
|
): void => {
|
|
601
|
+
// Deprecation warning
|
|
602
|
+
console.warn(
|
|
603
|
+
'SvelteVirtualList: scrollToIndex is deprecated and will be removed in a future version. ' +
|
|
604
|
+
'Use the new scroll method from the component instance instead.'
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
// Call the new scroll function with the provided parameters
|
|
608
|
+
scroll({ index, smoothScroll, shouldThrowOnBounds })
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Scrolls the virtual list to the item at the given index using a type-based options approach.
|
|
613
|
+
*
|
|
614
|
+
* @function scroll
|
|
615
|
+
* @param options Configuration options for scrolling behavior.
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* // Svelte usage:
|
|
619
|
+
* // In your <script> block:
|
|
620
|
+
* import SvelteVirtualList from './index.js';
|
|
621
|
+
* let virtualList;
|
|
622
|
+
* const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
|
|
623
|
+
*
|
|
624
|
+
* <button onclick={() => virtualList.scroll({ index: 5000 })}>
|
|
625
|
+
* Scroll to 5000
|
|
626
|
+
* </button>
|
|
627
|
+
* <SvelteVirtualList {items} bind:this={virtualList}>
|
|
628
|
+
* {#snippet renderItem(item)}
|
|
629
|
+
* <div>{item.text}</div>
|
|
630
|
+
* {/snippet}
|
|
631
|
+
* </SvelteVirtualList>
|
|
632
|
+
*
|
|
633
|
+
* @returns {void}
|
|
634
|
+
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
635
|
+
*/
|
|
636
|
+
export const scroll = (options: SvelteVirtualListScrollOptions): void => {
|
|
637
|
+
const { index, smoothScroll, shouldThrowOnBounds, align } = {
|
|
638
|
+
...DEFAULT_SCROLL_OPTIONS,
|
|
639
|
+
...options
|
|
640
|
+
}
|
|
641
|
+
|
|
528
642
|
if (!items.length) return
|
|
529
643
|
if (!viewportElement) {
|
|
530
644
|
tick().then(() => {
|
|
531
645
|
if (!viewportElement) return
|
|
532
|
-
|
|
646
|
+
scroll({ index, smoothScroll, shouldThrowOnBounds, align })
|
|
533
647
|
})
|
|
534
648
|
return
|
|
535
649
|
}
|
|
536
|
-
doScroll()
|
|
537
650
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (
|
|
651
|
+
// Bounds checking
|
|
652
|
+
let targetIndex = index
|
|
653
|
+
if (targetIndex < 0 || targetIndex >= items.length) {
|
|
654
|
+
if (shouldThrowOnBounds) {
|
|
542
655
|
throw new Error(
|
|
543
|
-
`
|
|
656
|
+
`scroll: index ${targetIndex} is out of bounds (0-${items.length - 1})`
|
|
544
657
|
)
|
|
658
|
+
} else {
|
|
659
|
+
targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1))
|
|
545
660
|
}
|
|
546
|
-
|
|
547
|
-
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems()
|
|
664
|
+
let scrollTarget: number | null = null
|
|
665
|
+
|
|
666
|
+
if (mode === 'bottomToTop') {
|
|
667
|
+
const totalHeight = items.length * calculatedItemHeight
|
|
668
|
+
const itemOffset = targetIndex * calculatedItemHeight
|
|
669
|
+
const itemHeight = calculatedItemHeight
|
|
670
|
+
if (align === 'auto') {
|
|
671
|
+
// If item is above the viewport, align to top
|
|
672
|
+
if (targetIndex < firstVisibleIndex) {
|
|
673
|
+
scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
|
|
674
|
+
// If item is below the viewport, align to bottom
|
|
675
|
+
} else if (targetIndex > lastVisibleIndex - 1) {
|
|
676
|
+
scrollTarget = Math.max(0, totalHeight - itemOffset - height)
|
|
677
|
+
} else {
|
|
678
|
+
// Item is visible but not aligned: align to nearest edge
|
|
679
|
+
// Calculate the offset of the item relative to the viewport
|
|
680
|
+
const itemTop = totalHeight - (itemOffset + itemHeight)
|
|
681
|
+
const itemBottom = totalHeight - itemOffset
|
|
682
|
+
const distanceToTop = Math.abs(scrollTop - itemTop)
|
|
683
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
|
|
684
|
+
if (distanceToTop < distanceToBottom) {
|
|
685
|
+
// Closer to top, align to top
|
|
686
|
+
scrollTarget = itemTop
|
|
687
|
+
} else {
|
|
688
|
+
// Closer to bottom, align to bottom
|
|
689
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
} else if (align === 'top') {
|
|
693
|
+
// Align to top
|
|
694
|
+
scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
|
|
695
|
+
} else if (align === 'bottom') {
|
|
696
|
+
// Align to bottom
|
|
697
|
+
scrollTarget = Math.max(0, totalHeight - itemOffset - height)
|
|
698
|
+
} else if (align === 'nearest') {
|
|
699
|
+
// If not visible, align to nearest edge; if visible, do nothing
|
|
700
|
+
const itemTop = totalHeight - (itemOffset + itemHeight)
|
|
701
|
+
const itemBottom = totalHeight - itemOffset
|
|
702
|
+
if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
|
|
703
|
+
// Not visible, align to nearest edge
|
|
704
|
+
const distanceToTop = Math.abs(scrollTop - itemTop)
|
|
705
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
|
|
706
|
+
if (distanceToTop < distanceToBottom) {
|
|
707
|
+
scrollTarget = itemTop
|
|
708
|
+
} else {
|
|
709
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
// Already visible, do nothing
|
|
713
|
+
return
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
// topToBottom (default)
|
|
718
|
+
if (align === 'auto') {
|
|
719
|
+
// If item is above the viewport, align to top
|
|
720
|
+
if (targetIndex < firstVisibleIndex) {
|
|
721
|
+
scrollTarget = getScrollOffsetForIndex(
|
|
722
|
+
heightCache,
|
|
723
|
+
calculatedItemHeight,
|
|
724
|
+
targetIndex
|
|
725
|
+
)
|
|
726
|
+
// If item is below the viewport, align to bottom
|
|
727
|
+
} else if (targetIndex > lastVisibleIndex - 1) {
|
|
728
|
+
const itemBottom = getScrollOffsetForIndex(
|
|
729
|
+
heightCache,
|
|
730
|
+
calculatedItemHeight,
|
|
731
|
+
targetIndex + 1
|
|
732
|
+
)
|
|
733
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
734
|
+
} else {
|
|
735
|
+
// Item is visible but not aligned: align to nearest edge
|
|
736
|
+
const itemTop = getScrollOffsetForIndex(
|
|
737
|
+
heightCache,
|
|
738
|
+
calculatedItemHeight,
|
|
739
|
+
targetIndex
|
|
740
|
+
)
|
|
741
|
+
const itemBottom = getScrollOffsetForIndex(
|
|
742
|
+
heightCache,
|
|
743
|
+
calculatedItemHeight,
|
|
744
|
+
targetIndex + 1
|
|
745
|
+
)
|
|
746
|
+
const distanceToTop = Math.abs(scrollTop - itemTop)
|
|
747
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
|
|
748
|
+
if (distanceToTop < distanceToBottom) {
|
|
749
|
+
// Closer to top, align to top
|
|
750
|
+
scrollTarget = itemTop
|
|
751
|
+
} else {
|
|
752
|
+
// Closer to bottom, align to bottom
|
|
753
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} else if (align === 'top') {
|
|
757
|
+
scrollTarget = getScrollOffsetForIndex(
|
|
548
758
|
heightCache,
|
|
549
759
|
calculatedItemHeight,
|
|
550
|
-
|
|
760
|
+
targetIndex
|
|
551
761
|
)
|
|
552
|
-
|
|
553
|
-
top: scrollTopTarget,
|
|
554
|
-
behavior: smoothScroll ? 'smooth' : 'auto'
|
|
555
|
-
})
|
|
556
|
-
} else if (mode === 'bottomToTop') {
|
|
557
|
-
// Invert the index for reversed rendering
|
|
558
|
-
const reversedIndex = items.length - 1 - clampedIndex
|
|
762
|
+
} else if (align === 'bottom') {
|
|
559
763
|
const itemBottom = getScrollOffsetForIndex(
|
|
560
764
|
heightCache,
|
|
561
765
|
calculatedItemHeight,
|
|
562
|
-
|
|
766
|
+
targetIndex + 1
|
|
767
|
+
)
|
|
768
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
769
|
+
} else if (align === 'nearest') {
|
|
770
|
+
const itemTop = getScrollOffsetForIndex(
|
|
771
|
+
heightCache,
|
|
772
|
+
calculatedItemHeight,
|
|
773
|
+
targetIndex
|
|
774
|
+
)
|
|
775
|
+
const itemBottom = getScrollOffsetForIndex(
|
|
776
|
+
heightCache,
|
|
777
|
+
calculatedItemHeight,
|
|
778
|
+
targetIndex + 1
|
|
779
|
+
)
|
|
780
|
+
if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
|
|
781
|
+
// Not visible, align to nearest edge
|
|
782
|
+
const distanceToTop = Math.abs(scrollTop - itemTop)
|
|
783
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
|
|
784
|
+
if (distanceToTop < distanceToBottom) {
|
|
785
|
+
scrollTarget = itemTop
|
|
786
|
+
} else {
|
|
787
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
// Already visible, do nothing
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (scrollTarget !== null) {
|
|
797
|
+
viewportElement.scrollTo({
|
|
798
|
+
top: scrollTarget,
|
|
799
|
+
behavior: smoothScroll ? 'smooth' : 'auto'
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Custom Svelte action to automatically observe item elements for size changes.
|
|
806
|
+
* This action is applied to each item element to detect when its dimensions change.
|
|
807
|
+
*
|
|
808
|
+
* @param element - The HTML element to observe
|
|
809
|
+
* @returns {{ destroy: () => void }} Object with destroy method for cleanup
|
|
810
|
+
*/
|
|
811
|
+
function autoObserveItemResize(element: HTMLElement) {
|
|
812
|
+
if (itemResizeObserver) {
|
|
813
|
+
itemResizeObserver.observe(element)
|
|
814
|
+
if (debug) {
|
|
815
|
+
console.log(
|
|
816
|
+
'Started observing element:',
|
|
817
|
+
element,
|
|
818
|
+
'Current height:',
|
|
819
|
+
element.getBoundingClientRect().height
|
|
563
820
|
)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
821
|
+
}
|
|
822
|
+
} else if (debug) {
|
|
823
|
+
console.log('itemResizeObserver not available for element:', element)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
destroy() {
|
|
828
|
+
if (itemResizeObserver) {
|
|
829
|
+
itemResizeObserver.unobserve(element)
|
|
830
|
+
if (debug) {
|
|
831
|
+
console.log('Stopped observing element:', element)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
571
834
|
}
|
|
572
835
|
}
|
|
573
836
|
}
|
|
@@ -630,7 +893,7 @@
|
|
|
630
893
|
: console.info('Virtual List Debug:', debugInfo)}
|
|
631
894
|
{/if}
|
|
632
895
|
<!-- Render each visible item -->
|
|
633
|
-
<div bind:this={itemElements[i]}>
|
|
896
|
+
<div bind:this={itemElements[i]} use:autoObserveItemResize>
|
|
634
897
|
{@render renderItem(
|
|
635
898
|
currentItem,
|
|
636
899
|
mode === 'bottomToTop'
|
|
@@ -678,4 +941,10 @@
|
|
|
678
941
|
left: 0;
|
|
679
942
|
top: 0;
|
|
680
943
|
}
|
|
944
|
+
|
|
945
|
+
/* Item wrapper divs should size to their content */
|
|
946
|
+
.virtual-list-items > div {
|
|
947
|
+
width: 100%;
|
|
948
|
+
display: block;
|
|
949
|
+
}
|
|
681
950
|
</style>
|
|
@@ -76,53 +76,75 @@
|
|
|
76
76
|
* - Height caching and estimation system
|
|
77
77
|
* - Progressive size adjustment system
|
|
78
78
|
*/
|
|
79
|
-
import type
|
|
79
|
+
import { type SvelteVirtualListProps, type SvelteVirtualListScrollOptions } from './types.js';
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
* by only mounting DOM nodes for visible items and a small buffer. Optimized for handling
|
|
83
|
-
* lists of 10k+ items through chunked processing and progressive initialization.
|
|
81
|
+
* SvelteVirtualList
|
|
84
82
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* -
|
|
88
|
-
* - `mode` - Scroll direction: 'topToBottom' or 'bottomToTop' (default: 'topToBottom')
|
|
89
|
-
* - `debug` - Enable debug logging (default: false)
|
|
90
|
-
* - `bufferSize` - Number of items to render outside visible area (default: 20)
|
|
91
|
-
* - `containerClass` - Custom class for container element
|
|
92
|
-
* - `viewportClass` - Custom class for viewport element
|
|
93
|
-
* - `contentClass` - Custom class for content wrapper
|
|
94
|
-
* - `itemsClass` - Custom class for items wrapper
|
|
95
|
-
* - `debugFunction` - Custom debug logging function
|
|
96
|
-
* - `testId` - Base test ID for component elements
|
|
83
|
+
* A high-performance, memory-efficient virtualized list component for Svelte 5.
|
|
84
|
+
* Renders only visible items plus a buffer, supporting dynamic item heights,
|
|
85
|
+
* bi-directional (top-to-bottom and bottom-to-top) scrolling, and programmatic control.
|
|
97
86
|
*
|
|
98
|
-
*
|
|
87
|
+
* =============================
|
|
88
|
+
* == Key Features ==
|
|
89
|
+
* =============================
|
|
90
|
+
* - Dynamic item height support (no fixed height required)
|
|
91
|
+
* - Top-to-bottom and bottom-to-top (chat-style) scrolling
|
|
92
|
+
* - Programmatic scrolling with flexible alignment (top, bottom, auto)
|
|
93
|
+
* - Smooth scrolling and buffer size configuration
|
|
94
|
+
* - SSR compatible and hydration-friendly
|
|
95
|
+
* - TypeScript and Svelte 5 runes/snippets support
|
|
96
|
+
* - Customizable styling via class props
|
|
97
|
+
* - Debug mode for development and testing
|
|
98
|
+
* - Optimized for large lists (10k+ items)
|
|
99
|
+
* - Comprehensive test coverage (unit and E2E)
|
|
100
|
+
*
|
|
101
|
+
* =============================
|
|
102
|
+
* == Usage Example ==
|
|
103
|
+
* =============================
|
|
99
104
|
* ```svelte
|
|
100
105
|
* <SvelteVirtualList
|
|
101
106
|
* items={data}
|
|
102
|
-
*
|
|
103
|
-
*
|
|
107
|
+
* mode="bottomToTop"
|
|
108
|
+
* bind:this={listRef}
|
|
104
109
|
* >
|
|
105
|
-
* {#snippet renderItem(item
|
|
106
|
-
* <div
|
|
110
|
+
* {#snippet renderItem(item)}
|
|
111
|
+
* <div>{item.text}</div>
|
|
107
112
|
* {/snippet}
|
|
108
113
|
* </SvelteVirtualList>
|
|
109
114
|
* ```
|
|
110
115
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* -
|
|
115
|
-
* -
|
|
116
|
-
* -
|
|
117
|
-
* -
|
|
118
|
-
* -
|
|
119
|
-
* -
|
|
120
|
-
* -
|
|
116
|
+
* =============================
|
|
117
|
+
* == Architecture Notes ==
|
|
118
|
+
* =============================
|
|
119
|
+
* - Uses a four-layer DOM structure for optimal performance
|
|
120
|
+
* - Only visible items + buffer are mounted in the DOM
|
|
121
|
+
* - Height caching and estimation for dynamic content
|
|
122
|
+
* - Handles resize events and dynamic content changes
|
|
123
|
+
* - Supports chunked initialization for very large lists
|
|
124
|
+
* - All scrolling logic is centralized in the scroll() method
|
|
125
|
+
* - Bi-directional support: mode="topToBottom" or "bottomToTop"
|
|
126
|
+
* - Designed for extensibility and easy debugging
|
|
127
|
+
*
|
|
128
|
+
* =============================
|
|
129
|
+
* == For Contributors ==
|
|
130
|
+
* =============================
|
|
131
|
+
* - Please keep all scrolling logic in the scroll() method
|
|
132
|
+
* - Add new features behind feature flags or as optional props
|
|
133
|
+
* - Write tests for all new features (see /test and /tests/scroll)
|
|
134
|
+
* - Use TypeScript and Svelte 5 runes for all new code
|
|
135
|
+
* - Document all exported functions and props with JSDoc
|
|
136
|
+
* - See README.md for API and usage details
|
|
137
|
+
* - For questions, open an issue or discussion on GitHub
|
|
138
|
+
*
|
|
139
|
+
* MIT License ยฉ Humanspeak, Inc.
|
|
121
140
|
*/
|
|
122
141
|
declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {
|
|
123
142
|
/**
|
|
124
143
|
* Scrolls the virtual list to the item at the given index.
|
|
125
144
|
*
|
|
145
|
+
* @deprecated This function is deprecated and will be removed in a future version.
|
|
146
|
+
* Use the new scroll method from the component instance instead.
|
|
147
|
+
*
|
|
126
148
|
* @function scrollToIndex
|
|
127
149
|
* @param index The index of the item to scroll to.
|
|
128
150
|
* @param smoothScroll (default: true) Whether to use smooth scrolling.
|
|
@@ -148,6 +170,31 @@ declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListPro
|
|
|
148
170
|
* @returns {void}
|
|
149
171
|
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
150
172
|
*/ scrollToIndex: (index: number, smoothScroll?: boolean, shouldThrowOnBounds?: boolean) => void;
|
|
173
|
+
/**
|
|
174
|
+
* Scrolls the virtual list to the item at the given index using a type-based options approach.
|
|
175
|
+
*
|
|
176
|
+
* @function scroll
|
|
177
|
+
* @param options Configuration options for scrolling behavior.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* // Svelte usage:
|
|
181
|
+
* // In your <script> block:
|
|
182
|
+
* import SvelteVirtualList from './index.js';
|
|
183
|
+
* let virtualList;
|
|
184
|
+
* const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
|
|
185
|
+
*
|
|
186
|
+
* <button onclick={() => virtualList.scroll({ index: 5000 })}>
|
|
187
|
+
* Scroll to 5000
|
|
188
|
+
* </button>
|
|
189
|
+
* <SvelteVirtualList {items} bind:this={virtualList}>
|
|
190
|
+
* {#snippet renderItem(item)}
|
|
191
|
+
* <div>{item.text}</div>
|
|
192
|
+
* {/snippet}
|
|
193
|
+
* </SvelteVirtualList>
|
|
194
|
+
*
|
|
195
|
+
* @returns {void}
|
|
196
|
+
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
197
|
+
*/ scroll: (options: SvelteVirtualListScrollOptions) => void;
|
|
151
198
|
}, "">;
|
|
152
199
|
type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
|
|
153
200
|
export default SvelteVirtualList;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import SvelteVirtualList from './SvelteVirtualList.svelte';
|
|
2
|
-
import type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps } from './types.js';
|
|
2
|
+
import type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps, SvelteVirtualListScrollAlign, SvelteVirtualListScrollOptions } from './types.js';
|
|
3
3
|
export default SvelteVirtualList;
|
|
4
|
-
export type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps };
|
|
4
|
+
export type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps, SvelteVirtualListScrollAlign, SvelteVirtualListScrollOptions };
|
package/dist/types.d.ts
CHANGED
|
@@ -82,3 +82,33 @@ export type SvelteVirtualListDebugInfo = {
|
|
|
82
82
|
processedItems: number;
|
|
83
83
|
averageItemHeight: number;
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Alignment options for programmatic scrolling.
|
|
87
|
+
*/
|
|
88
|
+
export type SvelteVirtualListScrollAlign = 'auto' | 'top' | 'bottom' | 'nearest';
|
|
89
|
+
/**
|
|
90
|
+
* Options for scrolling to a specific index in the virtual list.
|
|
91
|
+
*/
|
|
92
|
+
export interface SvelteVirtualListScrollOptions {
|
|
93
|
+
/** The index of the item to scroll to. */
|
|
94
|
+
index: number;
|
|
95
|
+
/** Whether to use smooth scrolling animation. Default: true */
|
|
96
|
+
smoothScroll?: boolean;
|
|
97
|
+
/** Whether to throw an error if the index is out of bounds. Default: true */
|
|
98
|
+
shouldThrowOnBounds?: boolean;
|
|
99
|
+
/** Alignment for the scrolled item: 'auto', 'top', or 'bottom'. Default: 'auto' */
|
|
100
|
+
align?: SvelteVirtualListScrollAlign;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Default options for scrolling.
|
|
104
|
+
*/
|
|
105
|
+
export declare const DEFAULT_SCROLL_OPTIONS: Partial<SvelteVirtualListScrollOptions>;
|
|
106
|
+
export type SvelteVirtualListHeightCacheItem = {
|
|
107
|
+
currentHeight: number;
|
|
108
|
+
dirty: boolean;
|
|
109
|
+
};
|
|
110
|
+
export type SvelteVirtualListHeightCache = Record<number, SvelteVirtualListHeightCacheItem>;
|
|
111
|
+
export type SvelteVirtualListPreviousVisibleRange = {
|
|
112
|
+
start: number;
|
|
113
|
+
end: number;
|
|
114
|
+
};
|
package/dist/types.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import type { HeightCache } from './types.js';
|
|
2
1
|
/**
|
|
3
2
|
* Calculates and updates the average height of visible items with debouncing.
|
|
4
3
|
*
|
|
5
4
|
* This function optimizes performance by:
|
|
6
5
|
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
7
|
-
* - Caching item heights to minimize recalculations
|
|
6
|
+
* - Caching item heights with dirty tracking to minimize recalculations
|
|
8
7
|
* - Only updating when significant changes are detected (>1px difference)
|
|
9
8
|
* - Early returns to prevent unnecessary processing
|
|
10
9
|
*
|
|
11
10
|
* Implementation details:
|
|
12
11
|
* - Uses a debounce timeout to batch height calculations
|
|
13
12
|
* - Tracks calculation state to prevent concurrent updates
|
|
14
|
-
* - Caches heights in heightCache for reuse
|
|
13
|
+
* - Caches heights in heightCache with currentHeight and dirty flags for reuse
|
|
15
14
|
* - Validates browser environment and calculation state
|
|
16
15
|
* - Checks for meaningful height changes before updates
|
|
17
16
|
*
|
|
18
17
|
* State interactions:
|
|
19
18
|
* - Updates calculatedItemHeight when significant changes occur
|
|
20
19
|
* - Updates lastMeasuredIndex to track progress
|
|
21
|
-
* - Modifies heightCache to store measured heights
|
|
20
|
+
* - Modifies heightCache to store measured heights with dirty tracking
|
|
22
21
|
* - Uses isCalculatingHeight flag for concurrency control
|
|
23
22
|
*
|
|
24
23
|
* Guard clauses:
|
|
@@ -54,13 +53,14 @@ import type { HeightCache } from './types.js';
|
|
|
54
53
|
* - Enhanced debounce timing precision
|
|
55
54
|
* - Added proper cleanup for timeouts
|
|
56
55
|
* - Documented all edge cases and failure modes
|
|
56
|
+
* - Updated to work with new HeightCache structure with dirty tracking
|
|
57
57
|
*
|
|
58
58
|
*
|
|
59
59
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
60
60
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
61
61
|
* @param visibleItemsGetter - Function to get current visible range
|
|
62
62
|
* @param itemElements - Array of DOM elements to measure
|
|
63
|
-
* @param heightCache - Cache of previously measured heights
|
|
63
|
+
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
64
64
|
* @param lastMeasuredIndex - Index of last measured element
|
|
65
65
|
* @param calculatedItemHeight - Current average height
|
|
66
66
|
* @param onUpdate - Callback for height updates
|
|
@@ -70,8 +70,8 @@ import type { HeightCache } from './types.js';
|
|
|
70
70
|
export declare const calculateAverageHeightDebounced: (isCalculatingHeight: boolean, heightUpdateTimeout: ReturnType<typeof setTimeout> | null, visibleItemsGetter: () => {
|
|
71
71
|
start: number;
|
|
72
72
|
end: number;
|
|
73
|
-
}, itemElements: HTMLElement[], heightCache:
|
|
73
|
+
}, itemElements: HTMLElement[], heightCache: Record<number, number>, lastMeasuredIndex: number, calculatedItemHeight: number, onUpdate: (result: {
|
|
74
74
|
newHeight: number;
|
|
75
75
|
newLastMeasuredIndex: number;
|
|
76
|
-
updatedHeightCache:
|
|
76
|
+
updatedHeightCache: Record<number, number>;
|
|
77
77
|
}) => void, debounceTime?: number) => NodeJS.Timeout | null;
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { BROWSER } from 'esm-env';
|
|
2
1
|
import { calculateAverageHeight } from './virtualList.js';
|
|
2
|
+
import { BROWSER } from 'esm-env';
|
|
3
3
|
/**
|
|
4
4
|
* Calculates and updates the average height of visible items with debouncing.
|
|
5
5
|
*
|
|
6
6
|
* This function optimizes performance by:
|
|
7
7
|
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
8
|
-
* - Caching item heights to minimize recalculations
|
|
8
|
+
* - Caching item heights with dirty tracking to minimize recalculations
|
|
9
9
|
* - Only updating when significant changes are detected (>1px difference)
|
|
10
10
|
* - Early returns to prevent unnecessary processing
|
|
11
11
|
*
|
|
12
12
|
* Implementation details:
|
|
13
13
|
* - Uses a debounce timeout to batch height calculations
|
|
14
14
|
* - Tracks calculation state to prevent concurrent updates
|
|
15
|
-
* - Caches heights in heightCache for reuse
|
|
15
|
+
* - Caches heights in heightCache with currentHeight and dirty flags for reuse
|
|
16
16
|
* - Validates browser environment and calculation state
|
|
17
17
|
* - Checks for meaningful height changes before updates
|
|
18
18
|
*
|
|
19
19
|
* State interactions:
|
|
20
20
|
* - Updates calculatedItemHeight when significant changes occur
|
|
21
21
|
* - Updates lastMeasuredIndex to track progress
|
|
22
|
-
* - Modifies heightCache to store measured heights
|
|
22
|
+
* - Modifies heightCache to store measured heights with dirty tracking
|
|
23
23
|
* - Uses isCalculatingHeight flag for concurrency control
|
|
24
24
|
*
|
|
25
25
|
* Guard clauses:
|
|
@@ -55,13 +55,14 @@ import { calculateAverageHeight } from './virtualList.js';
|
|
|
55
55
|
* - Enhanced debounce timing precision
|
|
56
56
|
* - Added proper cleanup for timeouts
|
|
57
57
|
* - Documented all edge cases and failure modes
|
|
58
|
+
* - Updated to work with new HeightCache structure with dirty tracking
|
|
58
59
|
*
|
|
59
60
|
*
|
|
60
61
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
61
62
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
62
63
|
* @param visibleItemsGetter - Function to get current visible range
|
|
63
64
|
* @param itemElements - Array of DOM elements to measure
|
|
64
|
-
* @param heightCache - Cache of previously measured heights
|
|
65
|
+
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
65
66
|
* @param lastMeasuredIndex - Index of last measured element
|
|
66
67
|
* @param calculatedItemHeight - Current average height
|
|
67
68
|
* @param onUpdate - Callback for height updates
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -39,9 +39,3 @@ export type VirtualListSetters = {
|
|
|
39
39
|
setScrollTop: (scrollTop: number) => void;
|
|
40
40
|
setInitialized: (initialized: boolean) => void;
|
|
41
41
|
};
|
|
42
|
-
/**
|
|
43
|
-
* Cache for storing measured item heights
|
|
44
|
-
* - Key: Item index in the list
|
|
45
|
-
* - Value: Measured height in pixels
|
|
46
|
-
*/
|
|
47
|
-
export type HeightCache = Record<number, number>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SvelteVirtualListMode } from '../types.js';
|
|
1
|
+
import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
|
|
2
2
|
import type { VirtualListSetters, VirtualListState } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Calculates the maximum scroll position for a virtual list.
|
|
@@ -26,12 +26,9 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
|
|
|
26
26
|
* @param {number} totalItems - Total number of items in the list
|
|
27
27
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
28
28
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
29
|
-
* @returns {
|
|
29
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
30
30
|
*/
|
|
31
|
-
export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) =>
|
|
32
|
-
start: number;
|
|
33
|
-
end: number;
|
|
34
|
-
};
|
|
31
|
+
export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => SvelteVirtualListPreviousVisibleRange;
|
|
35
32
|
/**
|
|
36
33
|
* Calculates the CSS transform value for positioning the virtual list items.
|
|
37
34
|
*
|
|
@@ -64,19 +61,19 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
64
61
|
* Calculates the average height of visible items in a virtual list.
|
|
65
62
|
*
|
|
66
63
|
* This function optimizes performance by:
|
|
67
|
-
* 1. Using a height cache to store measured item heights
|
|
64
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
68
65
|
* 2. Only measuring new items not in the cache
|
|
69
66
|
* 3. Calculating a running average of all measured heights
|
|
70
67
|
*
|
|
71
68
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
72
69
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
73
|
-
* @param {
|
|
70
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
74
71
|
* @param {number} currentItemHeight - Current average item height being used
|
|
75
72
|
*
|
|
76
73
|
* @returns {{
|
|
77
74
|
* newHeight: number,
|
|
78
75
|
* newLastMeasuredIndex: number,
|
|
79
|
-
* updatedHeightCache:
|
|
76
|
+
* updatedHeightCache: HeightCache
|
|
80
77
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
81
78
|
*
|
|
82
79
|
* @example
|
|
@@ -125,7 +122,7 @@ onComplete: () => void) => Promise<void>;
|
|
|
125
122
|
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
126
123
|
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
127
124
|
*
|
|
128
|
-
* @param {
|
|
125
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
129
126
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
130
127
|
* @param {number} totalItems - Total number of items in the list
|
|
131
128
|
* @param {number} blockSize - Number of items per block
|
|
@@ -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,7 +29,7 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
|
|
|
29
29
|
* @param {number} totalItems - Total number of items in the list
|
|
30
30
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
31
31
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
32
|
-
* @returns {
|
|
32
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
33
33
|
*/
|
|
34
34
|
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
|
|
35
35
|
if (mode === 'bottomToTop') {
|
|
@@ -101,19 +101,19 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
101
101
|
* Calculates the average height of visible items in a virtual list.
|
|
102
102
|
*
|
|
103
103
|
* This function optimizes performance by:
|
|
104
|
-
* 1. Using a height cache to store measured item heights
|
|
104
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
105
105
|
* 2. Only measuring new items not in the cache
|
|
106
106
|
* 3. Calculating a running average of all measured heights
|
|
107
107
|
*
|
|
108
108
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
109
109
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
110
|
-
* @param {
|
|
110
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
111
111
|
* @param {number} currentItemHeight - Current average item height being used
|
|
112
112
|
*
|
|
113
113
|
* @returns {{
|
|
114
114
|
* newHeight: number,
|
|
115
115
|
* newLastMeasuredIndex: number,
|
|
116
|
-
* updatedHeightCache:
|
|
116
|
+
* updatedHeightCache: HeightCache
|
|
117
117
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
118
118
|
*
|
|
119
119
|
* @example
|
|
@@ -206,7 +206,7 @@ onComplete) => {
|
|
|
206
206
|
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
207
207
|
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
208
208
|
*
|
|
209
|
-
* @param {
|
|
209
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
210
210
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
211
211
|
* @param {number} totalItems - Total number of items in the list
|
|
212
212
|
* @param {number} blockSize - Number of items per block
|
|
@@ -236,7 +236,7 @@ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, bl
|
|
|
236
236
|
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
237
237
|
* - For small indices, falls back to the original logic.
|
|
238
238
|
*
|
|
239
|
-
* @param {
|
|
239
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
240
240
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
241
241
|
* @param {number} idx - The index to scroll to (exclusive)
|
|
242
242
|
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6-beta.0",
|
|
4
4
|
"description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"files": [
|
|
44
44
|
"dist",
|
|
45
45
|
"!dist/**/*.test.*",
|
|
46
|
-
"!dist/**/*.spec.*"
|
|
46
|
+
"!dist/**/*.spec.*",
|
|
47
|
+
"!dist/test/**/*"
|
|
47
48
|
],
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "vite build && npm run package",
|
|
@@ -71,41 +72,44 @@
|
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
74
|
"dependencies": {
|
|
74
|
-
"esm-env": "^1.2.2"
|
|
75
|
+
"esm-env": "^1.2.2",
|
|
76
|
+
"runed": "^0.31.1"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
77
|
-
"@eslint/compat": "^1.
|
|
78
|
-
"@eslint/js": "^9.
|
|
79
|
-
"@faker-js/faker": "^9.
|
|
80
|
-
"@playwright/test": "^1.
|
|
79
|
+
"@eslint/compat": "^1.3.1",
|
|
80
|
+
"@eslint/js": "^9.32.0",
|
|
81
|
+
"@faker-js/faker": "^9.9.0",
|
|
82
|
+
"@playwright/test": "^1.54.1",
|
|
81
83
|
"@sveltejs/adapter-auto": "^6.0.1",
|
|
82
|
-
"@sveltejs/kit": "^2.
|
|
83
|
-
"@sveltejs/package": "^2.
|
|
84
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
85
|
-
"@testing-library/jest-dom": "^6.6.
|
|
84
|
+
"@sveltejs/kit": "^2.26.1",
|
|
85
|
+
"@sveltejs/package": "^2.4.0",
|
|
86
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
|
87
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
86
88
|
"@testing-library/svelte": "^5.2.8",
|
|
87
89
|
"@testing-library/user-event": "^14.6.1",
|
|
88
|
-
"@types/node": "^
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
90
|
-
"@typescript-eslint/parser": "^8.
|
|
91
|
-
"@vitest/coverage-v8": "^3.
|
|
92
|
-
"eslint": "^9.
|
|
93
|
-
"eslint-config-prettier": "^10.1.
|
|
94
|
-
"eslint-plugin-import": "^2.
|
|
95
|
-
"eslint-plugin-svelte": "^3.
|
|
90
|
+
"@types/node": "^24.1.0",
|
|
91
|
+
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
92
|
+
"@typescript-eslint/parser": "^8.38.0",
|
|
93
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
94
|
+
"eslint": "^9.32.0",
|
|
95
|
+
"eslint-config-prettier": "^10.1.8",
|
|
96
|
+
"eslint-plugin-import": "^2.32.0",
|
|
97
|
+
"eslint-plugin-svelte": "^3.11.0",
|
|
96
98
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
97
|
-
"globals": "^16.
|
|
99
|
+
"globals": "^16.3.0",
|
|
98
100
|
"jsdom": "^26.1.0",
|
|
99
|
-
"prettier": "^3.
|
|
100
|
-
"prettier-plugin-organize-imports": "^4.
|
|
101
|
+
"prettier": "^3.6.2",
|
|
102
|
+
"prettier-plugin-organize-imports": "^4.2.0",
|
|
103
|
+
"prettier-plugin-sort-json": "^4.1.1",
|
|
101
104
|
"prettier-plugin-svelte": "^3.4.0",
|
|
105
|
+
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
102
106
|
"publint": "^0.3.12",
|
|
103
|
-
"svelte": "^5.
|
|
104
|
-
"svelte-check": "^4.
|
|
107
|
+
"svelte": "^5.37.1",
|
|
108
|
+
"svelte-check": "^4.3.0",
|
|
105
109
|
"typescript": "^5.8.3",
|
|
106
|
-
"typescript-eslint": "^8.
|
|
107
|
-
"vite": "^
|
|
108
|
-
"vitest": "^3.
|
|
110
|
+
"typescript-eslint": "^8.38.0",
|
|
111
|
+
"vite": "^7.0.6",
|
|
112
|
+
"vitest": "^3.2.4"
|
|
109
113
|
},
|
|
110
114
|
"peerDependencies": {
|
|
111
115
|
"svelte": "^5.0.0"
|