@humanspeak/svelte-virtual-list 0.2.5 → 0.2.6-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/dist/SvelteVirtualList.svelte +471 -102
- package/dist/SvelteVirtualList.svelte.d.ts +78 -31
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +30 -0
- package/dist/types.js +8 -1
- package/dist/utils/heightCalculation.d.ts +9 -8
- package/dist/utils/heightCalculation.js +14 -10
- package/dist/utils/types.d.ts +0 -6
- package/dist/utils/virtualList.d.ts +10 -12
- package/dist/utils/virtualList.js +113 -29
- package/package.json +33 -27
|
@@ -76,53 +76,75 @@
|
|
|
76
76
|
* - Height caching and estimation system
|
|
77
77
|
* - Progressive size adjustment system
|
|
78
78
|
*/
|
|
79
|
-
import type
|
|
79
|
+
import { type SvelteVirtualListProps, type SvelteVirtualListScrollOptions } from './types.js';
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
* by only mounting DOM nodes for visible items and a small buffer. Optimized for handling
|
|
83
|
-
* lists of 10k+ items through chunked processing and progressive initialization.
|
|
81
|
+
* SvelteVirtualList
|
|
84
82
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* -
|
|
88
|
-
* - `mode` - Scroll direction: 'topToBottom' or 'bottomToTop' (default: 'topToBottom')
|
|
89
|
-
* - `debug` - Enable debug logging (default: false)
|
|
90
|
-
* - `bufferSize` - Number of items to render outside visible area (default: 20)
|
|
91
|
-
* - `containerClass` - Custom class for container element
|
|
92
|
-
* - `viewportClass` - Custom class for viewport element
|
|
93
|
-
* - `contentClass` - Custom class for content wrapper
|
|
94
|
-
* - `itemsClass` - Custom class for items wrapper
|
|
95
|
-
* - `debugFunction` - Custom debug logging function
|
|
96
|
-
* - `testId` - Base test ID for component elements
|
|
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.
|
|
97
86
|
*
|
|
98
|
-
*
|
|
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)
|
|
100
|
+
*
|
|
101
|
+
* =============================
|
|
102
|
+
* == Usage Example ==
|
|
103
|
+
* =============================
|
|
99
104
|
* ```svelte
|
|
100
105
|
* <SvelteVirtualList
|
|
101
106
|
* items={data}
|
|
102
|
-
*
|
|
103
|
-
*
|
|
107
|
+
* mode="bottomToTop"
|
|
108
|
+
* bind:this={listRef}
|
|
104
109
|
* >
|
|
105
|
-
* {#snippet renderItem(item
|
|
106
|
-
* <div
|
|
110
|
+
* {#snippet renderItem(item)}
|
|
111
|
+
* <div>{item.text}</div>
|
|
107
112
|
* {/snippet}
|
|
108
113
|
* </SvelteVirtualList>
|
|
109
114
|
* ```
|
|
110
115
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* -
|
|
115
|
-
* -
|
|
116
|
-
* -
|
|
117
|
-
* -
|
|
118
|
-
* -
|
|
119
|
-
* -
|
|
120
|
-
* -
|
|
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.
|
|
121
140
|
*/
|
|
122
141
|
declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListProps, {
|
|
123
142
|
/**
|
|
124
143
|
* Scrolls the virtual list to the item at the given index.
|
|
125
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
|
+
*
|
|
126
148
|
* @function scrollToIndex
|
|
127
149
|
* @param index The index of the item to scroll to.
|
|
128
150
|
* @param smoothScroll (default: true) Whether to use smooth scrolling.
|
|
@@ -148,6 +170,31 @@ declare const SvelteVirtualList: import("svelte").Component<SvelteVirtualListPro
|
|
|
148
170
|
* @returns {void}
|
|
149
171
|
* @throws {Error} If the index is out of bounds and shouldThrowOnBounds is true
|
|
150
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;
|
|
151
198
|
}, "">;
|
|
152
199
|
type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
|
|
153
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
|
@@ -82,3 +82,33 @@ export type SvelteVirtualListDebugInfo = {
|
|
|
82
82
|
processedItems: number;
|
|
83
83
|
averageItemHeight: number;
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Alignment options for programmatic scrolling.
|
|
87
|
+
*/
|
|
88
|
+
export type SvelteVirtualListScrollAlign = 'auto' | 'top' | 'bottom' | 'nearest';
|
|
89
|
+
/**
|
|
90
|
+
* Options for scrolling to a specific index in the virtual list.
|
|
91
|
+
*/
|
|
92
|
+
export interface SvelteVirtualListScrollOptions {
|
|
93
|
+
/** The index of the item to scroll to. */
|
|
94
|
+
index: number;
|
|
95
|
+
/** Whether to use smooth scrolling animation. Default: true */
|
|
96
|
+
smoothScroll?: boolean;
|
|
97
|
+
/** Whether to throw an error if the index is out of bounds. Default: true */
|
|
98
|
+
shouldThrowOnBounds?: boolean;
|
|
99
|
+
/** Alignment for the scrolled item: 'auto', 'top', or 'bottom'. Default: 'auto' */
|
|
100
|
+
align?: SvelteVirtualListScrollAlign;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Default options for scrolling.
|
|
104
|
+
*/
|
|
105
|
+
export declare const DEFAULT_SCROLL_OPTIONS: Partial<SvelteVirtualListScrollOptions>;
|
|
106
|
+
export type SvelteVirtualListHeightCacheItem = {
|
|
107
|
+
currentHeight: number;
|
|
108
|
+
dirty: boolean;
|
|
109
|
+
};
|
|
110
|
+
export type SvelteVirtualListHeightCache = Record<number, SvelteVirtualListHeightCacheItem>;
|
|
111
|
+
export type SvelteVirtualListPreviousVisibleRange = {
|
|
112
|
+
start: number;
|
|
113
|
+
end: number;
|
|
114
|
+
};
|
package/dist/types.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import type { HeightCache } from './types.js';
|
|
2
1
|
/**
|
|
3
2
|
* Calculates and updates the average height of visible items with debouncing.
|
|
4
3
|
*
|
|
5
4
|
* This function optimizes performance by:
|
|
6
5
|
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
7
|
-
* - Caching item heights to minimize recalculations
|
|
6
|
+
* - Caching item heights with dirty tracking to minimize recalculations
|
|
8
7
|
* - Only updating when significant changes are detected (>1px difference)
|
|
9
8
|
* - Early returns to prevent unnecessary processing
|
|
10
9
|
*
|
|
11
10
|
* Implementation details:
|
|
12
11
|
* - Uses a debounce timeout to batch height calculations
|
|
13
12
|
* - Tracks calculation state to prevent concurrent updates
|
|
14
|
-
* - Caches heights in heightCache for reuse
|
|
13
|
+
* - Caches heights in heightCache with currentHeight and dirty flags for reuse
|
|
15
14
|
* - Validates browser environment and calculation state
|
|
16
15
|
* - Checks for meaningful height changes before updates
|
|
17
16
|
*
|
|
18
17
|
* State interactions:
|
|
19
18
|
* - Updates calculatedItemHeight when significant changes occur
|
|
20
19
|
* - Updates lastMeasuredIndex to track progress
|
|
21
|
-
* - Modifies heightCache to store measured heights
|
|
20
|
+
* - Modifies heightCache to store measured heights with dirty tracking
|
|
22
21
|
* - Uses isCalculatingHeight flag for concurrency control
|
|
23
22
|
*
|
|
24
23
|
* Guard clauses:
|
|
@@ -54,13 +53,14 @@ import type { HeightCache } from './types.js';
|
|
|
54
53
|
* - Enhanced debounce timing precision
|
|
55
54
|
* - Added proper cleanup for timeouts
|
|
56
55
|
* - Documented all edge cases and failure modes
|
|
56
|
+
* - Updated to work with new HeightCache structure with dirty tracking
|
|
57
57
|
*
|
|
58
58
|
*
|
|
59
59
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
60
60
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
61
61
|
* @param visibleItemsGetter - Function to get current visible range
|
|
62
62
|
* @param itemElements - Array of DOM elements to measure
|
|
63
|
-
* @param heightCache - Cache of previously measured heights
|
|
63
|
+
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
64
64
|
* @param lastMeasuredIndex - Index of last measured element
|
|
65
65
|
* @param calculatedItemHeight - Current average height
|
|
66
66
|
* @param onUpdate - Callback for height updates
|
|
@@ -70,8 +70,9 @@ import type { HeightCache } from './types.js';
|
|
|
70
70
|
export declare const calculateAverageHeightDebounced: (isCalculatingHeight: boolean, heightUpdateTimeout: ReturnType<typeof setTimeout> | null, visibleItemsGetter: () => {
|
|
71
71
|
start: number;
|
|
72
72
|
end: number;
|
|
73
|
-
}, itemElements: HTMLElement[], heightCache:
|
|
73
|
+
}, itemElements: HTMLElement[], heightCache: Record<number, number>, lastMeasuredIndex: number, calculatedItemHeight: number, onUpdate: (result: {
|
|
74
74
|
newHeight: number;
|
|
75
75
|
newLastMeasuredIndex: number;
|
|
76
|
-
updatedHeightCache:
|
|
77
|
-
|
|
76
|
+
updatedHeightCache: Record<number, number>;
|
|
77
|
+
clearedDirtyItems: Set<number>;
|
|
78
|
+
}) => void, debounceTime: number, dirtyItems: Set<number>) => NodeJS.Timeout | null;
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { BROWSER } from 'esm-env';
|
|
2
1
|
import { calculateAverageHeight } from './virtualList.js';
|
|
2
|
+
import { BROWSER } from 'esm-env';
|
|
3
3
|
/**
|
|
4
4
|
* Calculates and updates the average height of visible items with debouncing.
|
|
5
5
|
*
|
|
6
6
|
* This function optimizes performance by:
|
|
7
7
|
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
8
|
-
* - Caching item heights to minimize recalculations
|
|
8
|
+
* - Caching item heights with dirty tracking to minimize recalculations
|
|
9
9
|
* - Only updating when significant changes are detected (>1px difference)
|
|
10
10
|
* - Early returns to prevent unnecessary processing
|
|
11
11
|
*
|
|
12
12
|
* Implementation details:
|
|
13
13
|
* - Uses a debounce timeout to batch height calculations
|
|
14
14
|
* - Tracks calculation state to prevent concurrent updates
|
|
15
|
-
* - Caches heights in heightCache for reuse
|
|
15
|
+
* - Caches heights in heightCache with currentHeight and dirty flags for reuse
|
|
16
16
|
* - Validates browser environment and calculation state
|
|
17
17
|
* - Checks for meaningful height changes before updates
|
|
18
18
|
*
|
|
19
19
|
* State interactions:
|
|
20
20
|
* - Updates calculatedItemHeight when significant changes occur
|
|
21
21
|
* - Updates lastMeasuredIndex to track progress
|
|
22
|
-
* - Modifies heightCache to store measured heights
|
|
22
|
+
* - Modifies heightCache to store measured heights with dirty tracking
|
|
23
23
|
* - Uses isCalculatingHeight flag for concurrency control
|
|
24
24
|
*
|
|
25
25
|
* Guard clauses:
|
|
@@ -55,13 +55,14 @@ import { calculateAverageHeight } from './virtualList.js';
|
|
|
55
55
|
* - Enhanced debounce timing precision
|
|
56
56
|
* - Added proper cleanup for timeouts
|
|
57
57
|
* - Documented all edge cases and failure modes
|
|
58
|
+
* - Updated to work with new HeightCache structure with dirty tracking
|
|
58
59
|
*
|
|
59
60
|
*
|
|
60
61
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
61
62
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
62
63
|
* @param visibleItemsGetter - Function to get current visible range
|
|
63
64
|
* @param itemElements - Array of DOM elements to measure
|
|
64
|
-
* @param heightCache - Cache of previously measured heights
|
|
65
|
+
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
65
66
|
* @param lastMeasuredIndex - Index of last measured element
|
|
66
67
|
* @param calculatedItemHeight - Current average height
|
|
67
68
|
* @param onUpdate - Callback for height updates
|
|
@@ -70,20 +71,23 @@ import { calculateAverageHeight } from './virtualList.js';
|
|
|
70
71
|
*/
|
|
71
72
|
export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout, visibleItemsGetter, itemElements, heightCache, lastMeasuredIndex, calculatedItemHeight,
|
|
72
73
|
/* trunk-ignore(eslint/no-unused-vars) */
|
|
73
|
-
onUpdate, debounceTime
|
|
74
|
-
if (!BROWSER || isCalculatingHeight
|
|
74
|
+
onUpdate, debounceTime, dirtyItems) => {
|
|
75
|
+
if (!BROWSER || isCalculatingHeight)
|
|
75
76
|
return null;
|
|
76
77
|
const visibleRange = visibleItemsGetter();
|
|
77
78
|
const currentIndex = visibleRange.start;
|
|
78
79
|
if (currentIndex === lastMeasuredIndex)
|
|
79
80
|
return null;
|
|
81
|
+
if (heightUpdateTimeout)
|
|
82
|
+
clearTimeout(heightUpdateTimeout);
|
|
80
83
|
return setTimeout(() => {
|
|
81
|
-
const { newHeight, newLastMeasuredIndex, updatedHeightCache } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight);
|
|
82
|
-
if (Math.abs(newHeight - calculatedItemHeight) > 1) {
|
|
84
|
+
const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight, dirtyItems);
|
|
85
|
+
if (Math.abs(newHeight - calculatedItemHeight) > 1 || dirtyItems.size > 0) {
|
|
83
86
|
onUpdate({
|
|
84
87
|
newHeight,
|
|
85
88
|
newLastMeasuredIndex,
|
|
86
|
-
updatedHeightCache
|
|
89
|
+
updatedHeightCache,
|
|
90
|
+
clearedDirtyItems
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
}, debounceTime);
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -39,9 +39,3 @@ 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>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SvelteVirtualListMode } from '../types.js';
|
|
1
|
+
import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
|
|
2
2
|
import type { VirtualListSetters, VirtualListState } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Calculates the maximum scroll position for a virtual list.
|
|
@@ -26,12 +26,9 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
|
|
|
26
26
|
* @param {number} totalItems - Total number of items in the list
|
|
27
27
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
28
28
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
29
|
-
* @returns {
|
|
29
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
30
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
|
-
};
|
|
31
|
+
export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => SvelteVirtualListPreviousVisibleRange;
|
|
35
32
|
/**
|
|
36
33
|
* Calculates the CSS transform value for positioning the virtual list items.
|
|
37
34
|
*
|
|
@@ -64,19 +61,19 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
64
61
|
* Calculates the average height of visible items in a virtual list.
|
|
65
62
|
*
|
|
66
63
|
* This function optimizes performance by:
|
|
67
|
-
* 1. Using a height cache to store measured item heights
|
|
64
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
68
65
|
* 2. Only measuring new items not in the cache
|
|
69
66
|
* 3. Calculating a running average of all measured heights
|
|
70
67
|
*
|
|
71
68
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
72
69
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
73
|
-
* @param {
|
|
70
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
74
71
|
* @param {number} currentItemHeight - Current average item height being used
|
|
75
72
|
*
|
|
76
73
|
* @returns {{
|
|
77
74
|
* newHeight: number,
|
|
78
75
|
* newLastMeasuredIndex: number,
|
|
79
|
-
* updatedHeightCache:
|
|
76
|
+
* updatedHeightCache: HeightCache
|
|
80
77
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
81
78
|
*
|
|
82
79
|
* @example
|
|
@@ -89,10 +86,11 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
89
86
|
*/
|
|
90
87
|
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
91
88
|
start: number;
|
|
92
|
-
}, heightCache: Record<number, number>, currentItemHeight: number) => {
|
|
89
|
+
}, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>) => {
|
|
93
90
|
newHeight: number;
|
|
94
91
|
newLastMeasuredIndex: number;
|
|
95
92
|
updatedHeightCache: Record<number, number>;
|
|
93
|
+
clearedDirtyItems: Set<number>;
|
|
96
94
|
};
|
|
97
95
|
/**
|
|
98
96
|
* Processes large arrays in chunks to prevent UI blocking.
|
|
@@ -125,7 +123,7 @@ onComplete: () => void) => Promise<void>;
|
|
|
125
123
|
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
126
124
|
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
127
125
|
*
|
|
128
|
-
* @param {
|
|
126
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
129
127
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
130
128
|
* @param {number} totalItems - Total number of items in the list
|
|
131
129
|
* @param {number} blockSize - Number of items per block
|
|
@@ -141,7 +139,7 @@ export declare const buildBlockSums: (heightCache: Record<number, number>, calcu
|
|
|
141
139
|
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
142
140
|
* - For small indices, falls back to the original logic.
|
|
143
141
|
*
|
|
144
|
-
* @param {
|
|
142
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
145
143
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
146
144
|
* @param {number} idx - The index to scroll to (exclusive)
|
|
147
145
|
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|
|
@@ -29,12 +29,23 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
|
|
|
29
29
|
* @param {number} totalItems - Total number of items in the list
|
|
30
30
|
* @param {number} bufferSize - Number of items to render outside the visible area
|
|
31
31
|
* @param {SvelteVirtualListMode} mode - Scroll direction mode
|
|
32
|
-
* @returns {
|
|
32
|
+
* @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
|
|
33
33
|
*/
|
|
34
34
|
export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
|
|
35
35
|
if (mode === 'bottomToTop') {
|
|
36
36
|
const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
|
|
37
37
|
const bottomIndex = totalItems - Math.floor(scrollTop / itemHeight);
|
|
38
|
+
// Safeguard: if bottomIndex is negative, it means scrollTop is too large for current itemHeight
|
|
39
|
+
// This can happen when itemHeight changes but scrollTop hasn't been corrected yet
|
|
40
|
+
if (bottomIndex < 0) {
|
|
41
|
+
// Calculate what scrollTop should be and use that for visible range
|
|
42
|
+
const totalHeight = totalItems * itemHeight;
|
|
43
|
+
const correctedScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
44
|
+
const correctedBottomIndex = totalItems - Math.floor(correctedScrollTop / itemHeight);
|
|
45
|
+
const start = Math.max(0, correctedBottomIndex - visibleCount - bufferSize);
|
|
46
|
+
const end = Math.min(totalItems, correctedBottomIndex + bufferSize);
|
|
47
|
+
return { start, end };
|
|
48
|
+
}
|
|
38
49
|
// Add buffer to both ends
|
|
39
50
|
const start = Math.max(0, bottomIndex - visibleCount - bufferSize);
|
|
40
51
|
const end = Math.min(totalItems, bottomIndex + bufferSize);
|
|
@@ -43,6 +54,23 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
43
54
|
else {
|
|
44
55
|
const start = Math.floor(scrollTop / itemHeight);
|
|
45
56
|
const end = Math.min(totalItems, start + Math.ceil(viewportHeight / itemHeight) + 1);
|
|
57
|
+
// Safeguard for topToBottom: ensure last item is fully visible when at max scroll
|
|
58
|
+
const totalHeight = totalItems * itemHeight;
|
|
59
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
60
|
+
// Add dynamic tolerance based on item height for browser rendering precision
|
|
61
|
+
const tolerance = Math.max(itemHeight, 10); // At least one full item height or 10px minimum
|
|
62
|
+
const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
|
|
63
|
+
if (isAtBottom) {
|
|
64
|
+
// When at the bottom, ensure we include all items up to the end
|
|
65
|
+
const adjustedEnd = totalItems;
|
|
66
|
+
const visibleItemCount = Math.ceil(viewportHeight / itemHeight) + bufferSize + 1;
|
|
67
|
+
const adjustedStart = Math.max(0, adjustedEnd - visibleItemCount);
|
|
68
|
+
// TopToBottom safeguard is now active
|
|
69
|
+
return {
|
|
70
|
+
start: adjustedStart,
|
|
71
|
+
end: adjustedEnd
|
|
72
|
+
};
|
|
73
|
+
}
|
|
46
74
|
// Add buffer to both ends
|
|
47
75
|
return {
|
|
48
76
|
start: Math.max(0, start - bufferSize),
|
|
@@ -65,9 +93,15 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
65
93
|
* @returns {number} The calculated transform Y value in pixels
|
|
66
94
|
*/
|
|
67
95
|
export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
if (mode === 'bottomToTop') {
|
|
97
|
+
// In bottomToTop mode, we need to position the container so that
|
|
98
|
+
// the first visible item (visibleStart) aligns with its correct position
|
|
99
|
+
// from the bottom of the total content
|
|
100
|
+
return (totalItems - visibleEnd) * itemHeight;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return visibleStart * itemHeight;
|
|
104
|
+
}
|
|
71
105
|
};
|
|
72
106
|
/**
|
|
73
107
|
* Updates the virtual list's height and scroll position when necessary.
|
|
@@ -101,19 +135,19 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
101
135
|
* Calculates the average height of visible items in a virtual list.
|
|
102
136
|
*
|
|
103
137
|
* This function optimizes performance by:
|
|
104
|
-
* 1. Using a height cache to store measured item heights
|
|
138
|
+
* 1. Using a height cache to store measured item heights with dirty tracking
|
|
105
139
|
* 2. Only measuring new items not in the cache
|
|
106
140
|
* 3. Calculating a running average of all measured heights
|
|
107
141
|
*
|
|
108
142
|
* @param {HTMLElement[]} itemElements - Array of currently rendered item elements
|
|
109
143
|
* @param {{ start: number }} visibleRange - Object containing the start index of visible items
|
|
110
|
-
* @param {
|
|
144
|
+
* @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
|
|
111
145
|
* @param {number} currentItemHeight - Current average item height being used
|
|
112
146
|
*
|
|
113
147
|
* @returns {{
|
|
114
148
|
* newHeight: number,
|
|
115
149
|
* newLastMeasuredIndex: number,
|
|
116
|
-
* updatedHeightCache:
|
|
150
|
+
* updatedHeightCache: HeightCache
|
|
117
151
|
* }} Object containing new calculated height, last measured index, and updated cache
|
|
118
152
|
*
|
|
119
153
|
* @example
|
|
@@ -124,39 +158,89 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
124
158
|
* 40
|
|
125
159
|
* )
|
|
126
160
|
*/
|
|
127
|
-
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
|
|
161
|
+
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight, dirtyItems) => {
|
|
128
162
|
const validElements = itemElements.filter((el) => el);
|
|
129
163
|
if (validElements.length === 0) {
|
|
130
164
|
return {
|
|
131
165
|
newHeight: currentItemHeight,
|
|
132
166
|
newLastMeasuredIndex: visibleRange.start,
|
|
133
|
-
updatedHeightCache: heightCache
|
|
167
|
+
updatedHeightCache: heightCache,
|
|
168
|
+
clearedDirtyItems: new Set()
|
|
134
169
|
};
|
|
135
170
|
}
|
|
136
171
|
const newHeightCache = { ...heightCache };
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
const clearedDirtyItems = new Set();
|
|
173
|
+
// Initialize running totals for O(1) average calculation
|
|
174
|
+
let totalValidHeight = 0;
|
|
175
|
+
let validHeightCount = 0;
|
|
176
|
+
// Calculate initial totals from existing cache
|
|
177
|
+
for (const height of Object.values(heightCache)) {
|
|
178
|
+
if (Number.isFinite(height) && height > 0) {
|
|
179
|
+
totalValidHeight += height;
|
|
180
|
+
validHeightCount++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Process only dirty items if they exist, otherwise process all visible items
|
|
184
|
+
if (dirtyItems.size > 0) {
|
|
185
|
+
// Process only dirty items
|
|
186
|
+
dirtyItems.forEach((itemIndex) => {
|
|
187
|
+
const elementIndex = itemIndex - visibleRange.start;
|
|
188
|
+
const element = validElements[elementIndex];
|
|
189
|
+
if (element && elementIndex >= 0 && elementIndex < validElements.length) {
|
|
190
|
+
try {
|
|
191
|
+
const height = element.getBoundingClientRect().height;
|
|
192
|
+
if (Number.isFinite(height) && height > 0) {
|
|
193
|
+
const oldHeight = newHeightCache[itemIndex];
|
|
194
|
+
// Only update if height actually changed (use smaller tolerance for precision)
|
|
195
|
+
if (!oldHeight || Math.abs(oldHeight - height) >= 0.1) {
|
|
196
|
+
// Update running totals
|
|
197
|
+
if (oldHeight && Number.isFinite(oldHeight) && oldHeight > 0) {
|
|
198
|
+
// Replace old height with new height in running total
|
|
199
|
+
totalValidHeight = totalValidHeight - oldHeight + height;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Add new height to running total
|
|
203
|
+
totalValidHeight += height;
|
|
204
|
+
validHeightCount++;
|
|
205
|
+
}
|
|
206
|
+
newHeightCache[itemIndex] = height;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
clearedDirtyItems.add(itemIndex);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Skip invalid measurements but still clear from dirty
|
|
213
|
+
clearedDirtyItems.add(itemIndex);
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
|
-
|
|
148
|
-
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Original behavior: process all visible items
|
|
220
|
+
validElements.forEach((el, i) => {
|
|
221
|
+
const itemIndex = visibleRange.start + i;
|
|
222
|
+
if (!newHeightCache[itemIndex]) {
|
|
223
|
+
try {
|
|
224
|
+
const height = el.getBoundingClientRect().height;
|
|
225
|
+
if (Number.isFinite(height) && height > 0) {
|
|
226
|
+
// Add new height to running totals
|
|
227
|
+
totalValidHeight += height;
|
|
228
|
+
validHeightCount++;
|
|
229
|
+
newHeightCache[itemIndex] = height;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Skip invalid measurements
|
|
234
|
+
}
|
|
149
235
|
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
//
|
|
153
|
-
const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// O(1) average calculation using running totals!
|
|
154
239
|
return {
|
|
155
|
-
newHeight:
|
|
156
|
-
? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
|
|
157
|
-
: currentItemHeight,
|
|
240
|
+
newHeight: validHeightCount > 0 ? totalValidHeight / validHeightCount : currentItemHeight,
|
|
158
241
|
newLastMeasuredIndex: visibleRange.start,
|
|
159
|
-
updatedHeightCache: newHeightCache
|
|
242
|
+
updatedHeightCache: newHeightCache,
|
|
243
|
+
clearedDirtyItems
|
|
160
244
|
};
|
|
161
245
|
};
|
|
162
246
|
/**
|
|
@@ -206,7 +290,7 @@ onComplete) => {
|
|
|
206
290
|
* Builds a block sum array for fast offset calculation in large virtual lists.
|
|
207
291
|
* Each entry in the array is the total height up to the end of that block (exclusive).
|
|
208
292
|
*
|
|
209
|
-
* @param {
|
|
293
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
210
294
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
211
295
|
* @param {number} totalItems - Total number of items in the list
|
|
212
296
|
* @param {number} blockSize - Number of items per block
|
|
@@ -236,7 +320,7 @@ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, bl
|
|
|
236
320
|
* - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
|
|
237
321
|
* - For small indices, falls back to the original logic.
|
|
238
322
|
*
|
|
239
|
-
* @param {
|
|
323
|
+
* @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
|
|
240
324
|
* @param {number} calculatedItemHeight - Estimated height for unmeasured items
|
|
241
325
|
* @param {number} idx - The index to scroll to (exclusive)
|
|
242
326
|
* @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
|