@humanspeak/svelte-virtual-list 0.2.6-beta.7 → 0.3.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,7 +61,7 @@
61
61
  MIT License © Humanspeak, Inc.
62
62
  -->
63
63
 
64
- <script lang="ts">
64
+ <script lang="ts" generics="TItem = any">
65
65
  /**
66
66
  * SvelteVirtualList Implementation Journey
67
67
  *
@@ -182,7 +182,7 @@
182
182
  import.meta.env.DEV && import.meta.env.VITE_SVELTE_VIRTUAL_LIST_DEBUG === 'true'
183
183
  /**
184
184
  * Core configuration props with default values
185
- * @type {SvelteVirtualListProps}
185
+ * @type {SvelteVirtualListProps<TItem>}
186
186
  */
187
187
  const {
188
188
  items = [], // Array of items to be rendered in the virtual list
@@ -197,7 +197,7 @@
197
197
  mode = 'topToBottom', // Scroll direction mode
198
198
  bufferSize = 20, // Number of items to render outside visible area
199
199
  testId // Base test ID for component elements (undefined = no data-testid attributes)
200
- }: SvelteVirtualListProps = $props()
200
+ }: SvelteVirtualListProps<TItem> = $props()
201
201
 
202
202
  /**
203
203
  * DOM References and Core State
@@ -89,6 +89,141 @@
89
89
  * - Progressive size adjustment system
90
90
  */
91
91
  import { type SvelteVirtualListProps, type SvelteVirtualListScrollOptions } from './types.js';
