@humanspeak/svelte-virtual-list 0.2.4 โ 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +36 -0
- package/dist/SvelteVirtualList.svelte +257 -116
- package/dist/SvelteVirtualList.svelte.d.ts +186 -32
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +58 -16
- package/dist/types.js +8 -1
- package/dist/utils/heightCalculation.d.ts +77 -0
- package/dist/utils/heightCalculation.js +90 -0
- package/dist/utils/raf.d.ts +29 -5
- package/dist/utils/raf.js +45 -19
- package/dist/utils/types.d.ts +6 -0
- package/dist/utils/virtualList.d.ts +37 -4
- package/dist/utils/virtualList.js +81 -9
- package/package.json +41 -33
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://github.com/humanspeak/svelte-virtual-list/blob/main/LICENSE)
|
|
7
7
|
[](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
|
|
8
8
|
[](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/codeql.yml)
|
|
9
|
+
[](https://packagephobia.com/result?p=@humanspeak/svelte-virtual-list)
|
|
9
10
|
[](https://trunk.io)
|
|
10
11
|
[](http://www.typescriptlang.org/)
|
|
11
12
|
[](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
|
|
@@ -27,6 +28,41 @@ A high-performance virtual list component for Svelte 5 applications that efficie
|
|
|
27
28
|
- ๐ง Memory-optimized for 10k+ items
|
|
28
29
|
- ๐งช Comprehensive test coverage (vitest and playwright)
|
|
29
30
|
- ๐ Progressive initialization for large datasets
|
|
31
|
+
- ๐น๏ธ Programmatic scrolling with `scroll`
|
|
32
|
+
|
|
33
|
+
## scroll: Programmatic Scrolling
|
|
34
|
+
|
|
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
|
+
|
|
37
|
+
### Usage Example
|
|
38
|
+
|
|
39
|
+
```svelte
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
import SvelteVirtualList from '@humanspeak/svelte-virtual-list'
|
|
42
|
+
let listRef
|
|
43
|
+
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }))
|
|
44
|
+
|
|
45
|
+
function goToItem5000() {
|
|
46
|
+
// Scroll to item 5000 with smooth scrolling and auto alignment
|
|
47
|
+
listRef.scroll({ index: 5000, smoothScroll: true, align: 'auto' })
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<button on:click={goToItem5000}> Scroll to item 5000 </button>
|
|
52
|
+
<SvelteVirtualList {items} bind:this={listRef}>
|
|
53
|
+
{#snippet renderItem(item)}
|
|
54
|
+
<div>{item.text}</div>
|
|
55
|
+
{/snippet}
|
|
56
|
+
</SvelteVirtualList>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### API
|
|
60
|
+
|
|
61
|
+
- `scroll(options: { index: number; smoothScroll?: boolean; shouldThrowOnBounds?: boolean; align?: 'auto' | 'top' | 'bottom' })`
|
|
62
|
+
- `index`: The item index to scroll to (0-based)
|
|
63
|
+
- `smoothScroll`: If true, uses smooth scrolling (default: true)
|
|
64
|
+
- `shouldThrowOnBounds`: If true, throws if index is out of bounds (default: true)
|
|
65
|
+
- `align`: Where to align the item in the viewport. `'auto'` (default) scrolls only if the item is out of view, aligning to top or bottom as needed. `'top'` always aligns to the top, `'bottom'` always aligns to the bottom.
|
|
30
66
|
|
|
31
67
|
## Installation
|
|
32
68
|
|
|
@@ -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,19 +140,26 @@
|
|
|
122
140
|
* - Progressive size adjustment system
|
|
123
141
|
*/
|
|
124
142
|
|
|
125
|
-
import {
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
import {
|
|
144
|
+
DEFAULT_SCROLL_OPTIONS,
|
|
145
|
+
type SvelteVirtualListProps,
|
|
146
|
+
type SvelteVirtualListScrollOptions
|
|
147
|
+
} from './types.js'
|
|
148
|
+
import { calculateAverageHeightDebounced } from './utils/heightCalculation.js'
|
|
149
|
+
import { createRafScheduler } from './utils/raf.js'
|
|
128
150
|
import {
|
|
129
151
|
calculateScrollPosition,
|
|
130
|
-
calculateVisibleRange,
|
|
131
152
|
calculateTransformY,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
processChunked
|
|
153
|
+
calculateVisibleRange,
|
|
154
|
+
getScrollOffsetForIndex,
|
|
155
|
+
processChunked,
|
|
156
|
+
updateHeightAndScroll as utilsUpdateHeightAndScroll
|
|
135
157
|
} from './utils/virtualList.js'
|
|
136
|
-
import {
|
|
137
|
-
import {
|
|
158
|
+
import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
|
|
159
|
+
import { BROWSER } from 'esm-env'
|
|
160
|
+
import { onMount, tick } from 'svelte'
|
|
161
|
+
|
|
162
|
+
const rafSchedule = createRafScheduler()
|
|
138
163
|
|
|
139
164
|
/**
|
|
140
165
|
* Core configuration props with default values
|
|
@@ -193,76 +218,23 @@
|
|
|
193
218
|
let prevVisibleRange = $state<{ start: number; end: number } | null>(null)
|
|
194
219
|
let prevHeight = $state<number>(0)
|
|
195
220
|
|
|
196
|
-
/**
|
|
197
|
-
* Calculates and updates the average height of visible items with debouncing.
|
|
198
|
-
*
|
|
199
|
-
* This function optimizes performance by:
|
|
200
|
-
* - Debouncing calculations to prevent excessive DOM reads
|
|
201
|
-
* - Caching item heights to minimize recalculations
|
|
202
|
-
* - Only updating when significant changes are detected
|
|
203
|
-
*
|
|
204
|
-
* Implementation details:
|
|
205
|
-
* - Uses a 200ms debounce timeout
|
|
206
|
-
* - Tracks calculation state to prevent concurrent updates
|
|
207
|
-
* - Caches heights in heightCache for reuse
|
|
208
|
-
* - Only updates if height difference > 1px
|
|
209
|
-
*
|
|
210
|
-
* State interactions:
|
|
211
|
-
* - Updates calculatedItemHeight
|
|
212
|
-
* - Updates lastMeasuredIndex
|
|
213
|
-
* - Modifies heightCache
|
|
214
|
-
* - Uses/sets isCalculatingHeight flag
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* ```typescript
|
|
218
|
-
* // Automatically called when items are rendered
|
|
219
|
-
* $effect(() => {
|
|
220
|
-
* if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
221
|
-
* calculateAverageHeightDebounced()
|
|
222
|
-
* }
|
|
223
|
-
* })
|
|
224
|
-
* ```
|
|
225
|
-
*
|
|
226
|
-
* @returns {void}
|
|
227
|
-
*/
|
|
228
|
-
const calculateAverageHeightDebounced = () => {
|
|
229
|
-
if (!BROWSER || isCalculatingHeight || heightUpdateTimeout) return
|
|
230
|
-
isCalculatingHeight = true
|
|
231
|
-
|
|
232
|
-
if (heightUpdateTimeout) {
|
|
233
|
-
clearTimeout(heightUpdateTimeout)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
heightUpdateTimeout = setTimeout(() => {
|
|
237
|
-
const visibleRange = visibleItems()
|
|
238
|
-
const currentIndex = visibleRange.start
|
|
239
|
-
|
|
240
|
-
if (currentIndex !== lastMeasuredIndex) {
|
|
241
|
-
const { newHeight, newLastMeasuredIndex, updatedHeightCache } =
|
|
242
|
-
calculateAverageHeight(
|
|
243
|
-
itemElements,
|
|
244
|
-
visibleRange,
|
|
245
|
-
heightCache,
|
|
246
|
-
lastMeasuredIndex,
|
|
247
|
-
calculatedItemHeight
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
if (Math.abs(newHeight - calculatedItemHeight) > 1) {
|
|
251
|
-
calculatedItemHeight = newHeight
|
|
252
|
-
lastMeasuredIndex = newLastMeasuredIndex
|
|
253
|
-
heightCache = updatedHeightCache
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
isCalculatingHeight = false
|
|
258
|
-
heightUpdateTimeout = null
|
|
259
|
-
}, 200)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
221
|
// Trigger height calculation when items are rendered
|
|
263
222
|
$effect(() => {
|
|
264
223
|
if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
265
|
-
calculateAverageHeightDebounced(
|
|
224
|
+
heightUpdateTimeout = calculateAverageHeightDebounced(
|
|
225
|
+
isCalculatingHeight,
|
|
226
|
+
heightUpdateTimeout,
|
|
227
|
+
visibleItems,
|
|
228
|
+
itemElements,
|
|
229
|
+
heightCache,
|
|
230
|
+
lastMeasuredIndex,
|
|
231
|
+
calculatedItemHeight,
|
|
232
|
+
(result) => {
|
|
233
|
+
calculatedItemHeight = result.newHeight
|
|
234
|
+
lastMeasuredIndex = result.newLastMeasuredIndex
|
|
235
|
+
heightCache = result.updatedHeightCache
|
|
236
|
+
}
|
|
237
|
+
)
|
|
266
238
|
}
|
|
267
239
|
})
|
|
268
240
|
|
|
@@ -305,7 +277,7 @@
|
|
|
305
277
|
const targetScrollTop = Math.max(0, totalHeight - height)
|
|
306
278
|
|
|
307
279
|
// Add delay to ensure layout is complete
|
|
308
|
-
|
|
280
|
+
tick().then(() => {
|
|
309
281
|
if (viewportElement) {
|
|
310
282
|
// Start at the bottom for bottom-to-top mode
|
|
311
283
|
viewportElement.scrollTop = targetScrollTop
|
|
@@ -320,7 +292,7 @@
|
|
|
320
292
|
initialized = true
|
|
321
293
|
})
|
|
322
294
|
}
|
|
323
|
-
}
|
|
295
|
+
})
|
|
324
296
|
}
|
|
325
297
|
})
|
|
326
298
|
|
|
@@ -406,12 +378,12 @@
|
|
|
406
378
|
*/
|
|
407
379
|
const updateHeightAndScroll = (immediate = false) => {
|
|
408
380
|
if (!initialized && mode === 'bottomToTop') {
|
|
409
|
-
|
|
381
|
+
tick().then(() => {
|
|
410
382
|
if (containerElement) {
|
|
411
383
|
const initialHeight = containerElement.getBoundingClientRect().height
|
|
412
384
|
height = initialHeight
|
|
413
385
|
|
|
414
|
-
|
|
386
|
+
tick().then(() => {
|
|
415
387
|
if (containerElement && viewportElement) {
|
|
416
388
|
const finalHeight = containerElement.getBoundingClientRect().height
|
|
417
389
|
height = finalHeight
|
|
@@ -438,9 +410,9 @@
|
|
|
438
410
|
}
|
|
439
411
|
})
|
|
440
412
|
}
|
|
441
|
-
}
|
|
413
|
+
})
|
|
442
414
|
}
|
|
443
|
-
}
|
|
415
|
+
})
|
|
444
416
|
return
|
|
445
417
|
}
|
|
446
418
|
|
|
@@ -541,6 +513,175 @@
|
|
|
541
513
|
prevHeight = calculatedItemHeight
|
|
542
514
|
}
|
|
543
515
|
})
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Scrolls the virtual list to the item at the given index.
|
|
519
|
+
*
|
|
520
|
+
* @deprecated This function is deprecated and will be removed in a future version.
|
|
521
|
+
* Use the new scroll method from the component instance instead.
|
|
522
|
+
*
|
|
523
|
+
* @function scrollToIndex
|
|
524
|
+
* @param index The index of the item to scroll to.
|
|
525
|
+
* @param smoothScroll (default: true) Whether to use smooth scrolling.
|
|
526
|
+
* @param shouldThrowOnBounds (default: true) Whether to throw an error if the index is out of bounds.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* // Svelte usage:
|
|
530
|
+
* // In your <script> block:
|
|
531
|
+
* import SvelteVirtualList from '@humanspeak/svelte-virtual-list';
|
|
532
|
+
* let virtualList;
|
|
533
|
+
* const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
|
|
534
|
+
*
|
|
535
|
+
* // In your markup:
|
|
536
|
+
* <button onclick={() => virtualList.scrollToIndex(5000)}>
|
|
537
|
+
* Scroll to 5000
|
|
538
|
+
* </button>
|
|
539
|
+
* <SvelteVirtualList {items} bind:this={virtualList}>
|
|
540
|
+
* {#snippet renderItem(item)}
|
|
541
|
+
* <div>{item.text}</div>
|
|
542
|
+
* {/snippet}
|
|
543
|
+
* </SvelteVirtualList>
|
|
544
|
+
*
|
|
545
|
+
* @returns {void}
|
|
546
|
+
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
547
|
+
*/
|
|
548
|
+
export const scrollToIndex = (
|
|
549
|
+
index: number,
|
|
550
|
+
smoothScroll = true,
|
|
551
|
+
shouldThrowOnBounds = true
|
|
552
|
+
): void => {
|
|
553
|
+
// Deprecation warning
|
|
554
|
+
console.warn(
|
|
555
|
+
'SvelteVirtualList: scrollToIndex is deprecated and will be removed in a future version. ' +
|
|
556
|
+
'Use the new scroll method from the component instance instead.'
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
// Call the new scroll function with the provided parameters
|
|
560
|
+
scroll({ index, smoothScroll, shouldThrowOnBounds })
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Scrolls the virtual list to the item at the given index using a type-based options approach.
|
|
565
|
+
*
|
|
566
|
+
* @function scroll
|
|
567
|
+
* @param options Configuration options for scrolling behavior.
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* // Svelte usage:
|
|
571
|
+
* // In your <script> block:
|
|
572
|
+
* import SvelteVirtualList from './index.js';
|
|
573
|
+
* let virtualList;
|
|
574
|
+
* const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
|
|
575
|
+
*
|
|
576
|
+
* <button onclick={() => virtualList.scroll({ index: 5000 })}>
|
|
577
|
+
* Scroll to 5000
|
|
578
|
+
* </button>
|
|
579
|
+
* <SvelteVirtualList {items} bind:this={virtualList}>
|
|
580
|
+
* {#snippet renderItem(item)}
|
|
581
|
+
* <div>{item.text}</div>
|
|
582
|
+
* {/snippet}
|
|
583
|
+
* </SvelteVirtualList>
|
|
584
|
+
*
|
|
585
|
+
* @returns {void}
|
|
586
|
+
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
587
|
+
*/
|
|
588
|
+
export const scroll = (options: SvelteVirtualListScrollOptions): void => {
|
|
589
|
+
const { index, smoothScroll, shouldThrowOnBounds, align } = {
|
|
590
|
+
...DEFAULT_SCROLL_OPTIONS,
|
|
591
|
+
...options
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (!items.length) return
|
|
595
|
+
if (!viewportElement) {
|
|
596
|
+
tick().then(() => {
|
|
597
|
+
if (!viewportElement) return
|
|
598
|
+
scroll({ index, smoothScroll, shouldThrowOnBounds, align })
|
|
599
|
+
})
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Bounds checking
|
|
604
|
+
let targetIndex = index
|
|
605
|
+
if (targetIndex < 0 || targetIndex >= items.length) {
|
|
606
|
+
if (shouldThrowOnBounds) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`scroll: index ${targetIndex} is out of bounds (0-${items.length - 1})`
|
|
609
|
+
)
|
|
610
|
+
} else {
|
|
611
|
+
targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1))
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems()
|
|
616
|
+
let scrollTarget: number | null = null
|
|
617
|
+
|
|
618
|
+
if (mode === 'bottomToTop') {
|
|
619
|
+
const totalHeight = items.length * calculatedItemHeight
|
|
620
|
+
const itemOffset = targetIndex * calculatedItemHeight
|
|
621
|
+
const itemHeight = calculatedItemHeight
|
|
622
|
+
if (align === 'auto') {
|
|
623
|
+
if (targetIndex < firstVisibleIndex) {
|
|
624
|
+
// Align to top
|
|
625
|
+
scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
|
|
626
|
+
} else if (targetIndex > lastVisibleIndex - 1) {
|
|
627
|
+
// Align to bottom
|
|
628
|
+
scrollTarget = Math.max(0, totalHeight - itemOffset - height)
|
|
629
|
+
} else {
|
|
630
|
+
// Already in view, do nothing
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
} else if (align === 'top') {
|
|
634
|
+
// Align to top
|
|
635
|
+
scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
|
|
636
|
+
} else if (align === 'bottom') {
|
|
637
|
+
// Align to bottom
|
|
638
|
+
scrollTarget = Math.max(0, totalHeight - itemOffset - height)
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
// topToBottom (default)
|
|
642
|
+
if (align === 'auto') {
|
|
643
|
+
if (targetIndex < firstVisibleIndex) {
|
|
644
|
+
// Scroll so item is at the top
|
|
645
|
+
scrollTarget = getScrollOffsetForIndex(
|
|
646
|
+
heightCache,
|
|
647
|
+
calculatedItemHeight,
|
|
648
|
+
targetIndex
|
|
649
|
+
)
|
|
650
|
+
} else if (targetIndex > lastVisibleIndex - 1) {
|
|
651
|
+
// Scroll so item is at the bottom
|
|
652
|
+
const itemBottom = getScrollOffsetForIndex(
|
|
653
|
+
heightCache,
|
|
654
|
+
calculatedItemHeight,
|
|
655
|
+
targetIndex + 1
|
|
656
|
+
)
|
|
657
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
658
|
+
} else {
|
|
659
|
+
// Already in view, do nothing
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
} else if (align === 'top') {
|
|
663
|
+
scrollTarget = getScrollOffsetForIndex(
|
|
664
|
+
heightCache,
|
|
665
|
+
calculatedItemHeight,
|
|
666
|
+
targetIndex
|
|
667
|
+
)
|
|
668
|
+
} else if (align === 'bottom') {
|
|
669
|
+
const itemBottom = getScrollOffsetForIndex(
|
|
670
|
+
heightCache,
|
|
671
|
+
calculatedItemHeight,
|
|
672
|
+
targetIndex + 1
|
|
673
|
+
)
|
|
674
|
+
scrollTarget = Math.max(0, itemBottom - height)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (scrollTarget !== null) {
|
|
679
|
+
viewportElement.scrollTo({
|
|
680
|
+
top: scrollTarget,
|
|
681
|
+
behavior: smoothScroll ? 'smooth' : 'auto'
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
}
|
|
544
685
|
</script>
|
|
545
686
|
|
|
546
687
|
<!--
|
|
@@ -597,7 +738,7 @@
|
|
|
597
738
|
)}
|
|
598
739
|
{debugFunction
|
|
599
740
|
? debugFunction(debugInfo)
|
|
600
|
-
: console.
|
|
741
|
+
: console.info('Virtual List Debug:', debugInfo)}
|
|
601
742
|
{/if}
|
|
602
743
|
<!-- Render each visible item -->
|
|
603
744
|
<div bind:this={itemElements[i]}>
|