@humanspeak/svelte-virtual-list 0.0.1 → 0.1.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 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} {itemHeight}>
46
+ <SvelteVirtualList {items}>
52
47
  {#snippet renderItem(item: Item, index: number)}
53
- <div bind:this={measureRef}>
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} {itemHeight} mode="bottomToTop">
57
+ <SvelteVirtualList {items} mode="bottomToTop">
63
58
  {#snippet renderItem(item: Item, index: number)}
64
- <div bind:this={measureRef}>
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
- - `height` - Height of the viewport in pixels
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 type { DebugInfo, Props } from './types.js'
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
- itemHeight = 40,
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
- }: Props = $props()
106
+ }: SvelteVirtualListProps = $props()
18
107
 
108
+ // DOM references and state management
19
109
  let containerElement: HTMLElement
20
110
  let viewportElement: HTMLElement
21
- let scrollTop = $state(0)
22
- let initialized = $state(false)
23
- let height = $state(0)
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
- // Initialize height
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
- // Separate effect for scroll initialization
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
- // Calculate total content height and max scroll position
42
- const totalHeight = items.length * itemHeight
43
- const maxScroll = totalHeight - height + itemHeight / 2 // Add half item height to ensure full scroll
44
-
45
- // Force scroll to bottom
46
- requestAnimationFrame(() => {
47
- viewportElement.scrollTop = maxScroll
48
- scrollTop = maxScroll
49
- initialized = true
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
- let visibleItems = $derived(() => {
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
- if (mode === 'bottomToTop' && !initialized) {
58
- // Show the absolute last items while initializing
59
- return {
60
- start: Math.max(0, items.length - Math.ceil(height / itemHeight)),
61
- end: items.length
62
- }
63
- }
204
+ return calculateVisibleRange(
205
+ scrollTop,
206
+ viewportHeight,
207
+ calculatedItemHeight,
208
+ items.length,
209
+ bufferSize,
210
+ mode
211
+ )
212
+ })
64
213
 
65
- const visibleStart = Math.floor(scrollTop / itemHeight)
66
- const visibleEnd = Math.min(items.length, Math.ceil((scrollTop + height) / itemHeight))
214
+ // Update scroll position when user scrolls
215
+ const handleScroll = () => {
216
+ if (!BROWSER || !viewportElement) return
217
+ scrollTop = viewportElement.scrollTop
218
+ }
67
219
 
68
- if (mode === 'bottomToTop') {
69
- return {
70
- start: Math.max(0, items.length - visibleEnd - bufferSize),
71
- end: Math.min(items.length, items.length - visibleStart + bufferSize)
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
- return {
76
- start: Math.max(0, visibleStart - bufferSize),
77
- end: Math.min(items.length, visibleEnd + bufferSize)
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
- // Handle scroll updates
82
- const handleScroll = () => {
83
- if (!viewportElement) return
84
- scrollTop = viewportElement.scrollTop
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 (containerElement) {
89
- height = containerElement.getBoundingClientRect().height
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
- style:height="{Math.max(height, items.length * itemHeight)}px"
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({mode === 'bottomToTop'
115
- ? (items.length - visibleItems().end) * itemHeight
116
- : visibleItems().start * itemHeight}px)"
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: 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
- {@render renderItem(
133
- currentItem,
134
- mode === 'bottomToTop'
135
- ? items.length - (visibleItems().start + i) - 1
136
- : visibleItems().start + i
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 { Props } from './types.js';
2
- declare const SvelteVirtualList: import("svelte").Component<Props, {}, "">;
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 { DebugInfo, Mode, Props } from './types.js';
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
- export type Mode = 'topToBottom' | 'bottomToTop';
3
- export type Props = {
4
- items: any[];
5
- itemHeight: number;
6
- debug?: boolean;
7
- debugFunction?: (_info: DebugInfo) => void;
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?: Mode;
13
- bufferSize?: number;
37
+ mode?: SvelteVirtualListMode;
14
38
  renderItem: Snippet<[item: any, index: number]>;
39
+ viewportClass?: string;
15
40
  };
16
- export type DebugInfo = {
17
- startIndex: number;
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
- visibleItemsCount: number;
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
+ "version": "0.1.0",
3
4
  "description": "A high-performance virtual list component for Svelte 5 that efficiently renders large datasets through DOM recycling. Perfect for handling infinite scrolling and rendering thousands of items with minimal memory footprint.",
4
- "version": "0.0.1",
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
- "repository": {
21
- "type": "git",
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
- "overrides": {
103
- "@sveltejs/kit": {
104
- "cookie": "^0.7.0"
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
- "volta": {
108
- "node": "22.12.0"
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
  }