92
+ declare function $$render<TItem = any>(): {
93
+ props: SvelteVirtualListProps<TItem>;
94
+ exports: {
95
+ /**
96
+ * Scrolls the virtual list to the item at the given index.
97
+ *
98
+ * @deprecated This function is deprecated and will be removed in a future version.
99
+ * Use the new scroll method from the component instance instead.
100
+ *
101
+ * @function scrollToIndex
102
+ * @param index The index of the item to scroll to.
103
+ * @param smoothScroll (default: true) Whether to use smooth scrolling.
104
+ * @param shouldThrowOnBounds (default: true) Whether to throw an error if the index is out of bounds.
105
+ *
106
+ * @example
107
+ * // Svelte usage:
108
+ * // In your <script> block:
109
+ * import SvelteVirtualList from '@humanspeak/svelte-virtual-list';
110
+ * let virtualList;
111
+ * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
112
+ *
113
+ * // In your markup:
114
+ * <button onclick={() => virtualList.scrollToIndex(5000)}>
115
+ * Scroll to 5000
116
+ * </button>
117
+ * <SvelteVirtualList {items} bind:this={virtualList}>
118
+ * {#snippet renderItem(item)}
119
+ * <div>{item.text}</div>
120
+ * {/snippet}
121
+ * </SvelteVirtualList>
122
+ *
123
+ * @returns {void}
124
+ * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
125
+ */ scrollToIndex: (index: number, smoothScroll?: boolean, shouldThrowOnBounds?: boolean) => void;
126
+ /**
127
+ * Scrolls the virtual list to the item at the given index using a type-based options approach.
128
+ *
129
+ * @function scroll
130
+ * @param options Configuration options for scrolling behavior.
131
+ *
132
+ * @example
133
+ * // Svelte usage:
134
+ * // In your <script> block:
135
+ * import SvelteVirtualList from './index.js';
136
+ * let virtualList;
137
+ * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
138
+ *
139
+ * <button onclick={() => virtualList.scroll({ index: 5000 })}>
140
+ * Scroll to 5000
141
+ * </button>
142
+ * <SvelteVirtualList {items} bind:this={virtualList}>
143
+ * {#snippet renderItem(item)}
144
+ * <div>{item.text}</div>
145
+ * {/snippet}
146
+ * </SvelteVirtualList>
147
+ *
148
+ * @returns {Promise<void>} Promise that resolves when scrolling is complete
149
+ * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
150
+ */ scroll: (options: SvelteVirtualListScrollOptions) => Promise<void>;
151
+ };
152
+ bindings: "";
153
+ slots: {};
154
+ events: {};
155
+ };
156
+ declare class __sveltets_Render<TItem = any> {
157
+ props(): ReturnType<typeof $$render<TItem>>['props'];
158
+ events(): ReturnType<typeof $$render<TItem>>['events'];
159
+ slots(): ReturnType<typeof $$render<TItem>>['slots'];
160
+ bindings(): "";
161
+ exports(): {
162
+ /**
163
+ * Scrolls the virtual list to the item at the given index.
164
+ *
165
+ * @deprecated This function is deprecated and will be removed in a future version.
166
+ * Use the new scroll method from the component instance instead.
167
+ *
168
+ * @function scrollToIndex
169
+ * @param index The index of the item to scroll to.
170
+ * @param smoothScroll (default: true) Whether to use smooth scrolling.
171
+ * @param shouldThrowOnBounds (default: true) Whether to throw an error if the index is out of bounds.
172
+ *
173
+ * @example
174
+ * // Svelte usage:
175
+ * // In your <script> block:
176
+ * import SvelteVirtualList from '@humanspeak/svelte-virtual-list';
177
+ * let virtualList;
178
+ * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
179
+ *
180
+ * // In your markup:
181
+ * <button onclick={() => virtualList.scrollToIndex(5000)}>
182
+ * Scroll to 5000
183
+ * </button>
184
+ * <SvelteVirtualList {items} bind:this={virtualList}>
185
+ * {#snippet renderItem(item)}
186
+ * <div>{item.text}</div>
187
+ * {/snippet}
188
+ * </SvelteVirtualList>
189
+ *
190
+ * @returns {void}
191
+ * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
192
+ */ scrollToIndex: (index: number, smoothScroll?: boolean, shouldThrowOnBounds?: boolean) => void;
193
+ /**
194
+ * Scrolls the virtual list to the item at the given index using a type-based options approach.
195
+ *
196
+ * @function scroll
197
+ * @param options Configuration options for scrolling behavior.
198
+ *
199
+ * @example
200
+ * // Svelte usage:
201
+ * // In your <script> block:
202
+ * import SvelteVirtualList from './index.js';
203
+ * let virtualList;
204
+ * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
205
+ *
206
+ * <button onclick={() => virtualList.scroll({ index: 5000 })}>
207
+ * Scroll to 5000
208
+ * </button>
209
+ * <SvelteVirtualList {items} bind:this={virtualList}>
210
+ * {#snippet renderItem(item)}
211
+ * <div>{item.text}</div>
212
+ * {/snippet}
213
+ * </SvelteVirtualList>
214
+ *
215
+ * @returns {Promise<void>} Promise that resolves when scrolling is complete
216
+ * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
217
+ */ scroll: (options: SvelteVirtualListScrollOptions) => Promise<void>;
218
+ };
219
+ }
220
+ interface $$IsomorphicComponent {
221
+ new <TItem = any>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TItem>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TItem>['props']>, ReturnType<__sveltets_Render<TItem>['events']>, ReturnType<__sveltets_Render<TItem>['slots']>> & {
222
+ $$bindings?: ReturnType<__sveltets_Render<TItem>['bindings']>;
223
+ } & ReturnType<__sveltets_Render<TItem>['exports']>;
224
+ <TItem = any>(internal: unknown, props: ReturnType<__sveltets_Render<TItem>['props']> & {}): ReturnType<__sveltets_Render<TItem>['exports']>;
225
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
226
+ }
92
227
  /**
93
228
  * SvelteVirtualList
94
229
  *
@@ -151,63 +286,6 @@ import { type SvelteVirtualListProps, type SvelteVirtualListScrollOptions } from
151
286
  *
152
287
  * MIT License © Humanspeak, Inc.
153
288
  */
