@humanspeak/svelte-virtual-list 0.2.6-beta.0 → 0.2.6-beta.2
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 +159 -208
- package/dist/utils/heightCalculation.d.ts +2 -1
- package/dist/utils/heightCalculation.js +8 -5
- package/dist/utils/initialization.d.ts +103 -0
- package/dist/utils/initialization.js +114 -0
- package/dist/utils/resizeObserver.d.ts +122 -0
- package/dist/utils/resizeObserver.js +176 -0
- package/dist/utils/scrollCalculation.d.ts +47 -0
- package/dist/utils/scrollCalculation.js +173 -0
- package/dist/utils/virtualList.d.ts +2 -1
- package/dist/utils/virtualList.js +107 -23
- package/package.json +4 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { processChunked } from './virtualList.js';
|
|
2
|
+
/**
|
|
3
|
+
* Determines whether to use chunked initialization based on item count and threshold.
|
|
4
|
+
*
|
|
5
|
+
* @param itemCount - Number of items to initialize
|
|
6
|
+
* @param threshold - Threshold above which chunked initialization is used (default: 1000)
|
|
7
|
+
* @returns True if chunked initialization should be used
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const useChunked = shouldUseChunkedInitialization(5000) // true
|
|
12
|
+
* const useImmediate = shouldUseChunkedInitialization(500) // false
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const shouldUseChunkedInitialization = (itemCount, threshold = 1000) => {
|
|
16
|
+
return itemCount > threshold;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Initializes a virtual list with items, using chunked processing for large datasets.
|
|
20
|
+
*
|
|
21
|
+
* This function automatically determines whether to use immediate or chunked initialization
|
|
22
|
+
* based on the number of items. For large datasets, it processes items in chunks to
|
|
23
|
+
* prevent UI blocking, yielding to the main thread between chunks.
|
|
24
|
+
*
|
|
25
|
+
* @param config - Configuration object for initialization
|
|
26
|
+
* @returns Promise that resolves when initialization is complete
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { initializeVirtualList } from './initialization.js'
|
|
31
|
+
*
|
|
32
|
+
* // Initialize with progress tracking
|
|
33
|
+
* await initializeVirtualList({
|
|
34
|
+
* items: largeDataset,
|
|
35
|
+
* chunkSize: 50,
|
|
36
|
+
* onProgress: (processed, total) => {
|
|
37
|
+
* console.log(`Progress: ${processed}/${total}`)
|
|
38
|
+
* },
|
|
39
|
+
* onComplete: () => {
|
|
40
|
+
* console.log('Initialization complete!')
|
|
41
|
+
* }
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const initializeVirtualList = async (config) => {
|
|
46
|
+
const { items, chunkSize, chunkThreshold = 1000, onProgress, onComplete } = config;
|
|
47
|
+
if (!items.length) {
|
|
48
|
+
onComplete?.();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (shouldUseChunkedInitialization(items.length, chunkThreshold)) {
|
|
52
|
+
await processChunked(items, chunkSize, (processedItems) => onProgress?.(processedItems, items.length), () => onComplete?.());
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Immediate initialization for small datasets
|
|
56
|
+
onProgress?.(items.length, items.length);
|
|
57
|
+
onComplete?.();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Calculates the optimal chunk size for initialization based on item count and device capabilities.
|
|
62
|
+
*
|
|
63
|
+
* This function provides a heuristic for determining an appropriate chunk size that balances
|
|
64
|
+
* performance and responsiveness. It considers both the total number of items and the
|
|
65
|
+
* estimated processing time per item.
|
|
66
|
+
*
|
|
67
|
+
* @param itemCount - Total number of items to process
|
|
68
|
+
* @param baseChunkSize - Base chunk size to use as a starting point (default: 50)
|
|
69
|
+
* @returns Recommended chunk size
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const chunkSize = calculateOptimalChunkSize(10000) // Returns optimized chunk size
|
|
74
|
+
* const smallChunkSize = calculateOptimalChunkSize(100) // Returns smaller chunk size
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export const calculateOptimalChunkSize = (itemCount, baseChunkSize = 50) => {
|
|
78
|
+
// For very large datasets, use smaller chunks to maintain responsiveness
|
|
79
|
+
if (itemCount > 50000) {
|
|
80
|
+
return Math.max(25, baseChunkSize / 2);
|
|
81
|
+
}
|
|
82
|
+
// For medium datasets, use base chunk size
|
|
83
|
+
if (itemCount > 5000) {
|
|
84
|
+
return baseChunkSize;
|
|
85
|
+
}
|
|
86
|
+
// For smaller datasets, we can use larger chunks
|
|
87
|
+
if (itemCount > 1000) {
|
|
88
|
+
return Math.min(100, baseChunkSize * 2);
|
|
89
|
+
}
|
|
90
|
+
// For very small datasets, process all at once
|
|
91
|
+
return itemCount;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Creates a progress tracking object for initialization.
|
|
95
|
+
*
|
|
96
|
+
* @param processed - Number of items processed
|
|
97
|
+
* @param total - Total number of items
|
|
98
|
+
* @returns Progress information object
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const progress = createProgressInfo(750, 1000)
|
|
103
|
+
* console.log(progress.percentage) // 75
|
|
104
|
+
* console.log(progress.isComplete) // false
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export const createProgressInfo = (processed, total) => {
|
|
108
|
+
return {
|
|
109
|
+
processed,
|
|
110
|
+
total,
|
|
111
|
+
percentage: total > 0 ? Math.round((processed / total) * 100) : 100,
|
|
112
|
+
isComplete: processed >= total
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
* 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
|
+
/**
|
|
44
|
+
* Configuration for container resize observation
|
|
45
|
+
*/
|
|
46
|
+
export interface ContainerResizeConfig {
|
|
47
|
+
/** Debug mode for logging resize events */
|
|
48
|
+
debug?: boolean;
|
|
49
|
+
/** Callback when container is resized */
|
|
50
|
+
onResize?: (entry: ResizeObserverEntry) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a ResizeObserver for monitoring container size changes.
|
|
54
|
+
*
|
|
55
|
+
* This function creates a ResizeObserver that watches for size changes in the
|
|
56
|
+
* virtual list container and triggers appropriate updates to height and scroll position.
|
|
57
|
+
*
|
|
58
|
+
* @param config - Configuration options
|
|
59
|
+
* @returns ResizeObserver instance
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const containerResizeObserver = createContainerResizeObserver({
|
|
64
|
+
* debug: true,
|
|
65
|
+
* onResize: (entry) => {
|
|
66
|
+
* const newHeight = entry.contentRect.height
|
|
67
|
+
* updateHeightAndScroll(true)
|
|
68
|
+
* }
|
|
69
|
+
* })
|
|
70
|
+
*
|
|
71
|
+
* if (containerElement) {
|
|
72
|
+
* containerResizeObserver.observe(containerElement)
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const createContainerResizeObserver: (config?: ContainerResizeConfig) => ResizeObserver;
|
|
77
|
+
/**
|
|
78
|
+
* Utility to safely observe elements with automatic cleanup.
|
|
79
|
+
*
|
|
80
|
+
* This function provides a safe way to observe elements with a ResizeObserver,
|
|
81
|
+
* handling cases where the observer might not be available or elements might be null.
|
|
82
|
+
*
|
|
83
|
+
* @param observer - ResizeObserver instance
|
|
84
|
+
* @param element - Element to observe
|
|
85
|
+
* @param debug - Whether to log debug information
|
|
86
|
+
* @returns Cleanup function to stop observing
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const cleanup = safeObserve(resizeObserver, element, true)
|
|
91
|
+
*
|
|
92
|
+
* // Later, stop observing
|
|
93
|
+
* cleanup()
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare const safeObserve: (observer: ResizeObserver | null, element: HTMLElement | null, debug?: boolean) => (() => void);
|
|
97
|
+
/**
|
|
98
|
+
* Manages multiple ResizeObserver instances with automatic cleanup.
|
|
99
|
+
*
|
|
100
|
+
* This class provides a convenient way to manage multiple ResizeObserver instances
|
|
101
|
+
* and ensures proper cleanup when the component is destroyed.
|
|
102
|
+
*/
|
|
103
|
+
export declare class ResizeObserverManager {
|
|
104
|
+
private observers;
|
|
105
|
+
private cleanupFunctions;
|
|
106
|
+
/**
|
|
107
|
+
* Adds a ResizeObserver to the manager
|
|
108
|
+
*/
|
|
109
|
+
addObserver(observer: ResizeObserver): void;
|
|
110
|
+
/**
|
|
111
|
+
* Adds a cleanup function to be called during cleanup
|
|
112
|
+
*/
|
|
113
|
+
addCleanup(cleanup: () => void): void;
|
|
114
|
+
/**
|
|
115
|
+
* Observes an element with automatic cleanup tracking
|
|
116
|
+
*/
|
|
117
|
+
observe(observer: ResizeObserver, element: HTMLElement, debug?: boolean): void;
|
|
118
|
+
/**
|
|
119
|
+
* Disconnects all observers and runs cleanup functions
|
|
120
|
+
*/
|
|
121
|
+
cleanup(): void;
|
|
122
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
/**
|
|
59
|
+
* Creates a ResizeObserver for monitoring container size changes.
|
|
60
|
+
*
|
|
61
|
+
* This function creates a ResizeObserver that watches for size changes in the
|
|
62
|
+
* virtual list container and triggers appropriate updates to height and scroll position.
|
|
63
|
+
*
|
|
64
|
+
* @param config - Configuration options
|
|
65
|
+
* @returns ResizeObserver instance
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const containerResizeObserver = createContainerResizeObserver({
|
|
70
|
+
* debug: true,
|
|
71
|
+
* onResize: (entry) => {
|
|
72
|
+
* const newHeight = entry.contentRect.height
|
|
73
|
+
* updateHeightAndScroll(true)
|
|
74
|
+
* }
|
|
75
|
+
* })
|
|
76
|
+
*
|
|
77
|
+
* if (containerElement) {
|
|
78
|
+
* containerResizeObserver.observe(containerElement)
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export const createContainerResizeObserver = (config = {}) => {
|
|
83
|
+
const { debug = false, onResize } = config;
|
|
84
|
+
return new ResizeObserver((entries) => {
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (debug) {
|
|
87
|
+
console.log('Container resized:', entry.contentRect);
|
|
88
|
+
}
|
|
89
|
+
onResize?.(entry);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Utility to safely observe elements with automatic cleanup.
|
|
95
|
+
*
|
|
96
|
+
* This function provides a safe way to observe elements with a ResizeObserver,
|
|
97
|
+
* handling cases where the observer might not be available or elements might be null.
|
|
98
|
+
*
|
|
99
|
+
* @param observer - ResizeObserver instance
|
|
100
|
+
* @param element - Element to observe
|
|
101
|
+
* @param debug - Whether to log debug information
|
|
102
|
+
* @returns Cleanup function to stop observing
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const cleanup = safeObserve(resizeObserver, element, true)
|
|
107
|
+
*
|
|
108
|
+
* // Later, stop observing
|
|
109
|
+
* cleanup()
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export const safeObserve = (observer, element, debug = false) => {
|
|
113
|
+
if (observer && element) {
|
|
114
|
+
observer.observe(element);
|
|
115
|
+
if (debug) {
|
|
116
|
+
console.log('Started observing element:', element);
|
|
117
|
+
}
|
|
118
|
+
return () => {
|
|
119
|
+
if (observer && element) {
|
|
120
|
+
observer.unobserve(element);
|
|
121
|
+
if (debug) {
|
|
122
|
+
console.log('Stopped observing element:', element);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (debug && !observer) {
|
|
128
|
+
console.log('ResizeObserver not available for element:', element);
|
|
129
|
+
}
|
|
130
|
+
return () => { }; // No-op cleanup function
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Manages multiple ResizeObserver instances with automatic cleanup.
|
|
134
|
+
*
|
|
135
|
+
* This class provides a convenient way to manage multiple ResizeObserver instances
|
|
136
|
+
* and ensures proper cleanup when the component is destroyed.
|
|
137
|
+
*/
|
|
138
|
+
export class ResizeObserverManager {
|
|
139
|
+
observers = [];
|
|
140
|
+
cleanupFunctions = [];
|
|
141
|
+
/**
|
|
142
|
+
* Adds a ResizeObserver to the manager
|
|
143
|
+
*/
|
|
144
|
+
addObserver(observer) {
|
|
145
|
+
this.observers.push(observer);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Adds a cleanup function to be called during cleanup
|
|
149
|
+
*/
|
|
150
|
+
addCleanup(cleanup) {
|
|
151
|
+
this.cleanupFunctions.push(cleanup);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Observes an element with automatic cleanup tracking
|
|
155
|
+
*/
|
|
156
|
+
observe(observer, element, debug = false) {
|
|
157
|
+
const cleanup = safeObserve(observer, element, debug);
|
|
158
|
+
this.addCleanup(cleanup);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Disconnects all observers and runs cleanup functions
|
|
162
|
+
*/
|
|
163
|
+
cleanup() {
|
|
164
|
+
// Disconnect all observers
|
|
165
|
+
for (const observer of this.observers) {
|
|
166
|
+
observer.disconnect();
|
|
167
|
+
}
|
|
168
|
+
// Run all cleanup functions
|
|
169
|
+
for (const cleanup of this.cleanupFunctions) {
|
|
170
|
+
cleanup();
|
|
171
|
+
}
|
|
172
|
+
// Clear arrays
|
|
173
|
+
this.observers.length = 0;
|
|
174
|
+
this.cleanupFunctions.length = 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SvelteVirtualListMode, SvelteVirtualListScrollAlignment } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for calculating scroll target position
|
|
4
|
+
*/
|
|
5
|
+
export interface ScrollTargetParams {
|
|
6
|
+
mode: SvelteVirtualListMode;
|
|
7
|
+
align: SvelteVirtualListScrollAlignment;
|
|
8
|
+
targetIndex: number;
|
|
9
|
+
itemsLength: number;
|
|
10
|
+
calculatedItemHeight: number;
|
|
11
|
+
height: number;
|
|
12
|
+
scrollTop: number;
|
|
13
|
+
firstVisibleIndex: number;
|
|
14
|
+
lastVisibleIndex: number;
|
|
15
|
+
heightCache: Record<number, number>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Calculates the target scroll position for scrolling to a specific item index.
|
|
19
|
+
*
|
|
20
|
+
* This function handles both topToBottom and bottomToTop scroll modes with different
|
|
21
|
+
* alignment options (auto, top, bottom, nearest). It takes into account the current
|
|
22
|
+
* viewport state and calculates the optimal scroll position.
|
|
23
|
+
*
|
|
24
|
+
* @param params - Parameters for scroll target calculation
|
|
25
|
+
* @returns The target scroll position in pixels, or null if no scroll is needed
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const scrollTarget = calculateScrollTarget({
|
|
30
|
+
* mode: 'topToBottom',
|
|
31
|
+
* align: 'auto',
|
|
32
|
+
* targetIndex: 100,
|
|
33
|
+
* itemsLength: 1000,
|
|
34
|
+
* calculatedItemHeight: 50,
|
|
35
|
+
* height: 400,
|
|
36
|
+
* scrollTop: 200,
|
|
37
|
+
* firstVisibleIndex: 4,
|
|
38
|
+
* lastVisibleIndex: 12,
|
|
39
|
+
* heightCache: {}
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* if (scrollTarget !== null) {
|
|
43
|
+
* viewportElement.scrollTo({ top: scrollTarget })
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const calculateScrollTarget: (params: ScrollTargetParams) => number | null;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { getScrollOffsetForIndex } from './virtualList.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calculates the target scroll position for scrolling to a specific item index.
|
|
4
|
+
*
|
|
5
|
+
* This function handles both topToBottom and bottomToTop scroll modes with different
|
|
6
|
+
* alignment options (auto, top, bottom, nearest). It takes into account the current
|
|
7
|
+
* viewport state and calculates the optimal scroll position.
|
|
8
|
+
*
|
|
9
|
+
* @param params - Parameters for scroll target calculation
|
|
10
|
+
* @returns The target scroll position in pixels, or null if no scroll is needed
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const scrollTarget = calculateScrollTarget({
|
|
15
|
+
* mode: 'topToBottom',
|
|
16
|
+
* align: 'auto',
|
|
17
|
+
* targetIndex: 100,
|
|
18
|
+
* itemsLength: 1000,
|
|
19
|
+
* calculatedItemHeight: 50,
|
|
20
|
+
* height: 400,
|
|
21
|
+
* scrollTop: 200,
|
|
22
|
+
* firstVisibleIndex: 4,
|
|
23
|
+
* lastVisibleIndex: 12,
|
|
24
|
+
* heightCache: {}
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* if (scrollTarget !== null) {
|
|
28
|
+
* viewportElement.scrollTo({ top: scrollTarget })
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const calculateScrollTarget = (params) => {
|
|
33
|
+
const { mode, align, targetIndex, itemsLength, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
|
|
34
|
+
if (mode === 'bottomToTop') {
|
|
35
|
+
return calculateBottomToTopScrollTarget({
|
|
36
|
+
align,
|
|
37
|
+
targetIndex,
|
|
38
|
+
itemsLength,
|
|
39
|
+
calculatedItemHeight,
|
|
40
|
+
height,
|
|
41
|
+
scrollTop,
|
|
42
|
+
firstVisibleIndex,
|
|
43
|
+
lastVisibleIndex
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return calculateTopToBottomScrollTarget({
|
|
48
|
+
align,
|
|
49
|
+
targetIndex,
|
|
50
|
+
calculatedItemHeight,
|
|
51
|
+
height,
|
|
52
|
+
scrollTop,
|
|
53
|
+
firstVisibleIndex,
|
|
54
|
+
lastVisibleIndex,
|
|
55
|
+
heightCache
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Calculates scroll target for bottom-to-top mode
|
|
61
|
+
*/
|
|
62
|
+
const calculateBottomToTopScrollTarget = (params) => {
|
|
63
|
+
const { align, targetIndex, itemsLength, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex } = params;
|
|
64
|
+
const totalHeight = itemsLength * calculatedItemHeight;
|
|
65
|
+
const itemOffset = targetIndex * calculatedItemHeight;
|
|
66
|
+
const itemHeight = calculatedItemHeight;
|
|
67
|
+
if (align === 'auto') {
|
|
68
|
+
// If item is above the viewport, align to top
|
|
69
|
+
if (targetIndex < firstVisibleIndex) {
|
|
70
|
+
return Math.max(0, totalHeight - (itemOffset + itemHeight));
|
|
71
|
+
}
|
|
72
|
+
// If item is below the viewport, align to bottom
|
|
73
|
+
else if (targetIndex > lastVisibleIndex - 1) {
|
|
74
|
+
return Math.max(0, totalHeight - itemOffset - height);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Item is visible but not aligned: align to nearest edge
|
|
78
|
+
const itemTop = totalHeight - (itemOffset + itemHeight);
|
|
79
|
+
const itemBottom = totalHeight - itemOffset;
|
|
80
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
81
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
82
|
+
if (distanceToTop < distanceToBottom) {
|
|
83
|
+
return itemTop;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return Math.max(0, itemBottom - height);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (align === 'top') {
|
|
91
|
+
return Math.max(0, totalHeight - (itemOffset + itemHeight));
|
|
92
|
+
}
|
|
93
|
+
else if (align === 'bottom') {
|
|
94
|
+
return Math.max(0, totalHeight - itemOffset - height);
|
|
95
|
+
}
|
|
96
|
+
else if (align === 'nearest') {
|
|
97
|
+
const itemTop = totalHeight - (itemOffset + itemHeight);
|
|
98
|
+
const itemBottom = totalHeight - itemOffset;
|
|
99
|
+
if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
|
|
100
|
+
// Not visible, align to nearest edge
|
|
101
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
102
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
103
|
+
if (distanceToTop < distanceToBottom) {
|
|
104
|
+
return itemTop;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
return Math.max(0, itemBottom - height);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Already visible, do nothing
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Calculates scroll target for top-to-bottom mode
|
|
119
|
+
*/
|
|
120
|
+
const calculateTopToBottomScrollTarget = (params) => {
|
|
121
|
+
const { align, targetIndex, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
|
|
122
|
+
if (align === 'auto') {
|
|
123
|
+
// If item is above the viewport, align to top
|
|
124
|
+
if (targetIndex < firstVisibleIndex) {
|
|
125
|
+
return getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
126
|
+
}
|
|
127
|
+
// If item is below the viewport, align to bottom
|
|
128
|
+
else if (targetIndex > lastVisibleIndex - 1) {
|
|
129
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
130
|
+
return Math.max(0, itemBottom - height);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Item is visible but not aligned: align to nearest edge
|
|
134
|
+
const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
135
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
136
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
137
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
138
|
+
if (distanceToTop < distanceToBottom) {
|
|
139
|
+
return itemTop;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
return Math.max(0, itemBottom - height);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (align === 'top') {
|
|
147
|
+
return getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
148
|
+
}
|
|
149
|
+
else if (align === 'bottom') {
|
|
150
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
151
|
+
return Math.max(0, itemBottom - height);
|
|
152
|
+
}
|
|
153
|
+
else if (align === 'nearest') {
|
|
154
|
+
const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
155
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
156
|
+
if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
|
|
157
|
+
// Not visible, align to nearest edge
|
|
158
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
159
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
160
|
+
if (distanceToTop < distanceToBottom) {
|
|
161
|
+
return itemTop;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
return Math.max(0, itemBottom - height);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Already visible, do nothing
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
@@ -86,10 +86,11 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
|
|
|
86
86
|
*/
|
|
87
87
|
export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
|
|
88
88
|
start: number;
|
|
89
|
-
}, heightCache: Record<number, number>, currentItemHeight: number) => {
|
|
89
|
+
}, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>) => {
|
|
90
90
|
newHeight: number;
|
|
91
91
|
newLastMeasuredIndex: number;
|
|
92
92
|
updatedHeightCache: Record<number, number>;
|
|
93
|
+
clearedDirtyItems: Set<number>;
|
|
93
94
|
};
|
|
94
95
|
/**
|
|
95
96
|
* Processes large arrays in chunks to prevent UI blocking.
|