@humanspeak/svelte-virtual-list 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -16
- package/dist/SvelteVirtualList.svelte +304 -56
- package/dist/SvelteVirtualList.svelte.d.ts +38 -2
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +46 -12
- package/dist/utils/types.d.ts +41 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/virtualList.d.ts +62 -0
- package/dist/utils/virtualList.js +97 -0
- package/package.json +59 -58
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ A virtual list component for Svelte applications. Built for Svelte 5 with TypeSc
|
|
|
18
18
|
npm install @humanspeak/svelte-virtual-list
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Dependencies
|
|
22
|
+
|
|
23
|
+
- [esm-env](https://github.com/benmccann/esm-env) - svelte5 suggested environment detecting
|
|
24
|
+
|
|
21
25
|
## Usage
|
|
22
26
|
|
|
23
27
|
```svelte
|
|
@@ -34,23 +38,14 @@ npm install @humanspeak/svelte-virtual-list
|
|
|
34
38
|
id: i,
|
|
35
39
|
text: `Item ${i}`
|
|
36
40
|
}))
|
|
37
|
-
|
|
38
|
-
let measureRef: HTMLElement
|
|
39
|
-
let itemHeight = 20 // default height
|
|
40
|
-
|
|
41
|
-
onMount(() => {
|
|
42
|
-
if (measureRef) {
|
|
43
|
-
itemHeight = measureRef.getBoundingClientRect().height
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
41
|
</script>
|
|
47
42
|
|
|
48
43
|
<div class="grid grid-cols-2 gap-8">
|
|
49
44
|
<!-- Top to bottom scrolling -->
|
|
50
45
|
<div>
|
|
51
|
-
<SvelteVirtualList {items}
|
|
46
|
+
<SvelteVirtualList {items}>
|
|
52
47
|
{#snippet renderItem(item: Item, index: number)}
|
|
53
|
-
<div
|
|
48
|
+
<div>
|
|
54
49
|
{item.text}
|
|
55
50
|
</div>
|
|
56
51
|
{/snippet}
|
|
@@ -59,9 +54,9 @@ npm install @humanspeak/svelte-virtual-list
|
|
|
59
54
|
|
|
60
55
|
<!-- Bottom to top scrolling -->
|
|
61
56
|
<div>
|
|
62
|
-
<SvelteVirtualList {items}
|
|
57
|
+
<SvelteVirtualList {items} mode="bottomToTop">
|
|
63
58
|
{#snippet renderItem(item: Item, index: number)}
|
|
64
|
-
<div
|
|
59
|
+
<div>
|
|
65
60
|
{item.text}
|
|
66
61
|
</div>
|
|
67
62
|
{/snippet}
|
|
@@ -75,9 +70,9 @@ npm install @humanspeak/svelte-virtual-list
|
|
|
75
70
|
The VirtualList component accepts the following props:
|
|
76
71
|
|
|
77
72
|
- `items` - Array of items to render
|
|
78
|
-
- `
|
|
79
|
-
- `itemHeight` - Height of each item in pixels
|
|
73
|
+
- `defaultItemHeight` - Initial height of each item in pixels (optional, defaults to 40)
|
|
80
74
|
- `mode` - Scroll direction ('topToBottom' or 'bottomToTop')
|
|
75
|
+
- `bufferSize` - Number of items to render outside the visible area (optional, defaults to 20)
|
|
81
76
|
- `debug` - Enable debug mode (optional)
|
|
82
77
|
- `containerClass` - Custom class for container element (optional)
|
|
83
78
|
- `viewportClass` - Custom class for viewport element (optional)
|
|
@@ -85,6 +80,18 @@ The VirtualList component accepts the following props:
|
|
|
85
80
|
- `itemsClass` - Custom class for items wrapper (optional)
|
|
86
81
|
- `renderItem` - Snippet function to render each item
|
|
87
82
|
|
|
83
|
+
Note: The component will automatically calculate the average item height based on rendered items, using `defaultItemHeight` only as an initial value until real measurements are available.
|
|
84
|
+
|
|
85
|
+
### Buffer Size
|
|
86
|
+
|
|
87
|
+
The `bufferSize` prop determines how many additional items are rendered outside the visible area. A larger buffer:
|
|
88
|
+
|
|
89
|
+
- Reduces the chance of seeing blank spaces during fast scrolling
|
|
90
|
+
- Provides smoother scrolling experience
|
|
91
|
+
- Increases memory usage (as more items are rendered)
|
|
92
|
+
|
|
93
|
+
Default value is 20 items, which provides a good balance between performance and smoothness.
|
|
94
|
+
|
|
88
95
|
## Features
|
|
89
96
|
|
|
90
97
|
- Efficient rendering of large lists
|
|
@@ -118,9 +125,71 @@ npm run build
|
|
|
118
125
|
|
|
119
126
|
[MIT](LICENSE)
|
|
120
127
|
|
|
128
|
+
## Key Features
|
|
129
|
+
|
|
130
|
+
- Dynamic item height handling - no fixed height required
|
|
131
|
+
- Bi-directional scrolling support (top-to-bottom and bottom-to-top)
|
|
132
|
+
- Automatic resize handling for dynamic content
|
|
133
|
+
- Efficient rendering of large lists
|
|
134
|
+
- TypeScript support
|
|
135
|
+
- Customizable styling
|
|
136
|
+
- Debug mode for development
|
|
137
|
+
- Smooth scrolling with buffer zones
|
|
138
|
+
- SSR compatible
|
|
139
|
+
- Svelte 5 runes support
|
|
140
|
+
|
|
141
|
+
## Usage Examples
|
|
142
|
+
|
|
143
|
+
### Basic Usage
|
|
144
|
+
|
|
145
|
+
### Default display
|
|
146
|
+
|
|
147
|
+
```svelte
|
|
148
|
+
<script lang="ts">
|
|
149
|
+
import SvelteVirtualList from '@humanspeak/svelte-virtual-list'
|
|
150
|
+
|
|
151
|
+
const items = Array.from({ length: 1000 }, (_, i) => ({
|
|
152
|
+
id: i,
|
|
153
|
+
text: `Item ${i}`
|
|
154
|
+
}))
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<SvelteVirtualList {items}>
|
|
158
|
+
{#snippet renderItem(item)}
|
|
159
|
+
<div>
|
|
160
|
+
{item.text}
|
|
161
|
+
</div>
|
|
162
|
+
{/snippet}
|
|
163
|
+
</SvelteVirtualList>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Bottom-to-Top Scrolling
|
|
167
|
+
|
|
168
|
+
The component supports reverse scrolling, which is useful for chat applications or logs:
|
|
169
|
+
|
|
170
|
+
```svelte
|
|
171
|
+
<SvelteVirtualList {items} mode="bottomToTop">
|
|
172
|
+
{#snippet renderItem(item)}
|
|
173
|
+
<div>{item.text}</div>
|
|
174
|
+
{/snippet}
|
|
175
|
+
</SvelteVirtualList>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Advanced Features
|
|
179
|
+
|
|
180
|
+
### Auto-resize Handling
|
|
181
|
+
|
|
182
|
+
The component automatically handles:
|
|
183
|
+
|
|
184
|
+
- Dynamic content changes within items
|
|
185
|
+
- Window resize events
|
|
186
|
+
- Container resize events
|
|
187
|
+
- Dynamic height calculations
|
|
188
|
+
|
|
189
|
+
No manual intervention is needed when item contents or dimensions change.
|
|
190
|
+
|
|
121
191
|
## Related
|
|
122
192
|
|
|
123
193
|
- [Svelte](https://svelte.dev) - JavaScript front-end framework
|
|
124
|
-
- [Original Component](https://github.com/pablo-abc/svelte-virtual-list) - Original inspiration
|
|
125
194
|
|
|
126
195
|
Made with ♥ by [Humanspeak](https://humanspeak.com)
|
|
@@ -1,10 +1,99 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A high-performance virtualized list component that efficiently renders large datasets
|
|
4
|
+
by only mounting DOM nodes for visible items and a small buffer.
|
|
5
|
+
|
|
6
|
+
Props:
|
|
7
|
+
- `items` - Array of items to render
|
|
8
|
+
- `defaultEstimatedItemHeight` - Initial height estimate for items (default: 40px)
|
|
9
|
+
- `mode` - Scroll direction: 'topToBottom' or 'bottomToTop' (default: 'topToBottom')
|
|
10
|
+
- `debug` - Enable debug logging (default: false)
|
|
11
|
+
- `bufferSize` - Number of items to render outside visible area (default: 20)
|
|
12
|
+
- `containerClass` - Custom class for container element
|
|
13
|
+
- `viewportClass` - Custom class for viewport element
|
|
14
|
+
- `contentClass` - Custom class for content wrapper
|
|
15
|
+
- `itemsClass` - Custom class for items wrapper
|
|
16
|
+
- `debugFunction` - Custom debug logging function
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
```svelte
|
|
20
|
+
<SvelteVirtualList
|
|
21
|
+
items={data}
|
|
22
|
+
defaultEstimatedItemHeight={40}
|
|
23
|
+
mode="topToBottom"
|
|
24
|
+
>
|
|
25
|
+
{#snippet renderItem(item, index)}
|
|
26
|
+
<div class="item">{item.text}</div>
|
|
27
|
+
{/snippet}
|
|
28
|
+
</SvelteVirtualList>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Features:
|
|
32
|
+
- Dynamic height calculation
|
|
33
|
+
- Bidirectional scrolling
|
|
34
|
+
- Configurable buffer size
|
|
35
|
+
- Debug mode
|
|
36
|
+
- Custom styling
|
|
37
|
+
-->
|
|
38
|
+
|
|
1
39
|
<script lang="ts">
|
|
40
|
+
/**
|
|
41
|
+
* SvelteVirtualList Implementation Journey
|
|
42
|
+
*
|
|
43
|
+
* Evolution & Architecture:
|
|
44
|
+
* 1. Initial Implementation
|
|
45
|
+
* - Basic virtual scrolling with fixed height items
|
|
46
|
+
* - Single direction scrolling (top-to-bottom)
|
|
47
|
+
* - Simple viewport calculations
|
|
48
|
+
*
|
|
49
|
+
* 2. Dynamic Height Enhancement
|
|
50
|
+
* - Added dynamic height calculation system
|
|
51
|
+
* - Implemented debounced measurements
|
|
52
|
+
* - Created height averaging mechanism for performance
|
|
53
|
+
*
|
|
54
|
+
* 3. Bidirectional Scrolling
|
|
55
|
+
* - Added bottomToTop mode
|
|
56
|
+
* - Solved complex initialization issues with flexbox
|
|
57
|
+
* - Implemented careful scroll position management
|
|
58
|
+
*
|
|
59
|
+
* 4. Performance Optimizations
|
|
60
|
+
* - Added element recycling through keyed each blocks
|
|
61
|
+
* - Implemented RAF for smooth animations
|
|
62
|
+
* - Optimized DOM updates with transform translations
|
|
63
|
+
*
|
|
64
|
+
* 5. Stability Improvements
|
|
65
|
+
* - Added ResizeObserver for responsive updates
|
|
66
|
+
* - Implemented proper cleanup on component destruction
|
|
67
|
+
* - Added debug mode for development assistance
|
|
68
|
+
*
|
|
69
|
+
* Technical Challenges Solved:
|
|
70
|
+
* - Bottom-to-top scrolling in flexbox layouts
|
|
71
|
+
* - Dynamic height calculations without layout thrashing
|
|
72
|
+
* - Smooth scrolling on various devices
|
|
73
|
+
* - Memory management for large lists
|
|
74
|
+
* - Browser compatibility issues
|
|
75
|
+
*
|
|
76
|
+
* Current Architecture:
|
|
77
|
+
* - Four-layer DOM structure for optimal performance
|
|
78
|
+
* - State management using Svelte 5's $state
|
|
79
|
+
* - Reactive height and scroll calculations
|
|
80
|
+
* - Configurable buffer zones for smooth scrolling
|
|
81
|
+
*/
|
|
82
|
+
|
|
2
83
|
import { onMount } from 'svelte'
|
|
3
|
-
import
|
|
84
|
+
import { BROWSER } from 'esm-env'
|
|
85
|
+
import type { SvelteVirtualListDebugInfo, SvelteVirtualListProps } from './types.js'
|
|
86
|
+
import {
|
|
87
|
+
calculateScrollPosition,
|
|
88
|
+
calculateVisibleRange,
|
|
89
|
+
calculateTransformY,
|
|
90
|
+
updateHeightAndScroll as utilsUpdateHeightAndScroll
|
|
91
|
+
} from './utils/virtualList.js'
|
|
4
92
|
|
|
93
|
+
// Core configuration props with default values
|
|
5
94
|
const {
|
|
6
95
|
items = [],
|
|
7
|
-
|
|
96
|
+
defaultEstimatedItemHeight = 40,
|
|
8
97
|
debug = false,
|
|
9
98
|
renderItem,
|
|
10
99
|
containerClass,
|
|
@@ -14,88 +103,232 @@
|
|
|
14
103
|
debugFunction,
|
|
15
104
|
mode = 'topToBottom',
|
|
16
105
|
bufferSize = 20
|
|
17
|
-
}:
|
|
106
|
+
}: SvelteVirtualListProps = $props()
|
|
18
107
|
|
|
108
|
+
// DOM references and state management
|
|
19
109
|
let containerElement: HTMLElement
|
|
20
110
|
let viewportElement: HTMLElement
|
|
21
|
-
|
|
22
|
-
let
|
|
23
|
-
let
|
|
111
|
+
const itemElements = $state<HTMLElement[]>([]) // Tracks rendered item elements for height calculations
|
|
112
|
+
let scrollTop = $state(0) // Current scroll position
|
|
113
|
+
let initialized = $state(false) // Tracks if initial setup is complete
|
|
114
|
+
let height = $state(0) // Container height
|
|
115
|
+
let calculatedItemHeight = $state(defaultEstimatedItemHeight) // Current average item height
|
|
116
|
+
let isCalculatingHeight = $state(false) // Prevents concurrent height calculations
|
|
117
|
+
let lastMeasuredIndex = $state(-1) // Tracks last measured item for optimization
|
|
118
|
+
let heightUpdateTimeout: ReturnType<typeof setTimeout> | null = null
|
|
119
|
+
let resizeObserver: ResizeObserver | null = null
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Calculates the average height of visible items to improve accuracy of virtual scrolling
|
|
123
|
+
* Uses debouncing to prevent excessive calculations
|
|
124
|
+
*/
|
|
125
|
+
const calculateAverageHeight = () => {
|
|
126
|
+
if (!BROWSER || isCalculatingHeight || heightUpdateTimeout) return
|
|
127
|
+
isCalculatingHeight = true
|
|
128
|
+
|
|
129
|
+
if (heightUpdateTimeout) {
|
|
130
|
+
clearTimeout(heightUpdateTimeout)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
heightUpdateTimeout = setTimeout(() => {
|
|
134
|
+
const visibleRange = visibleItems()
|
|
135
|
+
const currentIndex = visibleRange.start
|
|
136
|
+
|
|
137
|
+
// Only recalculate if we're looking at different items
|
|
138
|
+
if (currentIndex !== lastMeasuredIndex) {
|
|
139
|
+
const validElements = itemElements.filter((el) => el)
|
|
140
|
+
if (validElements.length > 0) {
|
|
141
|
+
// Calculate average height from actual rendered elements
|
|
142
|
+
const heights = validElements.map((el) => el.getBoundingClientRect().height)
|
|
143
|
+
const averageHeight = heights.reduce((sum, h) => sum + h, 0) / heights.length
|
|
24
144
|
|
|
25
|
-
|
|
145
|
+
// Update only if there's a significant change
|
|
146
|
+
if (
|
|
147
|
+
averageHeight > 0 &&
|
|
148
|
+
!isNaN(averageHeight) &&
|
|
149
|
+
Math.abs(averageHeight - calculatedItemHeight) > 1
|
|
150
|
+
) {
|
|
151
|
+
calculatedItemHeight = averageHeight
|
|
152
|
+
lastMeasuredIndex = currentIndex
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
isCalculatingHeight = false
|
|
158
|
+
heightUpdateTimeout = null
|
|
159
|
+
}, 200) // Debounce for 200ms
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Trigger height calculation when items are rendered
|
|
163
|
+
$effect(() => {
|
|
164
|
+
if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
165
|
+
calculateAverageHeight()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Update container height when element is mounted
|
|
26
170
|
$effect(() => {
|
|
27
|
-
if (containerElement) {
|
|
171
|
+
if (BROWSER && containerElement) {
|
|
28
172
|
height = containerElement.getBoundingClientRect().height
|
|
29
173
|
}
|
|
30
174
|
})
|
|
31
175
|
|
|
32
|
-
//
|
|
176
|
+
// Special handling for bottom-to-top mode initialization
|
|
33
177
|
$effect(() => {
|
|
34
178
|
if (
|
|
179
|
+
BROWSER &&
|
|
35
180
|
mode === 'bottomToTop' &&
|
|
36
181
|
viewportElement &&
|
|
37
182
|
height > 0 &&
|
|
38
183
|
items.length &&
|
|
39
184
|
!initialized
|
|
40
185
|
) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
186
|
+
const totalHeight = items.length * calculatedItemHeight
|
|
187
|
+
// Add delay to ensure layout is complete
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
if (viewportElement) {
|
|
190
|
+
// Start at the bottom for bottom-to-top mode
|
|
191
|
+
viewportElement.scrollTop = totalHeight - height
|
|
192
|
+
scrollTop = totalHeight - height
|
|
193
|
+
initialized = true
|
|
194
|
+
}
|
|
195
|
+
}, 50)
|
|
51
196
|
}
|
|
52
197
|
})
|
|
53
198
|
|
|
54
|
-
|
|
199
|
+
// Calculate which items should be visible based on current scroll position
|
|
200
|
+
const visibleItems = $derived(() => {
|
|
55
201
|
if (!items.length) return { start: 0, end: 0 }
|
|
202
|
+
const viewportHeight = height || 0
|
|
56
203
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
204
|
+
return calculateVisibleRange(
|
|
205
|
+
scrollTop,
|
|
206
|
+
viewportHeight,
|
|
207
|
+
calculatedItemHeight,
|
|
208
|
+
items.length,
|
|
209
|
+
bufferSize,
|
|
210
|
+
mode
|
|
211
|
+
)
|
|
212
|
+
})
|
|
64
213
|
|
|
65
|
-
|
|
66
|
-
|
|
214
|
+
// Update scroll position when user scrolls
|
|
215
|
+
const handleScroll = () => {
|
|
216
|
+
if (!BROWSER || !viewportElement) return
|
|
217
|
+
scrollTop = viewportElement.scrollTop
|
|
218
|
+
}
|
|
67
219
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Updates the height and scroll position of the virtual list.
|
|
222
|
+
*
|
|
223
|
+
* This function handles two scenarios:
|
|
224
|
+
* 1. Initial setup (critical for bottomToTop mode in flexbox layouts)
|
|
225
|
+
* 2. Subsequent resize events
|
|
226
|
+
*
|
|
227
|
+
* For bottomToTop mode, we need to ensure:
|
|
228
|
+
* - The flexbox layout is fully calculated
|
|
229
|
+
* - The height measurements are accurate
|
|
230
|
+
* - The scroll position starts at the bottom
|
|
231
|
+
*
|
|
232
|
+
* @param immediate - Whether to skip the delay (used for resize events)
|
|
233
|
+
*/
|
|
234
|
+
const updateHeightAndScroll = (immediate = false) => {
|
|
235
|
+
if (!initialized && mode === 'bottomToTop') {
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
if (containerElement) {
|
|
238
|
+
const initialHeight = containerElement.getBoundingClientRect().height
|
|
239
|
+
height = initialHeight
|
|
240
|
+
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
if (containerElement && viewportElement) {
|
|
243
|
+
const finalHeight = containerElement.getBoundingClientRect().height
|
|
244
|
+
height = finalHeight
|
|
245
|
+
|
|
246
|
+
const targetScrollTop = calculateScrollPosition(
|
|
247
|
+
items.length,
|
|
248
|
+
calculatedItemHeight,
|
|
249
|
+
finalHeight
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
void containerElement.offsetHeight
|
|
253
|
+
|
|
254
|
+
viewportElement.scrollTop = targetScrollTop
|
|
255
|
+
scrollTop = targetScrollTop
|
|
74
256
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
257
|
+
requestAnimationFrame(() => {
|
|
258
|
+
if (viewportElement) {
|
|
259
|
+
const currentScroll = viewportElement.scrollTop
|
|
260
|
+
if (currentScroll !== scrollTop) {
|
|
261
|
+
viewportElement.scrollTop = targetScrollTop
|
|
262
|
+
scrollTop = targetScrollTop
|
|
263
|
+
}
|
|
264
|
+
initialized = true
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
}, 100)
|
|
269
|
+
}
|
|
270
|
+
}, 100)
|
|
271
|
+
return
|
|
78
272
|
}
|
|
79
|
-
})
|
|
80
273
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
274
|
+
utilsUpdateHeightAndScroll(
|
|
275
|
+
{
|
|
276
|
+
initialized,
|
|
277
|
+
mode,
|
|
278
|
+
containerElement,
|
|
279
|
+
viewportElement,
|
|
280
|
+
calculatedItemHeight,
|
|
281
|
+
height,
|
|
282
|
+
scrollTop
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
setHeight: (h) => (height = h),
|
|
286
|
+
setScrollTop: (st) => (scrollTop = st),
|
|
287
|
+
setInitialized: (i) => (initialized = i)
|
|
288
|
+
},
|
|
289
|
+
immediate
|
|
290
|
+
)
|
|
85
291
|
}
|
|
86
292
|
|
|
293
|
+
// Setup and cleanup
|
|
87
294
|
onMount(() => {
|
|
88
|
-
if (
|
|
89
|
-
|
|
295
|
+
if (BROWSER) {
|
|
296
|
+
// Initial setup of heights and scroll position
|
|
297
|
+
updateHeightAndScroll()
|
|
298
|
+
|
|
299
|
+
// Watch for container size changes
|
|
300
|
+
resizeObserver = new ResizeObserver(() => {
|
|
301
|
+
updateHeightAndScroll(true)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
if (containerElement) {
|
|
305
|
+
resizeObserver.observe(containerElement)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Cleanup on component destruction
|
|
309
|
+
return () => {
|
|
310
|
+
if (resizeObserver) {
|
|
311
|
+
resizeObserver.disconnect()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
90
314
|
}
|
|
91
315
|
})
|
|
92
316
|
</script>
|
|
93
317
|
|
|
318
|
+
<!--
|
|
319
|
+
The template uses a four-layer structure:
|
|
320
|
+
1. Container - Overall boundary
|
|
321
|
+
2. Viewport - Scrollable area
|
|
322
|
+
3. Content - Full height container
|
|
323
|
+
4. Items - Translated list of visible items
|
|
324
|
+
-->
|
|
94
325
|
<div
|
|
95
326
|
id="virtual-list-container"
|
|
327
|
+
data-testid="virtual-list-container"
|
|
96
328
|
class={containerClass ?? 'virtual-list-container'}
|
|
97
329
|
bind:this={containerElement}
|
|
98
330
|
>
|
|
331
|
+
<!-- Viewport handles scrolling -->
|
|
99
332
|
<div
|
|
100
333
|
id="virtual-list-viewport"
|
|
101
334
|
data-testid="virtual-list-viewport"
|
|
@@ -103,23 +336,31 @@
|
|
|
103
336
|
bind:this={viewportElement}
|
|
104
337
|
onscroll={handleScroll}
|
|
105
338
|
>
|
|
339
|
+
<!-- Content provides full scrollable height -->
|
|
106
340
|
<div
|
|
107
341
|
id="virtual-list-content"
|
|
108
342
|
class={contentClass ?? 'virtual-list-content'}
|
|
109
|
-
|
|
343
|
+
data-testid="virtual-list-content"
|
|
344
|
+
style:height="{Math.max(height, items.length * calculatedItemHeight)}px"
|
|
110
345
|
>
|
|
346
|
+
<!-- Items container is translated to show correct items -->
|
|
111
347
|
<div
|
|
112
348
|
id="virtual-list-items"
|
|
113
349
|
class={itemsClass ?? 'virtual-list-items'}
|
|
114
|
-
style:transform="translateY({
|
|
115
|
-
|
|
116
|
-
|
|
350
|
+
style:transform="translateY({calculateTransformY(
|
|
351
|
+
mode,
|
|
352
|
+
items.length,
|
|
353
|
+
visibleItems().end,
|
|
354
|
+
visibleItems().start,
|
|
355
|
+
calculatedItemHeight
|
|
356
|
+
)}px)"
|
|
117
357
|
>
|
|
118
358
|
{#each mode === 'bottomToTop' ? items
|
|
119
359
|
.slice(visibleItems().start, visibleItems().end)
|
|
120
360
|
.reverse() : items.slice(visibleItems().start, visibleItems().end) as currentItem, i (currentItem?.id ?? i)}
|
|
361
|
+
<!-- Debug output for first item if debug mode is enabled -->
|
|
121
362
|
{#if debug && i === 0}
|
|
122
|
-
{@const debugInfo:
|
|
363
|
+
{@const debugInfo: SvelteVirtualListDebugInfo = {
|
|
123
364
|
visibleItemsCount: visibleItems().end - visibleItems().start,
|
|
124
365
|
startIndex: visibleItems().start,
|
|
125
366
|
endIndex: visibleItems().end,
|
|
@@ -129,12 +370,15 @@
|
|
|
129
370
|
? debugFunction(debugInfo)
|
|
130
371
|
: console.log('Virtual List Debug:', debugInfo)}
|
|
131
372
|
{/if}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
373
|
+
<!-- Render each visible item -->
|
|
374
|
+
<div bind:this={itemElements[i]}>
|
|
375
|
+
{@render renderItem(
|
|
376
|
+
currentItem,
|
|
377
|
+
mode === 'bottomToTop'
|
|
378
|
+
? items.length - (visibleItems().start + i) - 1
|
|
379
|
+
: visibleItems().start + i
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
138
382
|
{/each}
|
|
139
383
|
</div>
|
|
140
384
|
</div>
|
|
@@ -142,6 +386,7 @@
|
|
|
142
386
|
</div>
|
|
143
387
|
|
|
144
388
|
<style>
|
|
389
|
+
/* Container establishes positioning context */
|
|
145
390
|
.virtual-list-container {
|
|
146
391
|
position: relative;
|
|
147
392
|
width: 100%;
|
|
@@ -149,6 +394,7 @@
|
|
|
149
394
|
overflow: hidden;
|
|
150
395
|
}
|
|
151
396
|
|
|
397
|
+
/* Viewport handles scrolling with iOS momentum scroll */
|
|
152
398
|
.virtual-list-viewport {
|
|
153
399
|
position: absolute;
|
|
154
400
|
top: 0;
|
|
@@ -159,12 +405,14 @@
|
|
|
159
405
|
-webkit-overflow-scrolling: touch;
|
|
160
406
|
}
|
|
161
407
|
|
|
408
|
+
/* Content wrapper maintains full scrollable height */
|
|
162
409
|
.virtual-list-content {
|
|
163
410
|
position: relative;
|
|
164
411
|
width: 100%;
|
|
165
412
|
min-height: 100%;
|
|
166
413
|
}
|
|
167
414
|
|
|
415
|
+
/* Items wrapper is translated for virtual scrolling */
|
|
168
416
|
.virtual-list-items {
|
|
169
417
|
position: absolute;
|
|
170
418
|
width: 100%;
|
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { SvelteVirtualListProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A high-performance virtualized list component that efficiently renders large datasets
|
|
4
|
+
* by only mounting DOM nodes for visible items and a small buffer.
|
|
5
|
+
*
|
|
6
|
+
* Props:
|
|
7
|
+
* - `items` - Array of items to render
|
|
8
|
+
* - `defaultEstimatedItemHeight` - Initial height estimate for items (default: 40px)
|
|
9
|
+
* - `mode` - Scroll direction: 'topToBottom' or 'bottomToTop' (default: 'topToBottom')
|
|
10
|
+
* - `debug` - Enable debug logging (default: false)
|
|
11
|
+
* - `bufferSize` - Number of items to render outside visible area (default: 20)
|
|
12
|
+
* - `containerClass` - Custom class for container element
|
|
13
|
+
* - `viewportClass` - Custom class for viewport element
|
|
14
|
+
* - `contentClass` - Custom class for content wrapper
|
|
15
|
+
* - `itemsClass` - Custom class for items wrapper
|
|
16
|
+
* - `debugFunction` - Custom debug logging function
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <SvelteVirtualList
|
|
21
|
+
* items={data}
|
|
22
|
+
* defaultEstimatedItemHeight={40}
|
|
23
|
+
* mode="topToBottom"
|
|
24
|
+
* >
|
|
25
|
+
* {#snippet renderItem(item, index)}
|
|
26
|
+
* <div class="item">{item.text}</div>
|
|
27
|
+
* {/snippet}
|
|
28
|
+
* </SvelteVirtualList>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Features:
|
|
32
|
+
* - Dynamic height calculation
|
|
33
|
+
* - Bidirectional scrolling
|
|
34
|
+
* - Configurable buffer size
|
|
35
|
+
* - Debug mode
|
|
36
|
+
* - Custom styling
|
|
37
|
+
*/
|
|
38
|
+
declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {}, "">;
|
|
3
39
|
type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
|
|
4
40
|
export default SvelteVirtualList;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import SvelteVirtualList from './SvelteVirtualList.svelte';
|
|
2
|
-
import type {
|
|
2
|
+
import type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps } from './types.js';
|
|
3
3
|
export default SvelteVirtualList;
|
|
4
|
-
export type { DebugInfo, Mode, Props };
|
|
4
|
+
export type { SvelteVirtualListDebugInfo as DebugInfo, SvelteVirtualListMode as Mode, SvelteVirtualListProps as Props };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,21 +1,55 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Defines the scroll direction and rendering mode for the virtual list.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {'topToBottom' | 'bottomToTop'} SvelteVirtualListMode
|
|
6
|
+
*/
|
|
7
|
+
export type SvelteVirtualListMode = 'topToBottom' | 'bottomToTop';
|
|
8
|
+
/**
|
|
9
|
+
* Configuration properties for the SvelteVirtualList component.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {Object} SvelteVirtualListProps
|
|
12
|
+
* @property {number} [bufferSize] - Number of items to render outside the visible viewport
|
|
13
|
+
* for smooth scrolling.
|
|
14
|
+
* @property {string} [containerClass] - CSS class to apply to the outer container element.
|
|
15
|
+
* @property {string} [contentClass] - CSS class to apply to the content wrapper element.
|
|
16
|
+
* @property {number} [defaultEstimatedItemHeight] - Initial height estimate for each item in pixels.
|
|
17
|
+
* Used for optimization before actual measurements are available.
|
|
18
|
+
* @property {boolean} [debug] - When true, enables debug mode with additional logging and information.
|
|
19
|
+
* @property {Function} [debugFunction] - Custom callback to handle debug information.
|
|
20
|
+
* Receives a {@link SvelteVirtualListDebugInfo} object.
|
|
21
|
+
* @property {Array<any>} items - The complete array of items to be virtualized.
|
|
22
|
+
* @property {string} [itemsClass] - CSS class to apply to individual item containers.
|
|
23
|
+
* @property {SvelteVirtualListMode} [mode='topToBottom'] - Determines the scroll and render direction.
|
|
24
|
+
* @property {Snippet<[item: any, index: number]>} renderItem - Svelte snippet function that defines
|
|
25
|
+
* how each item should be rendered. Receives the item and its index as arguments.
|
|
26
|
+
* @property {string} [viewportClass] - CSS class to apply to the scrollable viewport element.
|
|
27
|
+
*/
|
|
28
|
+
export type SvelteVirtualListProps = {
|
|
29
|
+
bufferSize?: number;
|
|
8
30
|
containerClass?: string;
|
|
9
|
-
viewportClass?: string;
|
|
10
31
|
contentClass?: string;
|
|
32
|
+
defaultEstimatedItemHeight?: number;
|
|
33
|
+
debug?: boolean;
|
|
34
|
+
debugFunction?: (_info: SvelteVirtualListDebugInfo) => void;
|
|
35
|
+
items: any[];
|
|
11
36
|
itemsClass?: string;
|
|
12
|
-
mode?:
|
|
13
|
-
bufferSize?: number;
|
|
37
|
+
mode?: SvelteVirtualListMode;
|
|
14
38
|
renderItem: Snippet<[item: any, index: number]>;
|
|
39
|
+
viewportClass?: string;
|
|
15
40
|
};
|
|
16
|
-
|
|
17
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Debug information provided by the virtual list during rendering.
|
|
43
|
+
*
|
|
44
|
+
* @typedef {Object} SvelteVirtualListDebugInfo
|
|
45
|
+
* @property {number} endIndex - Index of the last rendered item in the viewport.
|
|
46
|
+
* @property {number} startIndex - Index of the first rendered item in the viewport.
|
|
47
|
+
* @property {number} totalItems - Total number of items in the list.
|
|
48
|
+
* @property {number} visibleItemsCount - Number of items currently visible in the viewport.
|
|
49
|
+
*/
|
|
50
|
+
export type SvelteVirtualListDebugInfo = {
|
|
18
51
|
endIndex: number;
|
|
19
|
-
|
|
52
|
+
startIndex: number;
|
|
20
53
|
totalItems: number;
|
|
54
|
+
visibleItemsCount: number;
|
|
21
55
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SvelteVirtualListMode } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents the internal state of a virtual list component.
|
|
4
|
+
*
|
|
5
|
+
* This type encapsulates all essential properties required to manage the virtual
|
|
6
|
+
* scrolling behavior and rendering optimization of a list component. It tracks both
|
|
7
|
+
* the DOM elements involved and the current scroll metrics.
|
|
8
|
+
*
|
|
9
|
+
* @property {boolean} initialized - Indicates whether the virtual list has completed its initial setup
|
|
10
|
+
* @property {SvelteVirtualListMode} mode - Defines the scrolling behavior ('topToBottom' or 'bottomToTop')
|
|
11
|
+
* @property {HTMLElement | null} containerElement - Reference to the outer container DOM element
|
|
12
|
+
* @property {HTMLElement | null} viewportElement - Reference to the viewport DOM element that clips visible content
|
|
13
|
+
* @property {number} calculatedItemHeight - The computed height of each list item in pixels
|
|
14
|
+
* @property {number} height - Total height of the virtual list container in pixels
|
|
15
|
+
* @property {number} scrollTop - Current vertical scroll position in pixels
|
|
16
|
+
*/
|
|
17
|
+
export type VirtualListState = {
|
|
18
|
+
initialized: boolean;
|
|
19
|
+
mode: SvelteVirtualListMode;
|
|
20
|
+
containerElement: HTMLElement | null;
|
|
21
|
+
viewportElement: HTMLElement | null;
|
|
22
|
+
calculatedItemHeight: number;
|
|
23
|
+
height: number;
|
|
24
|
+
scrollTop: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Collection of setter functions for updating VirtualListState properties.
|
|
28
|
+
*
|
|
29
|
+
* These setters provide a controlled interface for modifying the virtual list's state,
|
|
30
|
+
* ensuring that state updates are handled consistently throughout the component.
|
|
31
|
+
* Each setter is designed to update a single specific aspect of the virtual list's state.
|
|
32
|
+
*
|
|
33
|
+
* @property {Function} setHeight - Updates the total height of the virtual list container
|
|
34
|
+
* @property {Function} setScrollTop - Updates the current scroll position
|
|
35
|
+
* @property {Function} setInitialized - Updates the initialization status of the virtual list
|
|
36
|
+
*/
|
|
37
|
+
export type VirtualListSetters = {
|
|
38
|
+
setHeight: (height: number) => void;
|
|
39
|
+
setScrollTop: (scrollTop: number) => void;
|
|
40
|
+
setInitialized: (initialized: boolean) => void;
|
|
41
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SvelteVirtualListMode } from '../types.js';
|
|
2
|
+
import type { VirtualListSetters, VirtualListState } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Calculates the maximum scroll position for a virtual list.
|
|
5
|
+
*
|
|
6
|
+
* This function determines the maximum scrollable distance by computing the difference
|
|
7
|
+
* between the total content height and the visible container height. This is crucial
|
|
8
|
+
* for maintaining proper scroll boundaries in virtual lists.
|
|
9
|
+
*
|
|
10
|
+
* @param {number} totalItems - The total number of items in the list
|
|
11
|
+
* @param {number} itemHeight - The height of each individual item in pixels
|
|
12
|
+
* @param {number} containerHeight - The visible height of the container in pixels
|
|
13
|
+
* @returns {number} The maximum scroll position in pixels
|
|
14
|
+
*/
|
|
15
|
+
export declare const calculateScrollPosition: (totalItems: number, itemHeight: number, containerHeight: number) => number;
|
|
16
|
+
/**
|
|
17
|
+
* Determines the range of items that should be rendered in the virtual list.
|
|
18
|
+
*
|
|
19
|
+
* This function calculates which items should be visible based on the current scroll position,
|
|
20
|
+
* viewport size, and scroll direction. It includes a buffer zone to enable smooth scrolling
|
|
21
|
+
* and prevent visible gaps during rapid scroll movements.
|
|
22
|
+
*
|
|
23
|
+
* @param {number} scrollTop - Current scroll position in pixels
|
|
24
|
+
* @param {number} viewportHeight - Height of the visible area in pixels
|
|
25
|
+
* @param {number} itemHeight - Height of each list item in pixels
|
|
26
|
+
* @param {number} totalItems - Total number of items in the list
|
|
27
|
+
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
28
|
+
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
29
|
+
* @returns {{ start: number, end: number }} Range of indices to render
|
|
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
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Calculates the CSS transform value for positioning the virtual list items.
|
|
37
|
+
*
|
|
38
|
+
* This function determines the vertical offset needed to position the visible items
|
|
39
|
+
* correctly within the viewport, accounting for the scroll direction and current
|
|
40
|
+
* visible range.
|
|
41
|
+
*
|
|
42
|
+
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
43
|
+
* @param {number} totalItems - Total number of items in the list
|
|
44
|
+
* @param {number} visibleEnd - Index of the last visible item
|
|
45
|
+
* @param {number} visibleStart - Index of the first visible item
|
|
46
|
+
* @param {number} itemHeight - Height of each list item in pixels
|
|
47
|
+
* @returns {number} The calculated transform Y value in pixels
|
|
48
|
+
*/
|
|
49
|
+
export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number) => number;
|
|
50
|
+
/**
|
|
51
|
+
* Updates the virtual list's height and scroll position when necessary.
|
|
52
|
+
*
|
|
53
|
+
* This function handles dynamic updates to the virtual list's dimensions and scroll
|
|
54
|
+
* position, particularly important when the container size changes or when switching
|
|
55
|
+
* scroll directions. When immediate is true, it forces an immediate update of the
|
|
56
|
+
* height and scroll position.
|
|
57
|
+
*
|
|
58
|
+
* @param {VirtualListState} state - Current state of the virtual list
|
|
59
|
+
* @param {VirtualListSetters} setters - State setters for updating list properties
|
|
60
|
+
* @param {boolean} immediate - Whether to perform the update immediately
|
|
61
|
+
*/
|
|
62
|
+
export declare const updateHeightAndScroll: (state: VirtualListState, setters: VirtualListSetters, immediate?: boolean) => void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the maximum scroll position for a virtual list.
|
|
3
|
+
*
|
|
4
|
+
* This function determines the maximum scrollable distance by computing the difference
|
|
5
|
+
* between the total content height and the visible container height. This is crucial
|
|
6
|
+
* for maintaining proper scroll boundaries in virtual lists.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} totalItems - The total number of items in the list
|
|
9
|
+
* @param {number} itemHeight - The height of each individual item in pixels
|
|
10
|
+
* @param {number} containerHeight - The visible height of the container in pixels
|
|
11
|
+
* @returns {number} The maximum scroll position in pixels
|
|
12
|
+
*/
|
|
13
|
+
export const calculateScrollPosition = (totalItems, itemHeight, containerHeight) => {
|
|
14
|
+
const totalHeight = totalItems * itemHeight;
|
|
15
|
+
return totalHeight - containerHeight;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Determines the range of items that should be rendered in the virtual list.
|
|
19
|
+
*
|
|
20
|
+
* This function calculates which items should be visible based on the current scroll position,
|
|
21
|
+
* viewport size, and scroll direction. It includes a buffer zone to enable smooth scrolling
|
|
22
|
+
* and prevent visible gaps during rapid scroll movements.
|
|
23
|
+
*
|
|
24
|
+
* @param {number} scrollTop - Current scroll position in pixels
|
|
25
|
+
* @param {number} viewportHeight - Height of the visible area in pixels
|
|
26
|
+
* @param {number} itemHeight - Height of each list item in pixels
|
|
27
|
+
* @param {number} totalItems - Total number of items in the list
|
|
28
|
+
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
29
|
+
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
30
|
+
* @returns {{ start: number, end: number }} Range of indices to render
|
|
31
|
+
*/
|
|
32
|
+
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
|
|
33
|
+
if (mode === 'bottomToTop') {
|
|
34
|
+
const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
|
|
35
|
+
const bottomIndex = totalItems - Math.floor(scrollTop / itemHeight);
|
|
36
|
+
// Add buffer to both ends
|
|
37
|
+
const start = Math.max(0, bottomIndex - visibleCount - bufferSize);
|
|
38
|
+
const end = Math.min(totalItems, bottomIndex + bufferSize);
|
|
39
|
+
return { start, end };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const start = Math.floor(scrollTop / itemHeight);
|
|
43
|
+
const end = Math.min(totalItems, start + Math.ceil(viewportHeight / itemHeight) + 1);
|
|
44
|
+
// Add buffer to both ends
|
|
45
|
+
return {
|
|
46
|
+
start: Math.max(0, start - bufferSize),
|
|
47
|
+
end: Math.min(totalItems, end + bufferSize)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Calculates the CSS transform value for positioning the virtual list items.
|
|
53
|
+
*
|
|
54
|
+
* This function determines the vertical offset needed to position the visible items
|
|
55
|
+
* correctly within the viewport, accounting for the scroll direction and current
|
|
56
|
+
* visible range.
|
|
57
|
+
*
|
|
58
|
+
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
59
|
+
* @param {number} totalItems - Total number of items in the list
|
|
60
|
+
* @param {number} visibleEnd - Index of the last visible item
|
|
61
|
+
* @param {number} visibleStart - Index of the first visible item
|
|
62
|
+
* @param {number} itemHeight - Height of each list item in pixels
|
|
63
|
+
* @returns {number} The calculated transform Y value in pixels
|
|
64
|
+
*/
|
|
65
|
+
export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight) => {
|
|
66
|
+
return mode === 'bottomToTop'
|
|
67
|
+
? (totalItems - visibleEnd) * itemHeight
|
|
68
|
+
: visibleStart * itemHeight;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Updates the virtual list's height and scroll position when necessary.
|
|
72
|
+
*
|
|
73
|
+
* This function handles dynamic updates to the virtual list's dimensions and scroll
|
|
74
|
+
* position, particularly important when the container size changes or when switching
|
|
75
|
+
* scroll directions. When immediate is true, it forces an immediate update of the
|
|
76
|
+
* height and scroll position.
|
|
77
|
+
*
|
|
78
|
+
* @param {VirtualListState} state - Current state of the virtual list
|
|
79
|
+
* @param {VirtualListSetters} setters - State setters for updating list properties
|
|
80
|
+
* @param {boolean} immediate - Whether to perform the update immediately
|
|
81
|
+
*/
|
|
82
|
+
export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
83
|
+
const { initialized, mode, containerElement, viewportElement, calculatedItemHeight, scrollTop } = state;
|
|
84
|
+
const { setHeight, setScrollTop } = setters;
|
|
85
|
+
if (immediate) {
|
|
86
|
+
if (containerElement && viewportElement && initialized) {
|
|
87
|
+
const newHeight = containerElement.getBoundingClientRect().height;
|
|
88
|
+
setHeight(newHeight);
|
|
89
|
+
if (mode === 'bottomToTop') {
|
|
90
|
+
const visibleIndex = Math.floor(scrollTop / calculatedItemHeight);
|
|
91
|
+
const newScrollTop = visibleIndex * calculatedItemHeight;
|
|
92
|
+
viewportElement.scrollTop = newScrollTop;
|
|
93
|
+
setScrollTop(newScrollTop);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"svelte": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"svelte": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/**/*.test.*",
|
|
17
|
+
"!dist/**/*.spec.*"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"**/*.css"
|
|
21
|
+
],
|
|
5
22
|
"scripts": {
|
|
6
23
|
"dev": "vite dev",
|
|
7
24
|
"build": "vite build && npm run package",
|
|
@@ -17,60 +34,13 @@
|
|
|
17
34
|
"lint:fix": "npm run format && eslint . --fix",
|
|
18
35
|
"format": "prettier --write ."
|
|
19
36
|
},
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"url": "git+https://github.com/humanspeak/svelte-virtual-list.git"
|
|
23
|
-
},
|
|
24
|
-
"author": "Humanspeak, Inc.",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"bugs": {
|
|
27
|
-
"url": "https://github.com/humanspeak/svelte-virtual-list/issues"
|
|
28
|
-
},
|
|
29
|
-
"tags": [
|
|
30
|
-
"svelte",
|
|
31
|
-
"virtual-list",
|
|
32
|
-
"virtual-scroll",
|
|
33
|
-
"virtual-scroller",
|
|
34
|
-
"infinite-scroll",
|
|
35
|
-
"performance",
|
|
36
|
-
"ui-component",
|
|
37
|
-
"svelte5"
|
|
38
|
-
],
|
|
39
|
-
"keywords": [
|
|
40
|
-
"svelte",
|
|
41
|
-
"virtual-list",
|
|
42
|
-
"virtual-scroll",
|
|
43
|
-
"infinite-scroll",
|
|
44
|
-
"performance",
|
|
45
|
-
"ui-component",
|
|
46
|
-
"svelte5",
|
|
47
|
-
"dom-recycling",
|
|
48
|
-
"large-lists",
|
|
49
|
-
"scroll-optimization"
|
|
50
|
-
],
|
|
51
|
-
"homepage": "https://virtuallist.svelte.page",
|
|
52
|
-
"files": [
|
|
53
|
-
"dist",
|
|
54
|
-
"!dist/**/*.test.*",
|
|
55
|
-
"!dist/**/*.spec.*"
|
|
56
|
-
],
|
|
57
|
-
"sideEffects": [
|
|
58
|
-
"**/*.css"
|
|
59
|
-
],
|
|
60
|
-
"svelte": "./dist/index.js",
|
|
61
|
-
"types": "./dist/index.d.ts",
|
|
62
|
-
"type": "module",
|
|
63
|
-
"exports": {
|
|
64
|
-
".": {
|
|
65
|
-
"types": "./dist/index.d.ts",
|
|
66
|
-
"svelte": "./dist/index.js"
|
|
67
|
-
}
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"esm-env": "^1.2.1"
|
|
68
39
|
},
|
|
69
40
|
"peerDependencies": {
|
|
70
41
|
"svelte": "^5.0.0"
|
|
71
42
|
},
|
|
72
43
|
"devDependencies": {
|
|
73
|
-
"@eslint/eslintrc": "^3.2.0",
|
|
74
44
|
"@eslint/js": "^9.17.0",
|
|
75
45
|
"@sveltejs/adapter-auto": "^3.3.1",
|
|
76
46
|
"@sveltejs/kit": "^2.15.1",
|
|
@@ -91,7 +61,6 @@
|
|
|
91
61
|
"prettier": "^3.4.2",
|
|
92
62
|
"prettier-plugin-organize-imports": "^4.1.0",
|
|
93
63
|
"prettier-plugin-svelte": "^3.3.2",
|
|
94
|
-
"prettier-plugin-tailwindcss": "^0.6.9",
|
|
95
64
|
"publint": "^0.2.12",
|
|
96
65
|
"svelte": "^5.16.1",
|
|
97
66
|
"svelte-check": "^4.1.1",
|
|
@@ -99,13 +68,37 @@
|
|
|
99
68
|
"vite": "^6.0.7",
|
|
100
69
|
"vitest": "^3.0.0-beta.3"
|
|
101
70
|
},
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
71
|
+
"keywords": [
|
|
72
|
+
"svelte",
|
|
73
|
+
"virtual-list",
|
|
74
|
+
"virtual-scroll",
|
|
75
|
+
"infinite-scroll",
|
|
76
|
+
"performance",
|
|
77
|
+
"ui-component",
|
|
78
|
+
"svelte5",
|
|
79
|
+
"dom-recycling",
|
|
80
|
+
"large-lists",
|
|
81
|
+
"scroll-optimization"
|
|
82
|
+
],
|
|
83
|
+
"tags": [
|
|
84
|
+
"svelte",
|
|
85
|
+
"virtual-list",
|
|
86
|
+
"virtual-scroll",
|
|
87
|
+
"virtual-scroller",
|
|
88
|
+
"infinite-scroll",
|
|
89
|
+
"performance",
|
|
90
|
+
"ui-component",
|
|
91
|
+
"svelte5"
|
|
92
|
+
],
|
|
93
|
+
"author": "Humanspeak, Inc.",
|
|
94
|
+
"license": "MIT",
|
|
95
|
+
"repository": {
|
|
96
|
+
"type": "git",
|
|
97
|
+
"url": "git+https://github.com/humanspeak/svelte-virtual-list.git"
|
|
106
98
|
},
|
|
107
|
-
"
|
|
108
|
-
|
|
99
|
+
"homepage": "https://virtuallist.svelte.page",
|
|
100
|
+
"bugs": {
|
|
101
|
+
"url": "https://github.com/humanspeak/svelte-virtual-list/issues"
|
|
109
102
|
},
|
|
110
103
|
"funding": {
|
|
111
104
|
"type": "github",
|
|
@@ -113,5 +106,13 @@
|
|
|
113
106
|
},
|
|
114
107
|
"publishConfig": {
|
|
115
108
|
"access": "public"
|
|
109
|
+
},
|
|
110
|
+
"overrides": {
|
|
111
|
+
"@sveltejs/kit": {
|
|
112
|
+
"cookie": "^0.7.0"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"volta": {
|
|
116
|
+
"node": "22.12.0"
|
|
116
117
|
}
|
|
117
118
|
}
|