@humanspeak/svelte-virtual-list 0.2.6-beta.5 → 0.2.6-beta.7
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/dist/SvelteVirtualList.svelte +461 -130
- package/dist/SvelteVirtualList.svelte.d.ts +2 -2
- package/dist/reactive-height-manager/INTEGRATION_EXAMPLE.md +136 -0
- package/dist/reactive-height-manager/README.md +324 -0
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.d.ts +116 -0
- package/dist/reactive-height-manager/ReactiveHeightManager.svelte.js +200 -0
- package/dist/reactive-height-manager/benchmark.d.ts +5 -0
- package/dist/reactive-height-manager/benchmark.js +25 -0
- package/dist/reactive-height-manager/index.d.ts +50 -0
- package/dist/reactive-height-manager/index.js +55 -0
- package/dist/reactive-height-manager/types.d.ts +41 -0
- package/dist/reactive-height-manager/types.js +1 -0
- package/dist/utils/heightCalculation.d.ts +8 -1
- package/dist/utils/heightCalculation.js +5 -4
- package/dist/utils/heightChangeDetection.d.ts +12 -0
- package/dist/utils/heightChangeDetection.js +20 -0
- package/dist/utils/resizeObserver.d.ts +0 -33
- package/dist/utils/resizeObserver.js +0 -57
- package/dist/utils/scrollCalculation.d.ts +2 -2
- package/dist/utils/scrollCalculation.js +34 -21
- package/dist/utils/throttle.d.ts +95 -0
- package/dist/utils/throttle.js +155 -0
- package/dist/utils/virtualList.d.ts +11 -14
- package/dist/utils/virtualList.js +100 -53
- package/dist/utils/virtualListDebug.d.ts +1 -1
- package/dist/utils/virtualListDebug.js +1 -2
- package/package.json +22 -21
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactiveHeightManager - A standalone reactive height calculation system
|
|
3
|
+
*
|
|
4
|
+
* Efficiently manages height calculations for virtualized lists by:
|
|
5
|
+
* - Tracking measured vs unmeasured items incrementally
|
|
6
|
+
* - Processing only dirty/changed items (O(dirty) instead of O(all))
|
|
7
|
+
* - Providing reactive state updates using Svelte 5 runes
|
|
8
|
+
* - Maintaining accurate total height calculations
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const manager = new ReactiveHeightManager({ itemLength: 10000, estimatedHeight: 40 })
|
|
13
|
+
*
|
|
14
|
+
* // Process height changes incrementally
|
|
15
|
+
* manager.processDirtyHeights(dirtyResults)
|
|
16
|
+
*
|
|
17
|
+
* // Update calculated item height
|
|
18
|
+
* manager.calculatedItemHeight = 42
|
|
19
|
+
*
|
|
20
|
+
* // Get reactive total height (automatically updates)
|
|
21
|
+
* const totalHeight = manager.totalHeight
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class ReactiveHeightManager {
|
|
25
|
+
// Reactive state using Svelte 5 runes
|
|
26
|
+
_totalMeasuredHeight = $state(0);
|
|
27
|
+
_measuredCount = $state(0);
|
|
28
|
+
_itemLength = $state(0);
|
|
29
|
+
_itemHeight = $state(40);
|
|
30
|
+
_averageHeight = $state(40);
|
|
31
|
+
_totalHeight = $state(0);
|
|
32
|
+
_measuredFlags = null;
|
|
33
|
+
recomputeDerivedHeights() {
|
|
34
|
+
const average = this._measuredCount > 0
|
|
35
|
+
? this._totalMeasuredHeight / this._measuredCount
|
|
36
|
+
: this._itemHeight;
|
|
37
|
+
this._averageHeight = average;
|
|
38
|
+
const unmeasuredCount = this._itemLength - this._measuredCount;
|
|
39
|
+
this._totalHeight = this._totalMeasuredHeight + unmeasuredCount * average;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get total measured height of all measured items
|
|
43
|
+
*/
|
|
44
|
+
get totalMeasuredHeight() {
|
|
45
|
+
return this._totalMeasuredHeight;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get count of items that have been measured
|
|
49
|
+
*/
|
|
50
|
+
get measuredCount() {
|
|
51
|
+
return this._measuredCount;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get total number of items in the list
|
|
55
|
+
*/
|
|
56
|
+
get itemLength() {
|
|
57
|
+
return this._itemLength;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get/Set the height to use for unmeasured items (reactive)
|
|
61
|
+
*/
|
|
62
|
+
get itemHeight() {
|
|
63
|
+
return this._itemHeight;
|
|
64
|
+
}
|
|
65
|
+
set itemHeight(value) {
|
|
66
|
+
this._itemHeight = value;
|
|
67
|
+
this.recomputeDerivedHeights();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the calculated average height of measured items
|
|
71
|
+
* Falls back to itemHeight if no items have been measured yet
|
|
72
|
+
*/
|
|
73
|
+
get averageHeight() {
|
|
74
|
+
return this._averageHeight;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the reactive total height of all items (measured + estimated)
|
|
78
|
+
* This automatically updates when any dependencies change
|
|
79
|
+
*/
|
|
80
|
+
get totalHeight() {
|
|
81
|
+
return this._totalHeight;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create a new ReactiveHeightManager instance
|
|
85
|
+
*
|
|
86
|
+
* @param config - Configuration object containing itemLength and itemHeight
|
|
87
|
+
*/
|
|
88
|
+
constructor(config) {
|
|
89
|
+
this._itemLength = config.itemLength;
|
|
90
|
+
this._itemHeight = config.itemHeight;
|
|
91
|
+
this._measuredFlags = new Uint8Array(Math.max(0, this._itemLength));
|
|
92
|
+
this.recomputeDerivedHeights();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Process height changes incrementally - O(dirty items) instead of O(all items)
|
|
96
|
+
*
|
|
97
|
+
* This is the core optimization: instead of recalculating totals for all items,
|
|
98
|
+
* we only process the items that have changed, maintaining running totals.
|
|
99
|
+
*
|
|
100
|
+
* Accepts any object that has index, oldHeight, and newHeight properties,
|
|
101
|
+
* allowing consumers to pass objects with additional fields.
|
|
102
|
+
*
|
|
103
|
+
* @param dirtyResults - Array of height changes to process
|
|
104
|
+
*/
|
|
105
|
+
processDirtyHeights(dirtyResults) {
|
|
106
|
+
if (dirtyResults.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
// Batch calculate changes to trigger reactivity only once
|
|
109
|
+
let heightDelta = 0;
|
|
110
|
+
let countDelta = 0;
|
|
111
|
+
for (const change of dirtyResults) {
|
|
112
|
+
const { index, oldHeight, newHeight } = change;
|
|
113
|
+
// Remove old contribution if it existed
|
|
114
|
+
if (oldHeight !== undefined) {
|
|
115
|
+
heightDelta -= oldHeight;
|
|
116
|
+
countDelta -= 1;
|
|
117
|
+
}
|
|
118
|
+
// Add new contribution
|
|
119
|
+
if (newHeight !== undefined) {
|
|
120
|
+
heightDelta += newHeight;
|
|
121
|
+
countDelta += 1;
|
|
122
|
+
}
|
|
123
|
+
// Track measured flag (best-effort; full coalescing handled separately)
|
|
124
|
+
if (this._measuredFlags && index >= 0 && index < this._measuredFlags.length) {
|
|
125
|
+
this._measuredFlags[index] = 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (heightDelta === 0 && countDelta === 0)
|
|
129
|
+
return;
|
|
130
|
+
// Apply all changes at once - triggers reactivity only once
|
|
131
|
+
this._totalMeasuredHeight += heightDelta;
|
|
132
|
+
this._measuredCount += countDelta;
|
|
133
|
+
this.recomputeDerivedHeights();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Update when items array length changes
|
|
137
|
+
*
|
|
138
|
+
* @param newLength - New total number of items
|
|
139
|
+
*/
|
|
140
|
+
updateItemLength(newLength) {
|
|
141
|
+
this._itemLength = newLength;
|
|
142
|
+
this._measuredFlags = new Uint8Array(Math.max(0, newLength));
|
|
143
|
+
this.recomputeDerivedHeights();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Update estimated height for unmeasured items
|
|
147
|
+
*
|
|
148
|
+
* @param newEstimatedHeight - New estimated height
|
|
149
|
+
*/
|
|
150
|
+
updateEstimatedHeight(newEstimatedHeight) {
|
|
151
|
+
// Keep a single source of truth for the estimated height
|
|
152
|
+
this._itemHeight = newEstimatedHeight;
|
|
153
|
+
this.recomputeDerivedHeights();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reset all state to initial values
|
|
157
|
+
*
|
|
158
|
+
* Useful for testing or when completely reinitializing the list
|
|
159
|
+
*/
|
|
160
|
+
reset() {
|
|
161
|
+
this._totalMeasuredHeight = 0;
|
|
162
|
+
this._measuredCount = 0;
|
|
163
|
+
this._measuredFlags = this._itemLength > 0 ? new Uint8Array(this._itemLength) : null;
|
|
164
|
+
// Note: Don't reset _itemLength, _itemHeight as they represent configuration, not measured state
|
|
165
|
+
this.recomputeDerivedHeights();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get comprehensive debug information
|
|
169
|
+
*
|
|
170
|
+
* @returns Debug information object
|
|
171
|
+
*/
|
|
172
|
+
getDebugInfo() {
|
|
173
|
+
return {
|
|
174
|
+
totalMeasuredHeight: this._totalMeasuredHeight,
|
|
175
|
+
measuredCount: this._measuredCount,
|
|
176
|
+
itemLength: this._itemLength,
|
|
177
|
+
coveragePercent: this._itemLength > 0 ? (this._measuredCount / this._itemLength) * 100 : 0,
|
|
178
|
+
itemHeight: this._itemHeight,
|
|
179
|
+
averageHeight: this.averageHeight,
|
|
180
|
+
totalHeight: this.totalHeight
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get the percentage of items that have been measured
|
|
185
|
+
*
|
|
186
|
+
* @returns Percentage (0-100) of measured items
|
|
187
|
+
*/
|
|
188
|
+
getMeasurementCoverage() {
|
|
189
|
+
return this.getDebugInfo().coveragePercent;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Check if the manager has sufficient measurement data
|
|
193
|
+
*
|
|
194
|
+
* @param threshold - Minimum percentage of items that should be measured (default: 10)
|
|
195
|
+
* @returns true if coverage meets threshold
|
|
196
|
+
*/
|
|
197
|
+
hasSufficientMeasurements(threshold = 10) {
|
|
198
|
+
return this.getMeasurementCoverage() >= threshold;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHeightManager } from './index.js';
|
|
2
|
+
export function benchmarkHeightManager(itemCount, dirtyCount, iterations = 100) {
|
|
3
|
+
const manager = createHeightManager(itemCount);
|
|
4
|
+
const times = [];
|
|
5
|
+
for (let i = 0; i < iterations; i++) {
|
|
6
|
+
const dirtyResults = Array.from({ length: dirtyCount }, (_, idx) => ({
|
|
7
|
+
index: idx,
|
|
8
|
+
oldHeight: undefined,
|
|
9
|
+
newHeight: 40 + Math.random() * 20
|
|
10
|
+
}));
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
manager.processDirtyHeights(dirtyResults);
|
|
13
|
+
const end = performance.now();
|
|
14
|
+
times.push(end - start);
|
|
15
|
+
manager.reset(); // Reset for next iteration
|
|
16
|
+
}
|
|
17
|
+
const totalTime = times.reduce((sum, time) => sum + time, 0);
|
|
18
|
+
const avgTime = totalTime / iterations;
|
|
19
|
+
const opsPerSecond = 1000 / avgTime; // Convert ms to ops/second
|
|
20
|
+
return {
|
|
21
|
+
avgTime,
|
|
22
|
+
totalTime,
|
|
23
|
+
opsPerSecond
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive Height Manager
|
|
3
|
+
*
|
|
4
|
+
* A standalone, high-performance reactive height calculation system for virtualized lists.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Incremental height processing (O(dirty items) instead of O(all items))
|
|
8
|
+
* - Reactive state management using Svelte 5 runes
|
|
9
|
+
* - Comprehensive performance testing
|
|
10
|
+
* - Framework-agnostic types and interfaces
|
|
11
|
+
* - Memory-efficient measurement tracking
|
|
12
|
+
*
|
|
13
|
+
* @example Basic Usage
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { ReactiveHeightManager } from './reactive-height-manager'
|
|
16
|
+
*
|
|
17
|
+
* const manager = new ReactiveHeightManager({
|
|
18
|
+
* itemLength: 10000,
|
|
19
|
+
* estimatedHeight: 40
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* // Process height changes incrementally
|
|
23
|
+
* manager.processDirtyHeights(heightChanges)
|
|
24
|
+
*
|
|
25
|
+
* // Get reactive total height
|
|
26
|
+
* const totalHeight = manager.getDerivedTotalHeight(calculatedItemHeight)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example Performance Monitoring
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const debugInfo = manager.getDebugInfo()
|
|
32
|
+
* console.log(`Coverage: ${debugInfo.coveragePercent}%`)
|
|
33
|
+
* console.log(`Measured: ${debugInfo.measuredCount}/${debugInfo.itemLength}`)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export { ReactiveHeightManager } from './ReactiveHeightManager.svelte.js';
|
|
37
|
+
import { ReactiveHeightManager as ReactiveHeightManagerType } from './ReactiveHeightManager.svelte.js';
|
|
38
|
+
export type { HeightChange, HeightManagerConfig, HeightManagerDebugInfo } from './types.js';
|
|
39
|
+
export declare const VERSION = "1.0.0";
|
|
40
|
+
export declare const DEFAULT_ESTIMATED_HEIGHT = 40;
|
|
41
|
+
export declare const DEFAULT_MEASUREMENT_THRESHOLD = 10;
|
|
42
|
+
/**
|
|
43
|
+
* Factory function for creating ReactiveHeightManager instances
|
|
44
|
+
* with common configurations
|
|
45
|
+
*/
|
|
46
|
+
export declare function createHeightManager(itemLength: number, itemHeight?: number): ReactiveHeightManagerType;
|
|
47
|
+
/**
|
|
48
|
+
* Performance benchmarking utility
|
|
49
|
+
*/
|
|
50
|
+
export { benchmarkHeightManager } from './benchmark.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive Height Manager
|
|
3
|
+
*
|
|
4
|
+
* A standalone, high-performance reactive height calculation system for virtualized lists.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Incremental height processing (O(dirty items) instead of O(all items))
|
|
8
|
+
* - Reactive state management using Svelte 5 runes
|
|
9
|
+
* - Comprehensive performance testing
|
|
10
|
+
* - Framework-agnostic types and interfaces
|
|
11
|
+
* - Memory-efficient measurement tracking
|
|
12
|
+
*
|
|
13
|
+
* @example Basic Usage
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { ReactiveHeightManager } from './reactive-height-manager'
|
|
16
|
+
*
|
|
17
|
+
* const manager = new ReactiveHeightManager({
|
|
18
|
+
* itemLength: 10000,
|
|
19
|
+
* estimatedHeight: 40
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* // Process height changes incrementally
|
|
23
|
+
* manager.processDirtyHeights(heightChanges)
|
|
24
|
+
*
|
|
25
|
+
* // Get reactive total height
|
|
26
|
+
* const totalHeight = manager.getDerivedTotalHeight(calculatedItemHeight)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example Performance Monitoring
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const debugInfo = manager.getDebugInfo()
|
|
32
|
+
* console.log(`Coverage: ${debugInfo.coveragePercent}%`)
|
|
33
|
+
* console.log(`Measured: ${debugInfo.measuredCount}/${debugInfo.itemLength}`)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
// Export the main class
|
|
37
|
+
export { ReactiveHeightManager } from './ReactiveHeightManager.svelte.js';
|
|
38
|
+
import { ReactiveHeightManager as ReactiveHeightManagerType } from './ReactiveHeightManager.svelte.js';
|
|
39
|
+
// Export version for potential npm package
|
|
40
|
+
export const VERSION = '1.0.0';
|
|
41
|
+
// Export utility constants
|
|
42
|
+
export const DEFAULT_ESTIMATED_HEIGHT = 40;
|
|
43
|
+
export const DEFAULT_MEASUREMENT_THRESHOLD = 10; // percentage
|
|
44
|
+
/**
|
|
45
|
+
* Factory function for creating ReactiveHeightManager instances
|
|
46
|
+
* with common configurations
|
|
47
|
+
*/
|
|
48
|
+
export function createHeightManager(itemLength, itemHeight = DEFAULT_ESTIMATED_HEIGHT) {
|
|
49
|
+
return new ReactiveHeightManagerType({ itemLength, itemHeight });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Performance benchmarking utility
|
|
53
|
+
*/
|
|
54
|
+
// Moved out to keep index clean; re-exported from benchmark.ts
|
|
55
|
+
export { benchmarkHeightManager } from './benchmark.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a height change for a specific item
|
|
3
|
+
* This interface accepts any object with these minimum properties,
|
|
4
|
+
* allowing for additional fields that consumers may include
|
|
5
|
+
*/
|
|
6
|
+
export interface HeightChange {
|
|
7
|
+
/** The index of the item that changed */
|
|
8
|
+
readonly index: number;
|
|
9
|
+
/** The previous height (undefined if first measurement) */
|
|
10
|
+
readonly oldHeight: number | undefined;
|
|
11
|
+
/** The new height measurement */
|
|
12
|
+
readonly newHeight: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Configuration options for ReactiveHeightManager
|
|
16
|
+
*/
|
|
17
|
+
export interface HeightManagerConfig {
|
|
18
|
+
/** Total number of items in the list */
|
|
19
|
+
itemLength: number;
|
|
20
|
+
/** Height to use for unmeasured items */
|
|
21
|
+
itemHeight: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Debug information about the height manager state
|
|
25
|
+
*/
|
|
26
|
+
export interface HeightManagerDebugInfo {
|
|
27
|
+
/** Total measured height of all measured items */
|
|
28
|
+
totalMeasuredHeight: number;
|
|
29
|
+
/** Number of items that have been measured */
|
|
30
|
+
measuredCount: number;
|
|
31
|
+
/** Total number of items in the list */
|
|
32
|
+
itemLength: number;
|
|
33
|
+
/** Percentage of items that have been measured */
|
|
34
|
+
coveragePercent: number;
|
|
35
|
+
/** Current height to use for unmeasured items */
|
|
36
|
+
itemHeight: number;
|
|
37
|
+
/** Calculated average height of measured items */
|
|
38
|
+
averageHeight: number;
|
|
39
|
+
/** Current total height (measured + estimated) */
|
|
40
|
+
totalHeight: number;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SvelteVirtualListMode } from '../types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Calculates and updates the average height of visible items with debouncing.
|
|
3
4
|
*
|
|
@@ -77,4 +78,10 @@ export declare const calculateAverageHeightDebounced: (isCalculatingHeight: bool
|
|
|
77
78
|
clearedDirtyItems: Set<number>;
|
|
78
79
|
newTotalHeight: number;
|
|
79
80
|
newValidCount: number;
|
|
80
|
-
|
|
81
|
+
heightChanges: Array<{
|
|
82
|
+
index: number;
|
|
83
|
+
oldHeight: number;
|
|
84
|
+
newHeight: number;
|
|
85
|
+
delta: number;
|
|
86
|
+
}>;
|
|
87
|
+
}) => void, debounceTime: number, dirtyItems: Set<number>, currentTotalHeight?: number, currentValidCount?: number, mode?: SvelteVirtualListMode) => NodeJS.Timeout | null;
|
|
@@ -71,17 +71,17 @@ import { BROWSER } from 'esm-env';
|
|
|
71
71
|
*/
|
|
72
72
|
export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout, visibleItemsGetter, itemElements, heightCache, lastMeasuredIndex, calculatedItemHeight,
|
|
73
73
|
/* trunk-ignore(eslint/no-unused-vars) */
|
|
74
|
-
onUpdate, debounceTime, dirtyItems, currentTotalHeight = 0, currentValidCount = 0) => {
|
|
74
|
+
onUpdate, debounceTime, dirtyItems, currentTotalHeight = 0, currentValidCount = 0, mode = 'topToBottom') => {
|
|
75
75
|
if (!BROWSER || isCalculatingHeight)
|
|
76
76
|
return null;
|
|
77
77
|
const visibleRange = visibleItemsGetter();
|
|
78
78
|
const currentIndex = visibleRange.start;
|
|
79
|
-
if (currentIndex === lastMeasuredIndex)
|
|
79
|
+
if (currentIndex === lastMeasuredIndex && dirtyItems.size === 0)
|
|
80
80
|
return null;
|
|
81
81
|
if (heightUpdateTimeout)
|
|
82
82
|
clearTimeout(heightUpdateTimeout);
|
|
83
83
|
return setTimeout(() => {
|
|
84
|
-
const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems, newTotalHeight, newValidCount } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight, dirtyItems, currentTotalHeight, currentValidCount);
|
|
84
|
+
const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems, newTotalHeight, newValidCount, heightChanges } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight, dirtyItems, currentTotalHeight, currentValidCount, mode);
|
|
85
85
|
if (Math.abs(newHeight - calculatedItemHeight) > 1 || dirtyItems.size > 0) {
|
|
86
86
|
onUpdate({
|
|
87
87
|
newHeight,
|
|
@@ -89,7 +89,8 @@ onUpdate, debounceTime, dirtyItems, currentTotalHeight = 0, currentValidCount =
|
|
|
89
89
|
updatedHeightCache,
|
|
90
90
|
clearedDirtyItems,
|
|
91
91
|
newTotalHeight,
|
|
92
|
-
newValidCount
|
|
92
|
+
newValidCount,
|
|
93
|
+
heightChanges
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
}, debounceTime);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for detecting significant height changes in virtual list items
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a height change is significant enough to warrant marking an item as dirty
|
|
6
|
+
* @param itemIndex - The index of the item
|
|
7
|
+
* @param newHeight - The new measured height
|
|
8
|
+
* @param heightCache - Existing height cache to compare against
|
|
9
|
+
* @param marginOfError - Height difference threshold (default: 1px)
|
|
10
|
+
* @returns true if the height change is significant
|
|
11
|
+
*/
|
|
12
|
+
export declare const isSignificantHeightChange: (itemIndex: number, newHeight: number, heightCache: Record<number, number>, marginOfError?: number) => boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for detecting significant height changes in virtual list items
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a height change is significant enough to warrant marking an item as dirty
|
|
6
|
+
* @param itemIndex - The index of the item
|
|
7
|
+
* @param newHeight - The new measured height
|
|
8
|
+
* @param heightCache - Existing height cache to compare against
|
|
9
|
+
* @param marginOfError - Height difference threshold (default: 1px)
|
|
10
|
+
* @returns true if the height change is significant
|
|
11
|
+
*/
|
|
12
|
+
export const isSignificantHeightChange = (itemIndex, newHeight, heightCache, marginOfError = 1) => {
|
|
13
|
+
const previousHeight = heightCache[itemIndex];
|
|
14
|
+
if (previousHeight === undefined) {
|
|
15
|
+
// First time seeing this item, mark as significant
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const heightDifference = Math.abs(newHeight - previousHeight);
|
|
19
|
+
return heightDifference > marginOfError;
|
|
20
|
+
};
|
|
@@ -7,39 +7,6 @@ export interface ItemResizeConfig {
|
|
|
7
7
|
/** Callback when items are marked as dirty */
|
|
8
8
|
onItemsDirty?: (dirtyIndices: Set<number>) => void;
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Creates a ResizeObserver for monitoring individual item size changes.
|
|
12
|
-
*
|
|
13
|
-
* This function creates a ResizeObserver that watches for size changes in list items
|
|
14
|
-
* and maintains a dirty set of items that need height recalculation. It's designed
|
|
15
|
-
* specifically for virtual list components where item heights may change dynamically.
|
|
16
|
-
*
|
|
17
|
-
* @param itemElements - Array of item elements to watch
|
|
18
|
-
* @param getVisibleRange - Function to get current visible range
|
|
19
|
-
* @param dirtyItems - Set to track items that need recalculation
|
|
20
|
-
* @param config - Configuration options
|
|
21
|
-
* @returns ResizeObserver instance
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* const itemElements = $state<HTMLElement[]>([])
|
|
26
|
-
* const dirtyItems = $state(new Set<number>())
|
|
27
|
-
*
|
|
28
|
-
* const resizeObserver = createItemResizeObserver(
|
|
29
|
-
* itemElements,
|
|
30
|
-
* () => ({ start: 0, end: 10 }),
|
|
31
|
-
* dirtyItems,
|
|
32
|
-
* {
|
|
33
|
-
* debug: true,
|
|
34
|
-
* onItemsDirty: (indices) => console.log('Items dirty:', indices)
|
|
35
|
-
* }
|
|
36
|
-
* )
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export declare const createItemResizeObserver: (itemElements: HTMLElement[], getVisibleRange: () => {
|
|
40
|
-
start: number;
|
|
41
|
-
end: number;
|
|
42
|
-
}, dirtyItems: Set<number>, config?: ItemResizeConfig) => ResizeObserver;
|
|
43
10
|
/**
|
|
44
11
|
* Configuration for container resize observation
|
|
45
12
|
*/
|
|
@@ -1,60 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a ResizeObserver for monitoring individual item size changes.
|
|
3
|
-
*
|
|
4
|
-
* This function creates a ResizeObserver that watches for size changes in list items
|
|
5
|
-
* and maintains a dirty set of items that need height recalculation. It's designed
|
|
6
|
-
* specifically for virtual list components where item heights may change dynamically.
|
|
7
|
-
*
|
|
8
|
-
* @param itemElements - Array of item elements to watch
|
|
9
|
-
* @param getVisibleRange - Function to get current visible range
|
|
10
|
-
* @param dirtyItems - Set to track items that need recalculation
|
|
11
|
-
* @param config - Configuration options
|
|
12
|
-
* @returns ResizeObserver instance
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* const itemElements = $state<HTMLElement[]>([])
|
|
17
|
-
* const dirtyItems = $state(new Set<number>())
|
|
18
|
-
*
|
|
19
|
-
* const resizeObserver = createItemResizeObserver(
|
|
20
|
-
* itemElements,
|
|
21
|
-
* () => ({ start: 0, end: 10 }),
|
|
22
|
-
* dirtyItems,
|
|
23
|
-
* {
|
|
24
|
-
* debug: true,
|
|
25
|
-
* onItemsDirty: (indices) => console.log('Items dirty:', indices)
|
|
26
|
-
* }
|
|
27
|
-
* )
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export const createItemResizeObserver = (itemElements, getVisibleRange, dirtyItems, config = {}) => {
|
|
31
|
-
const { debug = false, onItemsDirty } = config;
|
|
32
|
-
return new ResizeObserver((entries) => {
|
|
33
|
-
let shouldRecalculate = false;
|
|
34
|
-
const newDirtyItems = new Set();
|
|
35
|
-
if (debug) {
|
|
36
|
-
console.log(`ResizeObserver fired for ${entries.length} entries`);
|
|
37
|
-
}
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
const element = entry.target;
|
|
40
|
-
const elementIndex = itemElements.indexOf(element);
|
|
41
|
-
if (elementIndex !== -1) {
|
|
42
|
-
const visibleRange = getVisibleRange();
|
|
43
|
-
const actualIndex = visibleRange.start + elementIndex;
|
|
44
|
-
// ResizeObserver fired = element resized, so add to dirty queue
|
|
45
|
-
dirtyItems.add(actualIndex);
|
|
46
|
-
newDirtyItems.add(actualIndex);
|
|
47
|
-
shouldRecalculate = true;
|
|
48
|
-
if (debug) {
|
|
49
|
-
console.log(`Item ${actualIndex} marked dirty (resized), queue size: ${dirtyItems.size}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (shouldRecalculate && onItemsDirty) {
|
|
54
|
-
onItemsDirty(newDirtyItems);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
};
|
|
58
1
|
/**
|
|
59
2
|
* Creates a ResizeObserver for monitoring container size changes.
|
|
60
3
|
*
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { SvelteVirtualListMode,
|
|
1
|
+
import type { SvelteVirtualListMode, SvelteVirtualListScrollAlign } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Parameters for calculating scroll target position
|
|
4
4
|
*/
|
|
5
5
|
export interface ScrollTargetParams {
|
|
6
6
|
mode: SvelteVirtualListMode;
|
|
7
|
-
align:
|
|
7
|
+
align: SvelteVirtualListScrollAlign;
|
|
8
8
|
targetIndex: number;
|
|
9
9
|
itemsLength: number;
|
|
10
10
|
calculatedItemHeight: number;
|