154
- declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {
155
- /**
156
- * Scrolls the virtual list to the item at the given index.
157
- *
158
- * @deprecated This function is deprecated and will be removed in a future version.
159
- * Use the new scroll method from the component instance instead.
160
- *
161
- * @function scrollToIndex
162
- * @param index The index of the item to scroll to.
163
- * @param smoothScroll (default: true) Whether to use smooth scrolling.
164
- * @param shouldThrowOnBounds (default: true) Whether to throw an error if the index is out of bounds.
165
- *
166
- * @example
167
- * // Svelte usage:
168
- * // In your <script> block:
169
- * import SvelteVirtualList from '@humanspeak/svelte-virtual-list';
170
- * let virtualList;
171
- * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
172
- *
173
- * // In your markup:
174
- * <button onclick={() => virtualList.scrollToIndex(5000)}>
175
- * Scroll to 5000
176
- * </button>
177
- * <SvelteVirtualList {items} bind:this={virtualList}>
178
- * {#snippet renderItem(item)}
179
- * <div>{item.text}</div>
180
- * {/snippet}
181
- * </SvelteVirtualList>
182
- *
183
- * @returns {void}
184
- * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
185
- */ scrollToIndex: (index: number, smoothScroll?: boolean, shouldThrowOnBounds?: boolean) => void;
186
- /**
187
- * Scrolls the virtual list to the item at the given index using a type-based options approach.
188
- *
189
- * @function scroll
190
- * @param options Configuration options for scrolling behavior.
191
- *
192
- * @example
193
- * // Svelte usage:
194
- * // In your <script> block:
195
- * import SvelteVirtualList from './index.js';
196
- * let virtualList;
197
- * const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
198
- *
199
- * <button onclick={() => virtualList.scroll({ index: 5000 })}>
200
- * Scroll to 5000
201
- * </button>
202
- * <SvelteVirtualList {items} bind:this={virtualList}>
203
- * {#snippet renderItem(item)}
204
- * <div>{item.text}</div>
205
- * {/snippet}
206
- * </SvelteVirtualList>
207
- *
208
- * @returns {Promise<void>} Promise that resolves when scrolling is complete
209
- * @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
210
- */ scroll: (options: SvelteVirtualListScrollOptions) => Promise<void>;
211
- }, "">;
212
- type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
289
+ declare const SvelteVirtualList: $$IsomorphicComponent;
290
+ type SvelteVirtualList<TItem = any> = InstanceType<typeof SvelteVirtualList<TItem>>;
213
291
  export default SvelteVirtualList;
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte'
3
+ import { ReactiveHeightManager } from '../ReactiveHeightManager.svelte.js'
4
+ import type { HeightChange, HeightManagerConfig } from '../types.js'
5
+
6
+ interface Props {
7
+ config: HeightManagerConfig
8
+ onReactiveUpdate?: (data: {
9
+ totalHeight: number
10
+ measuredCount: number
11
+ effectRuns: number
12
+ }) => void
13
+ }
14
+
15
+ let { config, onReactiveUpdate }: Props = $props()
16
+
17
+ // Create the manager
18
+ const manager = new ReactiveHeightManager(config)
19
+
20
+ // Derived reactive values (clean, no side effects)
21
+ let currentTotalHeight = $derived(manager.totalHeight)
22
+ let currentMeasuredCount = $derived(manager.measuredCount)
23
+
24
+ // Effect run counter (non-reactive - just for tracking)
25
+ let effectRunCount = 0
26
+
27
+ // Reactive counter for DOM display (separate from effect logic)
28
+ let displayEffectRuns = $state(0)
29
+
30
+ // Simple effect that just notifies - no state modification
31
+ $effect(() => {
32
+ // Read the current values (triggers when manager changes)
33
+ const totalHeight = manager.totalHeight
34
+ const measuredCount = manager.measuredCount
35
+
36
+ // Increment counters
37
+ effectRunCount++
38
+ displayEffectRuns = effectRunCount // Update reactive display
39
+
40
+ // Notify parent with fresh values each time
41
+ onReactiveUpdate?.({
42
+ totalHeight,
43
+ measuredCount,
44
+ effectRuns: effectRunCount
45
+ })
46
+ })
47
+
48
+ // Export methods for testing
49
+ export function processDirtyHeights(changes: HeightChange[]) {
50
+ manager.processDirtyHeights(changes)
51
+ }
52
+
53
+ export function updateItemLength(newLength: number) {
54
+ manager.updateItemLength(newLength)
55
+ }
56
+
57
+ export function setItemHeight(height: number) {
58
+ manager.itemHeight = height
59
+ }
60
+
61
+ export function getReactiveData() {
62
+ return {
63
+ totalHeight: currentTotalHeight,
64
+ measuredCount: currentMeasuredCount,
65
+ effectRuns: displayEffectRuns
66
+ }
67
+ }
68
+
69
+ export function getManager() {
70
+ return manager
71
+ }
72
+ </script>
73
+
74
+ <div data-testid="reactive-test-component">
75
+ <div data-testid="total-height">{currentTotalHeight}</div>
76
+ <div data-testid="measured-count">{currentMeasuredCount}</div>
77
+ <div data-testid="effect-runs">{displayEffectRuns}</div>
78
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ReactiveHeightManager } from '../ReactiveHeightManager.svelte.js';
2
+ import type { HeightChange, HeightManagerConfig } from '../types.js';
3
+ interface Props {
4
+ config: HeightManagerConfig;
5
+ onReactiveUpdate?: (data: {
6
+ totalHeight: number;
7
+ measuredCount: number;
8
+ effectRuns: number;
9
+ }) => void;
10
+ }
11
+ declare const TestComponent: import("svelte").Component<Props, {
12
+ processDirtyHeights: (changes: HeightChange[]) => void;
13
+ updateItemLength: (newLength: number) => void;
14
+ setItemHeight: (height: number) => void;
15
+ getReactiveData: () => {
16
+ totalHeight: number;
17
+ measuredCount: number;
18
+ effectRuns: number;
19
+ };
20
+ getManager: () => ReactiveHeightManager;
21
+ }, "">;
22
+ type TestComponent = ReturnType<typeof TestComponent>;
23
+ export default TestComponent;
@@ -8,8 +8,8 @@ export interface HeightChange {
8
8
  readonly index: number;
9
9
  /** The previous height (undefined if first measurement) */
10
10
  readonly oldHeight: number | undefined;
11
- /** The new height measurement */
12
- readonly newHeight: number;
11
+ /** The new height measurement (undefined represents removal/unset) */
12
+ readonly newHeight: number | undefined;
13
13
  }
