@humanspeak/svelte-virtual-list 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +36 -0
- package/dist/SvelteVirtualList.svelte +257 -116
- package/dist/SvelteVirtualList.svelte.d.ts +186 -32
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +58 -16
- package/dist/types.js +8 -1
- package/dist/utils/heightCalculation.d.ts +77 -0
- package/dist/utils/heightCalculation.js +90 -0
- package/dist/utils/raf.d.ts +29 -5
- package/dist/utils/raf.js +45 -19
- package/dist/utils/types.d.ts +6 -0
- package/dist/utils/virtualList.d.ts +37 -4
- package/dist/utils/virtualList.js +81 -9
- package/package.json +41 -33
|
@@ -1,46 +1,200 @@
|
|
|
1
|
-
import type { SvelteVirtualListProps } from './types.js';
|
|
2
1
|
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
* SvelteVirtualList Implementation Journey
|
|
3
|
+
*
|
|
4
|
+
* Evolution & Architecture:
|
|
5
|
+
* 1. Initial Implementation ✓
|
|
6
|
+
* - Basic virtual scrolling with fixed height items
|
|
7
|
+
* - Single direction scrolling (top-to-bottom)
|
|
8
|
+
* - Simple viewport calculations
|
|
9
|
+
*
|
|
10
|
+
* 2. Dynamic Height Enhancement ✓
|
|
11
|
+
* - Added dynamic height calculation system
|
|
12
|
+
* - Implemented debounced measurements
|
|
13
|
+
* - Created height averaging mechanism for performance
|
|
14
|
+
*
|
|
15
|
+
* 3. Bidirectional Scrolling ✓
|
|
16
|
+
* - Added bottomToTop mode
|
|
17
|
+
* - Solved complex initialization issues with flexbox
|
|
18
|
+
* - Implemented careful scroll position management
|
|
19
|
+
*
|
|
20
|
+
* 4. Performance Optimizations ✓
|
|
21
|
+
* - Added element recycling through keyed each blocks
|
|
22
|
+
* - Implemented RAF for smooth animations
|
|
23
|
+
* - Optimized DOM updates with transform translations
|
|
24
|
+
*
|
|
25
|
+
* 5. Stability Improvements ✓
|
|
26
|
+
* - Added ResizeObserver for responsive updates
|
|
27
|
+
* - Implemented proper cleanup on component destruction
|
|
28
|
+
* - Added debug mode for development assistance
|
|
29
|
+
*
|
|
30
|
+
* 6. Large Dataset Optimizations ✓
|
|
31
|
+
* - Implemented chunked processing for 10k+ items
|
|
32
|
+
* - Added progressive initialization system
|
|
33
|
+
* - Deferred height calculations for better initial load
|
|
34
|
+
* - Optimized memory usage for large lists
|
|
35
|
+
* - Added progress tracking for initialization
|
|
36
|
+
*
|
|
37
|
+
* 7. Size Management Improvements ✓
|
|
38
|
+
* - Implemented height caching system for measured items
|
|
39
|
+
* - Added smart height estimation for unmeasured items
|
|
40
|
+
* - Optimized resize handling with debouncing
|
|
41
|
+
* - Added height recalculation on content changes
|
|
42
|
+
* - Implemented progressive height adjustments
|
|
43
|
+
*
|
|
44
|
+
* 8. Code Quality & Maintainability ✓
|
|
45
|
+
* - Extracted debug utilities for better testing
|
|
46
|
+
* - Improved type safety throughout
|
|
47
|
+
* - Added comprehensive documentation
|
|
48
|
+
* - Optimized debug output to reduce noise
|
|
49
|
+
*
|
|
50
|
+
* 9. Future Improvements (Planned)
|
|
51
|
+
* - Add horizontal scrolling support
|
|
52
|
+
* - Implement variable-sized item caching
|
|
53
|
+
* - Add keyboard navigation support
|
|
54
|
+
* - Support for dynamic item updates
|
|
55
|
+
* - Add accessibility enhancements
|
|
56
|
+
*
|
|
57
|
+
* Technical Challenges Solved:
|
|
58
|
+
* - Bottom-to-top scrolling in flexbox layouts
|
|
59
|
+
* - Dynamic height calculations without layout thrashing
|
|
60
|
+
* - Smooth scrolling on various devices
|
|
61
|
+
* - Memory management for large lists
|
|
62
|
+
* - Browser compatibility issues
|
|
63
|
+
* - Performance optimization for 10k+ items
|
|
64
|
+
* - Progressive initialization for large datasets
|
|
65
|
+
* - Debug output optimization
|
|
66
|
+
* - Accurate size calculations with caching
|
|
67
|
+
* - Responsive size adjustments
|
|
68
|
+
*
|
|
69
|
+
* Current Architecture:
|
|
70
|
+
* - Four-layer DOM structure for optimal performance
|
|
71
|
+
* - State management using Svelte 5's $state
|
|
72
|
+
* - Reactive height and scroll calculations
|
|
73
|
+
* - Configurable buffer zones for smooth scrolling
|
|
74
|
+
* - Chunked processing system for large datasets
|
|
75
|
+
* - Separated debug utilities for better testing
|
|
76
|
+
* - Height caching and estimation system
|
|
77
|
+
* - Progressive size adjustment system
|
|
78
|
+
*/
|
|
79
|
+
import { type SvelteVirtualListProps, type SvelteVirtualListScrollOptions } from './types.js';
|
|
80
|
+
/**
|
|
81
|
+
* SvelteVirtualList
|
|
82
|
+
*
|
|
83
|
+
* A high-performance, memory-efficient virtualized list component for Svelte 5.
|
|
84
|
+
* Renders only visible items plus a buffer, supporting dynamic item heights,
|
|
85
|
+
* bi-directional (top-to-bottom and bottom-to-top) scrolling, and programmatic control.
|
|
6
86
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
87
|
+
* =============================
|
|
88
|
+
* == Key Features ==
|
|
89
|
+
* =============================
|
|
90
|
+
* - Dynamic item height support (no fixed height required)
|
|
91
|
+
* - Top-to-bottom and bottom-to-top (chat-style) scrolling
|
|
92
|
+
* - Programmatic scrolling with flexible alignment (top, bottom, auto)
|
|
93
|
+
* - Smooth scrolling and buffer size configuration
|
|
94
|
+
* - SSR compatible and hydration-friendly
|
|
95
|
+
* - TypeScript and Svelte 5 runes/snippets support
|
|
96
|
+
* - Customizable styling via class props
|
|
97
|
+
* - Debug mode for development and testing
|
|
98
|
+
* - Optimized for large lists (10k+ items)
|
|
99
|
+
* - Comprehensive test coverage (unit and E2E)
|
|
19
100
|
*
|
|
20
|
-
*
|
|
101
|
+
* =============================
|
|
102
|
+
* == Usage Example ==
|
|
103
|
+
* =============================
|
|
21
104
|
* ```svelte
|
|
22
105
|
* <SvelteVirtualList
|
|
23
106
|
* items={data}
|
|
24
|
-
*
|
|
25
|
-
*
|
|
107
|
+
* mode="bottomToTop"
|
|
108
|
+
* bind:this={listRef}
|
|
26
109
|
* >
|
|
27
|
-
* {#snippet renderItem(item
|
|
28
|
-
* <div
|
|
110
|
+
* {#snippet renderItem(item)}
|
|
111
|
+
* <div>{item.text}</div>
|
|
29
112
|
* {/snippet}
|
|
30
113
|
* </SvelteVirtualList>
|
|
31
114
|
* ```
|
|
32
115
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
38
|
-
* -
|
|
39
|
-
* -
|
|
40
|
-
* -
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
116
|
+
* =============================
|
|
117
|
+
* == Architecture Notes ==
|
|
118
|
+
* =============================
|
|
119
|
+
* - Uses a four-layer DOM structure for optimal performance
|
|
120
|
+
* - Only visible items + buffer are mounted in the DOM
|
|
121
|
+
* - Height caching and estimation for dynamic content
|
|
122
|
+
* - Handles resize events and dynamic content changes
|
|
123
|
+
* - Supports chunked initialization for very large lists
|
|
124
|
+
* - All scrolling logic is centralized in the scroll() method
|
|
125
|
+
* - Bi-directional support: mode="topToBottom" or "bottomToTop"
|
|
126
|
+
* - Designed for extensibility and easy debugging
|
|
127
|
+
*
|
|
128
|
+
* =============================
|
|
129
|
+
* == For Contributors ==
|
|
130
|
+
* =============================
|
|
131
|
+
* - Please keep all scrolling logic in the scroll() method
|
|
132
|
+
* - Add new features behind feature flags or as optional props
|
|
133
|
+
* - Write tests for all new features (see /test and /tests/scroll)
|
|
134
|
+
* - Use TypeScript and Svelte 5 runes for all new code
|
|
135
|
+
* - Document all exported functions and props with JSDoc
|
|
136
|
+
* - See README.md for API and usage details
|
|
137
|
+
* - For questions, open an issue or discussion on GitHub
|
|
138
|
+
*
|
|
139
|
+
* MIT License © Humanspeak, Inc.
|
|
43
140
|
*/
|
|
44
|
-
declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {
|
|
141
|
+
declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {
|
|
142
|
+
/**
|
|
143
|
+
* Scrolls the virtual list to the item at the given index.
|
|
144
|
+
*
|
|
145
|
+
* @deprecated This function is deprecated and will be removed in a future version.
|
|
146
|
+
* Use the new scroll method from the component instance instead.
|
|
147
|
+
*
|
|
148
|
+
* @function scrollToIndex
|
|
149
|
+
* @param index The index of the item to scroll to.
|
|
150
|
+
* @param smoothScroll (default: true) Whether to use smooth scrolling.
|
|
151
|
+
* @param shouldThrowOnBounds (default: true) Whether to throw an error if the index is out of bounds.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* // Svelte usage:
|
|
155
|
+
* // In your <script> block:
|
|
156
|
+
* import SvelteVirtualList from '@humanspeak/svelte-virtual-list';
|
|
157
|
+
* let virtualList;
|
|
158
|
+
* const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
|
|
159
|
+
*
|
|
160
|
+
* // In your markup:
|
|
161
|
+
* <button onclick={() => virtualList.scrollToIndex(5000)}>
|
|
162
|
+
* Scroll to 5000
|
|
163
|
+
* </button>
|
|
164
|
+
* <SvelteVirtualList {items} bind:this={virtualList}>
|
|
165
|
+
* {#snippet renderItem(item)}
|
|
166
|
+
* <div>{item.text}</div>
|
|
167
|
+
* {/snippet}
|
|
168
|
+
* </SvelteVirtualList>
|
|
169
|
+
*
|
|
170
|
+
* @returns {void}
|
|
171
|
+
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
172
|
+
*/ scrollToIndex: (index: number, smoothScroll?: boolean, shouldThrowOnBounds?: boolean) => void;
|
|
173
|
+
/**
|
|
174
|
+
* Scrolls the virtual list to the item at the given index using a type-based options approach.
|
|
175
|
+
*
|
|
176
|
+
* @function scroll
|
|
177
|
+
* @param options Configuration options for scrolling behavior.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* // Svelte usage:
|
|
181
|
+
* // In your <script> block:
|
|
182
|
+
* import SvelteVirtualList from './index.js';
|
|
183
|
+
* let virtualList;
|
|
184
|
+
* const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `Item ${i}` }));
|
|
185
|
+
*
|
|
186
|
+
* <button onclick={() => virtualList.scroll({ index: 5000 })}>
|
|
187
|
+
* Scroll to 5000
|
|
188
|
+
* </button>
|
|
189
|
+
* <SvelteVirtualList {items} bind:this={virtualList}>
|
|
190
|
+
* {#snippet renderItem(item)}
|
|
191
|
+
* <div>{item.text}</div>
|
|
192
|
+
* {/snippet}
|
|
193
|
+
* </SvelteVirtualList>
|
|
194
|
+
*
|
|
195
|
+
* @returns {void}
|
|
196
|
+
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
197
|
+
*/ scroll: (options: SvelteVirtualListScrollOptions) => void;
|
|
198
|
+
}, "">;
|
|
45
199
|
type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
|
|
46
200
|
export default SvelteVirtualList;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import SvelteVirtualList from './SvelteVirtualList.svelte';
|
|
2
|
-
import type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps } from './types.js';
|
|
2
|
+
import type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps, SvelteVirtualListScrollAlign, SvelteVirtualListScrollOptions } from './types.js';
|
|
3
3
|
export default SvelteVirtualList;
|
|
4
|
-
export type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps };
|
|
4
|
+
export type { SvelteVirtualListDebugInfo, SvelteVirtualListMode, SvelteVirtualListProps, SvelteVirtualListScrollAlign, SvelteVirtualListScrollOptions };
|
package/dist/types.d.ts
CHANGED
|
@@ -9,35 +9,59 @@ export type SvelteVirtualListMode = 'topToBottom' | 'bottomToTop';
|
|
|
9
9
|
* Configuration properties for the SvelteVirtualList component.
|
|
10
10
|
*
|
|
11
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} [testId] - Base test ID for component elements to facilitate testing.
|
|
27
|
-
* @property {string} [viewportClass] - CSS class to apply to the scrollable viewport element.
|
|
28
12
|
*/
|
|
29
13
|
export type SvelteVirtualListProps = {
|
|
14
|
+
/**
|
|
15
|
+
* Number of items to render outside the visible viewport for smooth scrolling.
|
|
16
|
+
* @default 20
|
|
17
|
+
*/
|
|
30
18
|
bufferSize?: number;
|
|
19
|
+
/**
|
|
20
|
+
* CSS class to apply to the outer container element.
|
|
21
|
+
*/
|
|
31
22
|
containerClass?: string;
|
|
23
|
+
/**
|
|
24
|
+
* CSS class to apply to the content wrapper element.
|
|
25
|
+
*/
|
|
32
26
|
contentClass?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Initial height estimate for each item in pixels. Used for optimization before actual measurements are available.
|
|
29
|
+
* @default 40
|
|
30
|
+
*/
|
|
33
31
|
defaultEstimatedItemHeight?: number;
|
|
32
|
+
/**
|
|
33
|
+
* When true, enables debug mode with additional logging and information.
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
34
36
|
debug?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Custom callback to handle debug information. Receives a SvelteVirtualListDebugInfo object.
|
|
39
|
+
*/
|
|
35
40
|
debugFunction?: (_info: SvelteVirtualListDebugInfo) => void;
|
|
41
|
+
/**
|
|
42
|
+
* The complete array of items to be virtualized.
|
|
43
|
+
*/
|
|
36
44
|
items: any[];
|
|
45
|
+
/**
|
|
46
|
+
* CSS class to apply to individual item containers.
|
|
47
|
+
*/
|
|
37
48
|
itemsClass?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Determines the scroll and render direction.
|
|
51
|
+
* @default 'topToBottom'
|
|
52
|
+
*/
|
|
38
53
|
mode?: SvelteVirtualListMode;
|
|
54
|
+
/**
|
|
55
|
+
* Svelte snippet function that defines how each item should be rendered. Receives the item and its index as arguments.
|
|
56
|
+
*/
|
|
39
57
|
renderItem: Snippet<[item: any, index: number]>;
|
|
58
|
+
/**
|
|
59
|
+
* Base test ID for component elements to facilitate testing.
|
|
60
|
+
*/
|
|
40
61
|
testId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* CSS class to apply to the scrollable viewport element.
|
|
64
|
+
*/
|
|
41
65
|
viewportClass?: string;
|
|
42
66
|
};
|
|
43
67
|
/**
|
|
@@ -58,3 +82,21 @@ export type SvelteVirtualListDebugInfo = {
|
|
|
58
82
|
processedItems: number;
|
|
59
83
|
averageItemHeight: number;
|
|
60
84
|
};
|
|
85
|
+
export type SvelteVirtualListScrollAlign = 'auto' | 'top' | 'bottom';
|
|
86
|
+
/**
|
|
87
|
+
* Options for scrolling to a specific index in the virtual list.
|
|
88
|
+
*/
|
|
89
|
+
export interface SvelteVirtualListScrollOptions {
|
|
90
|
+
/** The index of the item to scroll to. */
|
|
91
|
+
index: number;
|
|
92
|
+
/** Whether to use smooth scrolling animation. Default: true */
|
|
93
|
+
smoothScroll?: boolean;
|
|
94
|
+
/** Whether to throw an error if the index is out of bounds. Default: true */
|
|
95
|
+
shouldThrowOnBounds?: boolean;
|
|
96
|
+
/** Alignment for the scrolled item: 'auto', 'top', or 'bottom'. Default: 'auto' */
|
|
97
|
+
align?: SvelteVirtualListScrollAlign;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Default options for scrolling.
|
|
101
|
+
*/
|
|
102
|
+
export declare const DEFAULT_SCROLL_OPTIONS: Partial<SvelteVirtualListScrollOptions>;
|
package/dist/types.js
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { HeightCache } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calculates and updates the average height of visible items with debouncing.
|
|
4
|
+
*
|
|
5
|
+
* This function optimizes performance by:
|
|
6
|
+
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
7
|
+
* - Caching item heights to minimize recalculations
|
|
8
|
+
* - Only updating when significant changes are detected (>1px difference)
|
|
9
|
+
* - Early returns to prevent unnecessary processing
|
|
10
|
+
*
|
|
11
|
+
* Implementation details:
|
|
12
|
+
* - Uses a debounce timeout to batch height calculations
|
|
13
|
+
* - Tracks calculation state to prevent concurrent updates
|
|
14
|
+
* - Caches heights in heightCache for reuse
|
|
15
|
+
* - Validates browser environment and calculation state
|
|
16
|
+
* - Checks for meaningful height changes before updates
|
|
17
|
+
*
|
|
18
|
+
* State interactions:
|
|
19
|
+
* - Updates calculatedItemHeight when significant changes occur
|
|
20
|
+
* - Updates lastMeasuredIndex to track progress
|
|
21
|
+
* - Modifies heightCache to store measured heights
|
|
22
|
+
* - Uses isCalculatingHeight flag for concurrency control
|
|
23
|
+
*
|
|
24
|
+
* Guard clauses:
|
|
25
|
+
* - Returns null if not in browser environment
|
|
26
|
+
* - Returns null if calculation is already in progress
|
|
27
|
+
* - Returns null if update timeout is pending
|
|
28
|
+
* - Returns null if current index matches last measured
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* // Automatically called when items are rendered
|
|
33
|
+
* $effect(() => {
|
|
34
|
+
* if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
35
|
+
* calculateAverageHeightDebounced(
|
|
36
|
+
* false,
|
|
37
|
+
* null,
|
|
38
|
+
* () => getVisibleRange(),
|
|
39
|
+
* itemElements,
|
|
40
|
+
* heightCache,
|
|
41
|
+
* lastMeasuredIndex,
|
|
42
|
+
* currentHeight,
|
|
43
|
+
* handleUpdate
|
|
44
|
+
* )
|
|
45
|
+
* }
|
|
46
|
+
* })
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Change History:
|
|
50
|
+
*
|
|
51
|
+
* 2025-01-22
|
|
52
|
+
* - Added comprehensive test coverage for all guard clauses
|
|
53
|
+
* - Improved browser environment detection
|
|
54
|
+
* - Enhanced debounce timing precision
|
|
55
|
+
* - Added proper cleanup for timeouts
|
|
56
|
+
* - Documented all edge cases and failure modes
|
|
57
|
+
*
|
|
58
|
+
*
|
|
59
|
+
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
60
|
+
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
61
|
+
* @param visibleItemsGetter - Function to get current visible range
|
|
62
|
+
* @param itemElements - Array of DOM elements to measure
|
|
63
|
+
* @param heightCache - Cache of previously measured heights
|
|
64
|
+
* @param lastMeasuredIndex - Index of last measured element
|
|
65
|
+
* @param calculatedItemHeight - Current average height
|
|
66
|
+
* @param onUpdate - Callback for height updates
|
|
67
|
+
* @param debounceTime - Time to wait between calculations (default: 200ms)
|
|
68
|
+
* @returns Timeout object or null if calculation was skipped
|
|
69
|
+
*/
|
|
70
|
+
export declare const calculateAverageHeightDebounced: (isCalculatingHeight: boolean, heightUpdateTimeout: ReturnType<typeof setTimeout> | null, visibleItemsGetter: () => {
|
|
71
|
+
start: number;
|
|
72
|
+
end: number;
|
|
73
|
+
}, itemElements: HTMLElement[], heightCache: HeightCache, lastMeasuredIndex: number, calculatedItemHeight: number, onUpdate: (result: {
|
|
74
|
+
newHeight: number;
|
|
75
|
+
newLastMeasuredIndex: number;
|
|
76
|
+
updatedHeightCache: HeightCache;
|
|
77
|
+
}) => void, debounceTime?: number) => NodeJS.Timeout | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { BROWSER } from 'esm-env';
|
|
2
|
+
import { calculateAverageHeight } from './virtualList.js';
|
|
3
|
+
/**
|
|
4
|
+
* Calculates and updates the average height of visible items with debouncing.
|
|
5
|
+
*
|
|
6
|
+
* This function optimizes performance by:
|
|
7
|
+
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
8
|
+
* - Caching item heights to minimize recalculations
|
|
9
|
+
* - Only updating when significant changes are detected (>1px difference)
|
|
10
|
+
* - Early returns to prevent unnecessary processing
|
|
11
|
+
*
|
|
12
|
+
* Implementation details:
|
|
13
|
+
* - Uses a debounce timeout to batch height calculations
|
|
14
|
+
* - Tracks calculation state to prevent concurrent updates
|
|
15
|
+
* - Caches heights in heightCache for reuse
|
|
16
|
+
* - Validates browser environment and calculation state
|
|
17
|
+
* - Checks for meaningful height changes before updates
|
|
18
|
+
*
|
|
19
|
+
* State interactions:
|
|
20
|
+
* - Updates calculatedItemHeight when significant changes occur
|
|
21
|
+
* - Updates lastMeasuredIndex to track progress
|
|
22
|
+
* - Modifies heightCache to store measured heights
|
|
23
|
+
* - Uses isCalculatingHeight flag for concurrency control
|
|
24
|
+
*
|
|
25
|
+
* Guard clauses:
|
|
26
|
+
* - Returns null if not in browser environment
|
|
27
|
+
* - Returns null if calculation is already in progress
|
|
28
|
+
* - Returns null if update timeout is pending
|
|
29
|
+
* - Returns null if current index matches last measured
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Automatically called when items are rendered
|
|
34
|
+
* $effect(() => {
|
|
35
|
+
* if (BROWSER && itemElements.length > 0 && !isCalculatingHeight) {
|
|
36
|
+
* calculateAverageHeightDebounced(
|
|
37
|
+
* false,
|
|
38
|
+
* null,
|
|
39
|
+
* () => getVisibleRange(),
|
|
40
|
+
* itemElements,
|
|
41
|
+
* heightCache,
|
|
42
|
+
* lastMeasuredIndex,
|
|
43
|
+
* currentHeight,
|
|
44
|
+
* handleUpdate
|
|
45
|
+
* )
|
|
46
|
+
* }
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Change History:
|
|
51
|
+
*
|
|
52
|
+
* 2025-01-22
|
|
53
|
+
* - Added comprehensive test coverage for all guard clauses
|
|
54
|
+
* - Improved browser environment detection
|
|
55
|
+
* - Enhanced debounce timing precision
|
|
56
|
+
* - Added proper cleanup for timeouts
|
|
57
|
+
* - Documented all edge cases and failure modes
|
|
58
|
+
*
|
|
59
|
+
*
|
|
60
|
+
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
61
|
+
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
62
|
+
* @param visibleItemsGetter - Function to get current visible range
|
|
63
|
+
* @param itemElements - Array of DOM elements to measure
|
|
64
|
+
* @param heightCache - Cache of previously measured heights
|
|
65
|
+
* @param lastMeasuredIndex - Index of last measured element
|
|
66
|
+
* @param calculatedItemHeight - Current average height
|
|
67
|
+
* @param onUpdate - Callback for height updates
|
|
68
|
+
* @param debounceTime - Time to wait between calculations (default: 200ms)
|
|
69
|
+
* @returns Timeout object or null if calculation was skipped
|
|
70
|
+
*/
|
|
71
|
+
export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout, visibleItemsGetter, itemElements, heightCache, lastMeasuredIndex, calculatedItemHeight,
|
|
72
|
+
/* trunk-ignore(eslint/no-unused-vars) */
|
|
73
|
+
onUpdate, debounceTime = 200) => {
|
|
74
|
+
if (!BROWSER || isCalculatingHeight || heightUpdateTimeout)
|
|
75
|
+
return null;
|
|
76
|
+
const visibleRange = visibleItemsGetter();
|
|
77
|
+
const currentIndex = visibleRange.start;
|
|
78
|
+
if (currentIndex === lastMeasuredIndex)
|
|
79
|
+
return null;
|
|
80
|
+
return setTimeout(() => {
|
|
81
|
+
const { newHeight, newLastMeasuredIndex, updatedHeightCache } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight);
|
|
82
|
+
if (Math.abs(newHeight - calculatedItemHeight) > 1) {
|
|
83
|
+
onUpdate({
|
|
84
|
+
newHeight,
|
|
85
|
+
newLastMeasuredIndex,
|
|
86
|
+
updatedHeightCache
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}, debounceTime);
|
|
90
|
+
};
|
package/dist/utils/raf.d.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* If a function is already scheduled, the new function will replace it.
|
|
4
|
-
* This helps prevent multiple RAF calls and ensures smooth animations.
|
|
2
|
+
* Creates a requestAnimationFrame (RAF) scheduler for debouncing animation frame callbacks.
|
|
5
3
|
*
|
|
6
|
-
*
|
|
4
|
+
* This factory returns a function that schedules a callback to run on the next animation frame.
|
|
5
|
+
* If multiple calls are made before the frame executes, only the last callback is executed.
|
|
6
|
+
*
|
|
7
|
+
* This is ideal for scenarios where you want to batch or debounce UI updates, such as scroll or resize handlers,
|
|
8
|
+
* without risking global state leaks or cross-component interference.
|
|
9
|
+
*
|
|
10
|
+
* ### Why use this?
|
|
11
|
+
* - Prevents redundant RAF calls and excessive re-renders.
|
|
12
|
+
* - Ensures only the latest callback is executed per frame.
|
|
13
|
+
* - Each scheduler instance is independent—no global state is shared.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Svelte usage example:
|
|
17
|
+
* <script lang="ts">
|
|
18
|
+
* import { createRafScheduler } from './raf.js';
|
|
19
|
+
* const rafSchedule = createRafScheduler();
|
|
20
|
+
* function onScroll() {
|
|
21
|
+
* rafSchedule(() => {
|
|
22
|
+
* // Perform expensive DOM measurement or update
|
|
23
|
+
* });
|
|
24
|
+
* }
|
|
25
|
+
* </script>
|
|
26
|
+
* <div on:scroll={onScroll}> ... </div>
|
|
27
|
+
*
|
|
28
|
+
* @returns {(fn: () => void) => void} A scheduler function. Call with a callback to schedule it for the next animation frame.
|
|
29
|
+
*
|
|
30
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
|
|
7
31
|
*/
|
|
8
|
-
export declare
|
|
32
|
+
export declare function createRafScheduler(): (fn: () => void) => void;
|
package/dist/utils/raf.js
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
|
-
let scheduled = false;
|
|
2
|
-
let callback = null;
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
* If a function is already scheduled, the new function will replace it.
|
|
6
|
-
* This helps prevent multiple RAF calls and ensures smooth animations.
|
|
2
|
+
* Creates a requestAnimationFrame (RAF) scheduler for debouncing animation frame callbacks.
|
|
7
3
|
*
|
|
8
|
-
*
|
|
4
|
+
* This factory returns a function that schedules a callback to run on the next animation frame.
|
|
5
|
+
* If multiple calls are made before the frame executes, only the last callback is executed.
|
|
6
|
+
*
|
|
7
|
+
* This is ideal for scenarios where you want to batch or debounce UI updates, such as scroll or resize handlers,
|
|
8
|
+
* without risking global state leaks or cross-component interference.
|
|
9
|
+
*
|
|
10
|
+
* ### Why use this?
|
|
11
|
+
* - Prevents redundant RAF calls and excessive re-renders.
|
|
12
|
+
* - Ensures only the latest callback is executed per frame.
|
|
13
|
+
* - Each scheduler instance is independent—no global state is shared.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Svelte usage example:
|
|
17
|
+
* <script lang="ts">
|
|
18
|
+
* import { createRafScheduler } from './raf.js';
|
|
19
|
+
* const rafSchedule = createRafScheduler();
|
|
20
|
+
* function onScroll() {
|
|
21
|
+
* rafSchedule(() => {
|
|
22
|
+
* // Perform expensive DOM measurement or update
|
|
23
|
+
* });
|
|
24
|
+
* }
|
|
25
|
+
* </script>
|
|
26
|
+
* <div on:scroll={onScroll}> ... </div>
|
|
27
|
+
*
|
|
28
|
+
* @returns {(fn: () => void) => void} A scheduler function. Call with a callback to schedule it for the next animation frame.
|
|
29
|
+
*
|
|
30
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
|
|
9
31
|
*/
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
32
|
+
export function createRafScheduler() {
|
|
33
|
+
let scheduled = false;
|
|
34
|
+
let callback = null;
|
|
35
|
+
return (fn) => {
|
|
36
|
+
callback = fn;
|
|
37
|
+
if (!scheduled) {
|
|
38
|
+
scheduled = true;
|
|
39
|
+
requestAnimationFrame(() => {
|
|
40
|
+
scheduled = false;
|
|
41
|
+
if (callback) {
|
|
42
|
+
callback();
|
|
43
|
+
callback = null;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -39,3 +39,9 @@ export type VirtualListSetters = {
|
|
|
39
39
|
setScrollTop: (scrollTop: number) => void;
|
|
40
40
|
setInitialized: (initialized: boolean) => void;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Cache for storing measured item heights
|
|
44
|
+
* - Key: Item index in the list
|
|
45
|
+
* - Value: Measured height in pixels
|
|
46
|
+
*/
|
|
47
|
+
export type HeightCache = Record<number, number>;
|