@humanspeak/svelte-virtual-list 0.2.6 → 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.
- package/README.md +14 -2
- package/dist/SvelteVirtualList.svelte +619 -179
- package/dist/SvelteVirtualList.svelte.d.ts +156 -65
- 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/test/TestComponent.svelte +78 -0
- package/dist/reactive-height-manager/test/TestComponent.svelte.d.ts +23 -0
- package/dist/reactive-height-manager/types.d.ts +41 -0
- package/dist/reactive-height-manager/types.js +1 -0
- package/dist/types.d.ts +24 -5
- package/dist/utils/heightCalculation.d.ts +18 -8
- package/dist/utils/heightCalculation.js +18 -11
- package/dist/utils/heightChangeDetection.d.ts +12 -0
- package/dist/utils/heightChangeDetection.js +20 -0
- package/dist/utils/resizeObserver.d.ts +89 -0
- package/dist/utils/resizeObserver.js +119 -0
- package/dist/utils/scrollCalculation.d.ts +47 -0
- package/dist/utils/scrollCalculation.js +167 -0
- package/dist/utils/throttle.d.ts +95 -0
- package/dist/utils/throttle.js +155 -0
- package/dist/utils/types.d.ts +0 -6
- package/dist/utils/virtualList.d.ts +20 -23
- package/dist/utils/virtualList.js +153 -61
- package/dist/utils/virtualListDebug.d.ts +12 -7
- package/dist/utils/virtualListDebug.js +19 -9
- package/package.json +33 -31
|
@@ -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,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;
|
|
@@ -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 (undefined represents removal/unset) */
|
|
12
|
+
readonly newHeight: number | undefined;
|
|
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 {};
|
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:
|
|
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:
|
|
57
|
+
renderItem: Snippet<[item: TItem, index: number]>;
|
|
58
58
|
/**
|
|
59
59
|
* Base test ID for component elements to facilitate testing.
|
|
60
60
|
*/
|
|
@@ -72,7 +72,11 @@ export type SvelteVirtualListProps = {
|
|
|
72
72
|
* @property {number} startIndex - Index of the first rendered item in the viewport.
|
|
73
73
|
* @property {number} totalItems - Total number of items in the list.
|
|
74
74
|
* @property {number} visibleItemsCount - Number of items currently visible in the viewport.
|
|
75
|
-
* @property {number} processedItems - Number of items
|
|
75
|
+
* @property {number} processedItems - Number of items with measured heights in cache.
|
|
76
|
+
* @property {number} averageItemHeight - Current calculated average height per item.
|
|
77
|
+
* @property {boolean} atTop - Whether the list is scrolled to the top position.
|
|
78
|
+
* @property {boolean} atBottom - Whether the list is scrolled to the bottom position.
|
|
79
|
+
* @property {number} totalHeight - Total calculated height of all items in the list.
|
|
76
80
|
*/
|
|
77
81
|
export type SvelteVirtualListDebugInfo = {
|
|
78
82
|
endIndex: number;
|
|
@@ -81,8 +85,14 @@ export type SvelteVirtualListDebugInfo = {
|
|
|
81
85
|
visibleItemsCount: number;
|
|
82
86
|
processedItems: number;
|
|
83
87
|
averageItemHeight: number;
|
|
88
|
+
atTop: boolean;
|
|
89
|
+
atBottom: boolean;
|
|
90
|
+
totalHeight: number;
|
|
84
91
|
};
|
|
85
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Alignment options for programmatic scrolling.
|
|
94
|
+
*/
|
|
95
|
+
export type SvelteVirtualListScrollAlign = 'auto' | 'top' | 'bottom' | 'nearest';
|
|
86
96
|
/**
|
|
87
97
|
* Options for scrolling to a specific index in the virtual list.
|
|
88
98
|
*/
|
|
@@ -100,3 +110,12 @@ export interface SvelteVirtualListScrollOptions {
|
|
|
100
110
|
* Default options for scrolling.
|
|
101
111
|
*/
|
|
102
112
|
export declare const DEFAULT_SCROLL_OPTIONS: Partial<SvelteVirtualListScrollOptions>;
|
|
113
|
+
export type SvelteVirtualListHeightCacheItem = {
|
|
114
|
+
currentHeight: number;
|
|
115
|
+
dirty: boolean;
|
|
116
|
+
};
|
|
117
|
+
export type SvelteVirtualListHeightCache = Record<number, SvelteVirtualListHeightCacheItem>;
|
|
118
|
+
export type SvelteVirtualListPreviousVisibleRange = {
|
|
119
|
+
start: number;
|
|
120
|
+
end: number;
|
|
121
|
+
};
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SvelteVirtualListMode } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Calculates and updates the average height of visible items with debouncing.
|
|
4
4
|
*
|
|
5
5
|
* This function optimizes performance by:
|
|
6
6
|
* - Debouncing calculations to prevent excessive DOM reads (200ms default)
|
|
7
|
-
* - Caching item heights to minimize recalculations
|
|
7
|
+
* - Caching item heights with dirty tracking to minimize recalculations
|
|
8
8
|
* - Only updating when significant changes are detected (>1px difference)
|
|
9
9
|
* - Early returns to prevent unnecessary processing
|
|
10
10
|
*
|
|
11
11
|
* Implementation details:
|
|
12
12
|
* - Uses a debounce timeout to batch height calculations
|
|
13
13
|
* - Tracks calculation state to prevent concurrent updates
|
|
14
|
-
* - Caches heights in heightCache for reuse
|
|
14
|
+
* - Caches heights in heightCache with currentHeight and dirty flags for reuse
|
|
15
15
|
* - Validates browser environment and calculation state
|
|
16
16
|
* - Checks for meaningful height changes before updates
|
|
17
17
|
*
|
|
18
18
|
* State interactions:
|
|
19
19
|
* - Updates calculatedItemHeight when significant changes occur
|
|
20
20
|
* - Updates lastMeasuredIndex to track progress
|
|
21
|
-
* - Modifies heightCache to store measured heights
|
|
21
|
+
* - Modifies heightCache to store measured heights with dirty tracking
|
|
22
22
|
* - Uses isCalculatingHeight flag for concurrency control
|
|
23
23
|
*
|
|
24
24
|
* Guard clauses:
|
|
@@ -54,13 +54,14 @@ import type { HeightCache } from './types.js';
|
|
|
54
54
|
* - Enhanced debounce timing precision
|
|
55
55
|
* - Added proper cleanup for timeouts
|
|
56
56
|
* - Documented all edge cases and failure modes
|
|
57
|
+
* - Updated to work with new HeightCache structure with dirty tracking
|
|
57
58
|
*
|
|
58
59
|
*
|
|
59
60
|
* @param isCalculatingHeight - Flag to prevent concurrent calculations
|
|
60
61
|
* @param heightUpdateTimeout - Reference to existing update timeout
|
|
61
62
|
* @param visibleItemsGetter - Function to get current visible range
|
|
62
63
|
* @param itemElements - Array of DOM elements to measure
|
|
63
|
-
* @param heightCache - Cache of previously measured heights
|
|
64
|
+
* @param heightCache - Cache of previously measured heights with dirty tracking
|
|
64
65
|
* @param lastMeasuredIndex - Index of last measured element
|
|
65
66
|
* @param calculatedItemHeight - Current average height
|
|
66
67
|
* @param onUpdate - Callback for height updates
|
|
@@ -70,8 +71,17 @@ import type { HeightCache } from './types.js';
|
|
|
70
71
|
export declare const calculateAverageHeightDebounced: (isCalculatingHeight: boolean, heightUpdateTimeout: ReturnType<typeof setTimeout> | null, visibleItemsGetter: () => {
|
|
71
72
|
start: number;
|
|
72
73
|
end: number;
|
|
73
|
-
}, itemElements: HTMLElement[], heightCache:
|
|
74
|
+
}, itemElements: HTMLElement[], heightCache: Record<number, number>, lastMeasuredIndex: number, calculatedItemHeight: number, onUpdate: (result: {
|
|
74
75
|
newHeight: number;
|
|
75
76
|
newLastMeasuredIndex: number;
|
|
76
|
-
updatedHeightCache:
|
|
77
|
-
|
|
77
|
+
updatedHeightCache: Record<number, number>;
|
|
78
|
+
clearedDirtyItems: Set<number>;
|
|
79
|
+
newTotalHeight: number;
|
|
80
|
+
newValidCount: number;
|
|
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;
|
|
@@ -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,26 @@ 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, currentTotalHeight = 0, currentValidCount = 0, mode = 'topToBottom') => {
|
|
75
|
+
if (!BROWSER || isCalculatingHeight)
|
|
75
76
|
return null;
|
|
76
77
|
const visibleRange = visibleItemsGetter();
|
|
77
78
|
const currentIndex = visibleRange.start;
|
|
78
|
-
if (currentIndex === lastMeasuredIndex)
|
|
79
|
+
if (currentIndex === lastMeasuredIndex && dirtyItems.size === 0)
|
|
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, newTotalHeight, newValidCount, heightChanges } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight, dirtyItems, currentTotalHeight, currentValidCount, mode);
|
|
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,
|
|
91
|
+
newTotalHeight,
|
|
92
|
+
newValidCount,
|
|
93
|
+
heightChanges
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
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
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for item resize observation
|
|
3
|
+
*/
|
|
4
|
+
export interface ItemResizeConfig {
|
|
5
|
+
/** Debug mode for logging resize events */
|
|
6
|
+
debug?: boolean;
|
|
7
|
+
/** Callback when items are marked as dirty */
|
|
8
|
+
onItemsDirty?: (dirtyIndices: Set<number>) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for container resize observation
|
|
12
|
+
*/
|
|
13
|
+
export interface ContainerResizeConfig {
|
|
14
|
+
/** Debug mode for logging resize events */
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
/** Callback when container is resized */
|
|
17
|
+
onResize?: (entry: ResizeObserverEntry) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a ResizeObserver for monitoring container size changes.
|
|
21
|
+
*
|
|
22
|
+
* This function creates a ResizeObserver that watches for size changes in the
|
|
23
|
+
* virtual list container and triggers appropriate updates to height and scroll position.
|
|
24
|
+
*
|
|
25
|
+
* @param config - Configuration options
|
|
26
|
+
* @returns ResizeObserver instance
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const containerResizeObserver = createContainerResizeObserver({
|
|
31
|
+
* debug: true,
|
|
32
|
+
* onResize: (entry) => {
|
|
33
|
+
* const newHeight = entry.contentRect.height
|
|
34
|
+
* updateHeightAndScroll(true)
|
|
35
|
+
* }
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* if (containerElement) {
|
|
39
|
+
* containerResizeObserver.observe(containerElement)
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare const createContainerResizeObserver: (config?: ContainerResizeConfig) => ResizeObserver;
|
|
44
|
+
/**
|
|
45
|
+
* Utility to safely observe elements with automatic cleanup.
|
|
46
|
+
*
|
|
47
|
+
* This function provides a safe way to observe elements with a ResizeObserver,
|
|
48
|
+
* handling cases where the observer might not be available or elements might be null.
|
|
49
|
+
*
|
|
50
|
+
* @param observer - ResizeObserver instance
|
|
51
|
+
* @param element - Element to observe
|
|
52
|
+
* @param debug - Whether to log debug information
|
|
53
|
+
* @returns Cleanup function to stop observing
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const cleanup = safeObserve(resizeObserver, element, true)
|
|
58
|
+
*
|
|
59
|
+
* // Later, stop observing
|
|
60
|
+
* cleanup()
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const safeObserve: (observer: ResizeObserver | null, element: HTMLElement | null, debug?: boolean) => (() => void);
|
|
64
|
+
/**
|
|
65
|
+
* Manages multiple ResizeObserver instances with automatic cleanup.
|
|
66
|
+
*
|
|
67
|
+
* This class provides a convenient way to manage multiple ResizeObserver instances
|
|
68
|
+
* and ensures proper cleanup when the component is destroyed.
|
|
69
|
+
*/
|
|
70
|
+
export declare class ResizeObserverManager {
|
|
71
|
+
private observers;
|
|
72
|
+
private cleanupFunctions;
|
|
73
|
+
/**
|
|
74
|
+
* Adds a ResizeObserver to the manager
|
|
75
|
+
*/
|
|
76
|
+
addObserver(observer: ResizeObserver): void;
|
|
77
|
+
/**
|
|
78
|
+
* Adds a cleanup function to be called during cleanup
|
|
79
|
+
*/
|
|
80
|
+
addCleanup(cleanup: () => void): void;
|
|
81
|
+
/**
|
|
82
|
+
* Observes an element with automatic cleanup tracking
|
|
83
|
+
*/
|
|
84
|
+
observe(observer: ResizeObserver, element: HTMLElement, debug?: boolean): void;
|
|
85
|
+
/**
|
|
86
|
+
* Disconnects all observers and runs cleanup functions
|
|
87
|
+
*/
|
|
88
|
+
cleanup(): void;
|
|
89
|
+
}
|