14
14
  /**
15
15
  * Configuration options for ReactiveHeightManager
package/dist/types.d.ts CHANGED
@@ -10,7 +10,7 @@ export type SvelteVirtualListMode = 'topToBottom' | 'bottomToTop';
10
10
  *
11
11
  * @typedef {Object} SvelteVirtualListProps
12
12
  */
13
- export type SvelteVirtualListProps = {
13
+ export type SvelteVirtualListProps<TItem = any> = {
14
14
  /**
15
15
  * Number of items to render outside the visible viewport for smooth scrolling.
16
16
  * @default 20
@@ -41,7 +41,7 @@ export type SvelteVirtualListProps = {
41
41
  /**
42
42
  * The complete array of items to be virtualized.
43
43
  */
44
- items: any[];
44
+ items: TItem[];
45
45
  /**
46
46
  * CSS class to apply to individual item containers.
47
47
  */
@@ -54,7 +54,7 @@ export type SvelteVirtualListProps = {
54
54
  /**
55
55
  * Svelte snippet function that defines how each item should be rendered. Receives the item and its index as arguments.
56
56
  */
57
- renderItem: Snippet<[item: any, index: number]>;
57
+ renderItem: Snippet<[item: TItem, index: number]>;
58
58
  /**
59
59
  * Base test ID for component elements to facilitate testing.
60
60
  */
@@ -110,28 +110,16 @@ const calculateBottomToTopScrollTarget = (params) => {
110
110
  */
111
111
  const calculateTopToBottomScrollTarget = (params) => {
112
112
  const { align, targetIndex, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
113
- console.log('[DEBUG] calculateTopToBottomScrollTarget:', {
114
- align,
115
- targetIndex,
116
- calculatedItemHeight,
117
- height,
118
- scrollTop,
119
- firstVisibleIndex,
120
- lastVisibleIndex,
121
- heightCacheKeys: Object.keys(heightCache).length
122
- });
123
113
  if (align === 'auto') {
124
114
  // If item is above the viewport, align to top
125
115
  if (targetIndex < firstVisibleIndex) {
126
116
  const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
127
- console.log(`[DEBUG] Item ${targetIndex} above viewport (${firstVisibleIndex}), scrolling to top:`, scrollTarget);
128
117
  return scrollTarget;
129
118
  }
130
119
  // If item is below the viewport, align to bottom
131
120
  else if (targetIndex > lastVisibleIndex - 1) {
132
121
  const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
133
122
  const scrollTarget = Math.max(0, itemBottom - height);
134
- console.log(`[DEBUG] Item ${targetIndex} below viewport (${lastVisibleIndex}), scrolling to bottom:`, scrollTarget);
135
123
  return scrollTarget;
136
124
  }
137
125
  else {
@@ -140,12 +128,6 @@ const calculateTopToBottomScrollTarget = (params) => {
140
128
  const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
141
129
  const distanceToTop = Math.abs(scrollTop - itemTop);
142
130
  const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
143
- console.log(`[DEBUG] Item ${targetIndex} visible, choosing nearest edge:`, {
144
- itemTop,
145
- itemBottom,
146
- distanceToTop,
147
- distanceToBottom
148
- });
149
131
  if (distanceToTop < distanceToBottom) {
150
132
  return itemTop;
151
133
  }
@@ -156,7 +138,6 @@ const calculateTopToBottomScrollTarget = (params) => {
156
138
  }
157
139
  else if (align === 'top') {
158
140
  const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
159
- console.log(`[DEBUG] Align to top for index ${targetIndex}:`, scrollTarget);
160
141
  return scrollTarget;
161
142
  }
162
143
  else if (align === 'bottom') {
@@ -33,14 +33,6 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
33
33
  */
34
34
  export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode, atBottom, wasAtBottomBeforeHeightChange, lastVisibleRange, totalContentHeight) => {
35
35
  if (mode === 'bottomToTop') {
36
- // if (wasAtBottomBeforeHeightChange && lastVisibleRange) {
37
- // // console.log('calculateVisibleRange:wasAtBottomBeforeHeightChange', {
38
- // // lastVisibleRange,
39
- // // atBottom,
40
- // // wasAtBottomBeforeHeightChange
41
- // // })
42
- // return lastVisibleRange
43
- // }
44
36
  const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
45
37
  // In bottomToTop mode, scrollTop represents distance from the total content end
46
38
  // scrollTop = 0 means we're at the beginning (showing first items)
@@ -50,37 +42,16 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
50
42
  // Convert scrollTop to "distance from start" for bottomToTop
51
43
  const distanceFromStart = maxScrollTop - scrollTop;
52
44
  const startIndex = Math.floor(distanceFromStart / itemHeight);
53
- // console.log(
54
- // `[DEBUG] calculateVisibleRange bottomToTop: scrollTop=${scrollTop}, maxScrollTop=${maxScrollTop}, distanceFromStart=${distanceFromStart}, startIndex=${startIndex}`
55
- // )
56
45
  // Safeguard: handle edge cases
57
46
  if (startIndex < 0) {
58
47
  // We're scrolled beyond the maximum (showing first items)
59
48
  const start = 0;
60
49
  const end = Math.min(totalItems, visibleCount + bufferSize * 2);
61
- // console.log(
62
- // `[DEBUG] calculateVisibleRange (startIndex < 0): start=${start}, end=${end}`
63
- // )
64
- // console.log('calculateVisibleRange:startIndex < 0', {
65
- // start,
66
- // end,
67
- // atBottom,
68
- // wasAtBottomBeforeHeightChange,
69
- // lastVisibleRange
70
- // })
71
50
  return { start, end };
72
51
  }
73
52
  // Add buffer to both ends
74
53
  const start = Math.max(0, startIndex - bufferSize);
75
54
  const end = Math.min(totalItems, startIndex + visibleCount + bufferSize);
76
- // console.log(`[DEBUG] calculateVisibleRange result: start=${start}, end=${end}`)
77
- // console.log('calculateVisibleRange:startIndex >= 0', {
78
- // start,
79
- // end,
80
- // atBottom,
81
- // wasAtBottomBeforeHeightChange,
82
- // lastVisibleRange
83
- // })
84
55
  return { start, end };
85
56
  }
86
57
  else {
@@ -102,13 +73,6 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
102
73
  end: adjustedEnd
103
74
  };
104
75
  }
105
- // console.log('calculateVisibleRange:isNotAtBottom', {
106
- // start: Math.max(0, start - bufferSize),
107
- // end: Math.min(totalItems, end + bufferSize),
108
- // atBottom,
109
- // wasAtBottomBeforeHeightChange,
110
- // lastVisibleRange
111
- // })
112
76
  // Add buffer to both ends
113
77
  const finalStart = Math.max(0, start - bufferSize);
114
78
  const finalEnd = Math.min(totalItems, end + bufferSize);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.2.6-beta.7",
3
+ "version": "0.3.1-beta.0",
4
4
  "description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -44,8 +44,7 @@
44
44
  "dist",
45
45
  "!dist/**/*.test.*",
46
46
  "!dist/**/*.spec.*",
47
- "!dist/test/**/*",
48
- "!dist/reactive-height-manager/test/**/*"
47
+ "!dist/test/**/*"
49
48
  ],
50
49
  "scripts": {
51
50
  "build": "vite build && npm run package",
@@ -110,7 +109,7 @@
110
109
  "svelte-check": "^4.3.1",
111
110
  "typescript": "^5.9.2",
112
111
  "typescript-eslint": "^8.39.0",
113
- "vite": "^7.1.0",
112
+ "vite": "^7.1.1",
114
113
  "vitest": "^3.2.4"
115
114
  },
116
115
  "peerDependencies": {