@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,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a ResizeObserver for monitoring container size changes.
|
|
3
|
+
*
|
|
4
|
+
* This function creates a ResizeObserver that watches for size changes in the
|
|
5
|
+
* virtual list container and triggers appropriate updates to height and scroll position.
|
|
6
|
+
*
|
|
7
|
+
* @param config - Configuration options
|
|
8
|
+
* @returns ResizeObserver instance
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const containerResizeObserver = createContainerResizeObserver({
|
|
13
|
+
* debug: true,
|
|
14
|
+
* onResize: (entry) => {
|
|
15
|
+
* const newHeight = entry.contentRect.height
|
|
16
|
+
* updateHeightAndScroll(true)
|
|
17
|
+
* }
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* if (containerElement) {
|
|
21
|
+
* containerResizeObserver.observe(containerElement)
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const createContainerResizeObserver = (config = {}) => {
|
|
26
|
+
const { debug = false, onResize } = config;
|
|
27
|
+
return new ResizeObserver((entries) => {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (debug) {
|
|
30
|
+
console.log('Container resized:', entry.contentRect);
|
|
31
|
+
}
|
|
32
|
+
onResize?.(entry);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Utility to safely observe elements with automatic cleanup.
|
|
38
|
+
*
|
|
39
|
+
* This function provides a safe way to observe elements with a ResizeObserver,
|
|
40
|
+
* handling cases where the observer might not be available or elements might be null.
|
|
41
|
+
*
|
|
42
|
+
* @param observer - ResizeObserver instance
|
|
43
|
+
* @param element - Element to observe
|
|
44
|
+
* @param debug - Whether to log debug information
|
|
45
|
+
* @returns Cleanup function to stop observing
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const cleanup = safeObserve(resizeObserver, element, true)
|
|
50
|
+
*
|
|
51
|
+
* // Later, stop observing
|
|
52
|
+
* cleanup()
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export const safeObserve = (observer, element, debug = false) => {
|
|
56
|
+
if (observer && element) {
|
|
57
|
+
observer.observe(element);
|
|
58
|
+
if (debug) {
|
|
59
|
+
console.log('Started observing element:', element);
|
|
60
|
+
}
|
|
61
|
+
return () => {
|
|
62
|
+
if (observer && element) {
|
|
63
|
+
observer.unobserve(element);
|
|
64
|
+
if (debug) {
|
|
65
|
+
console.log('Stopped observing element:', element);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (debug && !observer) {
|
|
71
|
+
console.log('ResizeObserver not available for element:', element);
|
|
72
|
+
}
|
|
73
|
+
return () => { }; // No-op cleanup function
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Manages multiple ResizeObserver instances with automatic cleanup.
|
|
77
|
+
*
|
|
78
|
+
* This class provides a convenient way to manage multiple ResizeObserver instances
|
|
79
|
+
* and ensures proper cleanup when the component is destroyed.
|
|
80
|
+
*/
|
|
81
|
+
export class ResizeObserverManager {
|
|
82
|
+
observers = [];
|
|
83
|
+
cleanupFunctions = [];
|
|
84
|
+
/**
|
|
85
|
+
* Adds a ResizeObserver to the manager
|
|
86
|
+
*/
|
|
87
|
+
addObserver(observer) {
|
|
88
|
+
this.observers.push(observer);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Adds a cleanup function to be called during cleanup
|
|
92
|
+
*/
|
|
93
|
+
addCleanup(cleanup) {
|
|
94
|
+
this.cleanupFunctions.push(cleanup);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Observes an element with automatic cleanup tracking
|
|
98
|
+
*/
|
|
99
|
+
observe(observer, element, debug = false) {
|
|
100
|
+
const cleanup = safeObserve(observer, element, debug);
|
|
101
|
+
this.addCleanup(cleanup);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Disconnects all observers and runs cleanup functions
|
|
105
|
+
*/
|
|
106
|
+
cleanup() {
|
|
107
|
+
// Disconnect all observers
|
|
108
|
+
for (const observer of this.observers) {
|
|
109
|
+
observer.disconnect();
|
|
110
|
+
}
|
|
111
|
+
// Run all cleanup functions
|
|
112
|
+
for (const cleanup of this.cleanupFunctions) {
|
|
113
|
+
cleanup();
|
|
114
|
+
}
|
|
115
|
+
// Clear arrays
|
|
116
|
+
this.observers.length = 0;
|
|
117
|
+
this.cleanupFunctions.length = 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SvelteVirtualListMode, SvelteVirtualListScrollAlign } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for calculating scroll target position
|
|
4
|
+
*/
|
|
5
|
+
export interface ScrollTargetParams {
|
|
6
|
+
mode: SvelteVirtualListMode;
|
|
7
|
+
align: SvelteVirtualListScrollAlign;
|
|
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,167 @@
|
|
|
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
|
+
heightCache
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
return calculateTopToBottomScrollTarget({
|
|
49
|
+
align,
|
|
50
|
+
targetIndex,
|
|
51
|
+
calculatedItemHeight,
|
|
52
|
+
height,
|
|
53
|
+
scrollTop,
|
|
54
|
+
firstVisibleIndex,
|
|
55
|
+
lastVisibleIndex,
|
|
56
|
+
heightCache
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Calculates scroll target for bottom-to-top mode
|
|
62
|
+
*/
|
|
63
|
+
const calculateBottomToTopScrollTarget = (params) => {
|
|
64
|
+
const { align, targetIndex, itemsLength, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
|
|
65
|
+
// Use getScrollOffsetForIndex for accurate positioning with height cache
|
|
66
|
+
const totalHeight = getScrollOffsetForIndex(heightCache, calculatedItemHeight, itemsLength);
|
|
67
|
+
const itemOffset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
68
|
+
const itemHeight = calculatedItemHeight;
|
|
69
|
+
if (align === 'auto') {
|
|
70
|
+
// If item is above the viewport, align to top
|
|
71
|
+
if (targetIndex < firstVisibleIndex) {
|
|
72
|
+
return Math.max(0, totalHeight - (itemOffset + itemHeight));
|
|
73
|
+
}
|
|
74
|
+
else if (targetIndex > lastVisibleIndex - 1) {
|
|
75
|
+
// In bottomToTop, "below" means higher indices that need HIGHER scrollTop
|
|
76
|
+
return Math.max(0, totalHeight - itemOffset - height);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const itemTop = totalHeight - (itemOffset + itemHeight);
|
|
80
|
+
const itemBottom = totalHeight - itemOffset;
|
|
81
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
82
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
83
|
+
return distanceToTop < distanceToBottom ? itemTop : Math.max(0, itemBottom - height);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (align === 'top') {
|
|
87
|
+
return Math.max(0, totalHeight - (itemOffset + itemHeight));
|
|
88
|
+
}
|
|
89
|
+
else if (align === 'bottom') {
|
|
90
|
+
return Math.max(0, totalHeight - itemOffset - height);
|
|
91
|
+
}
|
|
92
|
+
else if (align === 'nearest') {
|
|
93
|
+
const itemTop = totalHeight - (itemOffset + itemHeight);
|
|
94
|
+
const itemBottom = totalHeight - itemOffset;
|
|
95
|
+
if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
|
|
96
|
+
// Not visible, align to nearest edge
|
|
97
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
98
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
99
|
+
return distanceToTop < distanceToBottom ? itemTop : Math.max(0, itemBottom - height);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Already visible, do nothing
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Calculates scroll target for top-to-bottom mode
|
|
110
|
+
*/
|
|
111
|
+
const calculateTopToBottomScrollTarget = (params) => {
|
|
112
|
+
const { align, targetIndex, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
|
|
113
|
+
if (align === 'auto') {
|
|
114
|
+
// If item is above the viewport, align to top
|
|
115
|
+
if (targetIndex < firstVisibleIndex) {
|
|
116
|
+
const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
117
|
+
return scrollTarget;
|
|
118
|
+
}
|
|
119
|
+
// If item is below the viewport, align to bottom
|
|
120
|
+
else if (targetIndex > lastVisibleIndex - 1) {
|
|
121
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
122
|
+
const scrollTarget = Math.max(0, itemBottom - height);
|
|
123
|
+
return scrollTarget;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Item is visible but not aligned: align to nearest edge
|
|
127
|
+
const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
128
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
129
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
130
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
131
|
+
if (distanceToTop < distanceToBottom) {
|
|
132
|
+
return itemTop;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
return Math.max(0, itemBottom - height);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (align === 'top') {
|
|
140
|
+
const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
141
|
+
return scrollTarget;
|
|
142
|
+
}
|
|
143
|
+
else if (align === 'bottom') {
|
|
144
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
145
|
+
return Math.max(0, itemBottom - height);
|
|
146
|
+
}
|
|
147
|
+
else if (align === 'nearest') {
|
|
148
|
+
const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
|
|
149
|
+
const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
|
|
150
|
+
if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
|
|
151
|
+
// Not visible, align to nearest edge
|
|
152
|
+
const distanceToTop = Math.abs(scrollTop - itemTop);
|
|
153
|
+
const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
|
|
154
|
+
if (distanceToTop < distanceToBottom) {
|
|
155
|
+
return itemTop;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return Math.max(0, itemBottom - height);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Already visible, do nothing
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttling utilities for performance optimization.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Provides throttling functions to limit execution frequency of callbacks,
|
|
5
|
+
* particularly useful for preventing excessive reactive effect triggers and debounced function calls.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Time provider abstraction that can be mocked in tests.
|
|
9
|
+
* Uses performance.now() in production for high precision timing,
|
|
10
|
+
* but can fallback to Date.now() for testing environments.
|
|
11
|
+
*/
|
|
12
|
+
export declare const timeProvider: {
|
|
13
|
+
now: () => number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a throttled version of a callback function that limits execution frequency.
|
|
17
|
+
*
|
|
18
|
+
* The throttled function will execute immediately on first call, then ignore subsequent
|
|
19
|
+
* calls until the specified delay has elapsed. This is different from debouncing, which
|
|
20
|
+
* delays execution until after calls stop coming.
|
|
21
|
+
*
|
|
22
|
+
* @template T - The type of the callback function
|
|
23
|
+
* @param callback - The function to throttle
|
|
24
|
+
* @param delay - Minimum time between executions in milliseconds (default: 16ms ≈ 60fps)
|
|
25
|
+
* @returns A throttled version of the callback function
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // Basic usage
|
|
30
|
+
* const throttledLog = createThrottledCallback(
|
|
31
|
+
* (message: string) => console.log(message),
|
|
32
|
+
* 100
|
|
33
|
+
* );
|
|
34
|
+
*
|
|
35
|
+
* // Called immediately
|
|
36
|
+
* throttledLog("First call");
|
|
37
|
+
*
|
|
38
|
+
* // Ignored (within 100ms)
|
|
39
|
+
* throttledLog("Second call");
|
|
40
|
+
*
|
|
41
|
+
* // After 100ms, this would execute
|
|
42
|
+
* setTimeout(() => throttledLog("Third call"), 150);
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Throttling reactive effects in Svelte
|
|
48
|
+
* const throttledUpdate = createThrottledCallback(() => {
|
|
49
|
+
* if (BROWSER && dirtyItemsCount > 0) {
|
|
50
|
+
* updateHeight();
|
|
51
|
+
* }
|
|
52
|
+
* }, 16); // ~60fps
|
|
53
|
+
*
|
|
54
|
+
* $effect(() => {
|
|
55
|
+
* throttledUpdate();
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare const createThrottledCallback: <T extends (..._args: unknown[]) => void>(callback: T, delay?: number) => T;
|
|
60
|
+
/**
|
|
61
|
+
* Creates a throttled callback with leading and trailing execution options.
|
|
62
|
+
*
|
|
63
|
+
* Unlike the basic throttle, this version allows control over whether the function
|
|
64
|
+
* executes on the leading edge (immediately) and/or trailing edge (after delay).
|
|
65
|
+
*
|
|
66
|
+
* @template T - The type of the callback function
|
|
67
|
+
* @param callback - The function to throttle
|
|
68
|
+
* @param delay - Minimum time between executions in milliseconds
|
|
69
|
+
* @param options - Configuration options
|
|
70
|
+
* @param options.leading - Execute on the leading edge (default: true)
|
|
71
|
+
* @param options.trailing - Execute on the trailing edge (default: false)
|
|
72
|
+
* @returns A throttled version of the callback function
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Execute immediately and after delay
|
|
77
|
+
* const throttledWithTrailing = createAdvancedThrottledCallback(
|
|
78
|
+
* () => console.log("Throttled call"),
|
|
79
|
+
* 100,
|
|
80
|
+
* { leading: true, trailing: true }
|
|
81
|
+
* );
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare const createAdvancedThrottledCallback: <T extends (..._args: unknown[]) => void>(callback: T, delay: number, options?: {
|
|
85
|
+
leading?: boolean;
|
|
86
|
+
trailing?: boolean;
|
|
87
|
+
}) => T;
|
|
88
|
+
/**
|
|
89
|
+
* Type definitions for throttle utilities
|
|
90
|
+
*/
|
|
91
|
+
export type ThrottledCallback<T extends (..._args: unknown[]) => void> = T;
|
|
92
|
+
export type ThrottleOptions = {
|
|
93
|
+
leading?: boolean;
|
|
94
|
+
trailing?: boolean;
|
|
95
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttling utilities for performance optimization.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Provides throttling functions to limit execution frequency of callbacks,
|
|
5
|
+
* particularly useful for preventing excessive reactive effect triggers and debounced function calls.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Time provider abstraction that can be mocked in tests.
|
|
9
|
+
* Uses performance.now() in production for high precision timing,
|
|
10
|
+
* but can fallback to Date.now() for testing environments.
|
|
11
|
+
*/
|
|
12
|
+
export const timeProvider = {
|
|
13
|
+
now: () => {
|
|
14
|
+
// Use performance.now() for high precision in production
|
|
15
|
+
if (typeof performance !== 'undefined' && performance.now) {
|
|
16
|
+
return performance.now();
|
|
17
|
+
}
|
|
18
|
+
// Fallback to Date.now() (mainly for testing or older environments)
|
|
19
|
+
return Date.now();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Creates a throttled version of a callback function that limits execution frequency.
|
|
24
|
+
*
|
|
25
|
+
* The throttled function will execute immediately on first call, then ignore subsequent
|
|
26
|
+
* calls until the specified delay has elapsed. This is different from debouncing, which
|
|
27
|
+
* delays execution until after calls stop coming.
|
|
28
|
+
*
|
|
29
|
+
* @template T - The type of the callback function
|
|
30
|
+
* @param callback - The function to throttle
|
|
31
|
+
* @param delay - Minimum time between executions in milliseconds (default: 16ms ≈ 60fps)
|
|
32
|
+
* @returns A throttled version of the callback function
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Basic usage
|
|
37
|
+
* const throttledLog = createThrottledCallback(
|
|
38
|
+
* (message: string) => console.log(message),
|
|
39
|
+
* 100
|
|
40
|
+
* );
|
|
41
|
+
*
|
|
42
|
+
* // Called immediately
|
|
43
|
+
* throttledLog("First call");
|
|
44
|
+
*
|
|
45
|
+
* // Ignored (within 100ms)
|
|
46
|
+
* throttledLog("Second call");
|
|
47
|
+
*
|
|
48
|
+
* // After 100ms, this would execute
|
|
49
|
+
* setTimeout(() => throttledLog("Third call"), 150);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* // Throttling reactive effects in Svelte
|
|
55
|
+
* const throttledUpdate = createThrottledCallback(() => {
|
|
56
|
+
* if (BROWSER && dirtyItemsCount > 0) {
|
|
57
|
+
* updateHeight();
|
|
58
|
+
* }
|
|
59
|
+
* }, 16); // ~60fps
|
|
60
|
+
*
|
|
61
|
+
* $effect(() => {
|
|
62
|
+
* throttledUpdate();
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export const createThrottledCallback = (callback, delay = 16 // ~60fps default for smooth UI updates
|
|
67
|
+
) => {
|
|
68
|
+
let lastExecutionTime = 0;
|
|
69
|
+
let isFirstCall = true;
|
|
70
|
+
return ((...args) => {
|
|
71
|
+
const now = timeProvider.now();
|
|
72
|
+
if (isFirstCall || now - lastExecutionTime >= delay) {
|
|
73
|
+
isFirstCall = false;
|
|
74
|
+
lastExecutionTime = now;
|
|
75
|
+
callback(...args);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Creates a throttled callback with leading and trailing execution options.
|
|
81
|
+
*
|
|
82
|
+
* Unlike the basic throttle, this version allows control over whether the function
|
|
83
|
+
* executes on the leading edge (immediately) and/or trailing edge (after delay).
|
|
84
|
+
*
|
|
85
|
+
* @template T - The type of the callback function
|
|
86
|
+
* @param callback - The function to throttle
|
|
87
|
+
* @param delay - Minimum time between executions in milliseconds
|
|
88
|
+
* @param options - Configuration options
|
|
89
|
+
* @param options.leading - Execute on the leading edge (default: true)
|
|
90
|
+
* @param options.trailing - Execute on the trailing edge (default: false)
|
|
91
|
+
* @returns A throttled version of the callback function
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* // Execute immediately and after delay
|
|
96
|
+
* const throttledWithTrailing = createAdvancedThrottledCallback(
|
|
97
|
+
* () => console.log("Throttled call"),
|
|
98
|
+
* 100,
|
|
99
|
+
* { leading: true, trailing: true }
|
|
100
|
+
* );
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export const createAdvancedThrottledCallback = (callback, delay, options = {}) => {
|
|
104
|
+
const { leading = true, trailing = false } = options;
|
|
105
|
+
let lastExecutionTime = 0;
|
|
106
|
+
let trailingTimeoutId = null;
|
|
107
|
+
let lastArgs = null;
|
|
108
|
+
let isFirstCall = true;
|
|
109
|
+
const execute = (args) => {
|
|
110
|
+
lastExecutionTime = timeProvider.now();
|
|
111
|
+
callback(...args);
|
|
112
|
+
};
|
|
113
|
+
return ((...args) => {
|
|
114
|
+
const now = timeProvider.now();
|
|
115
|
+
const timeSinceLastExecution = isFirstCall ? delay : now - lastExecutionTime;
|
|
116
|
+
lastArgs = args;
|
|
117
|
+
// Clear any pending trailing execution
|
|
118
|
+
if (trailingTimeoutId) {
|
|
119
|
+
clearTimeout(trailingTimeoutId);
|
|
120
|
+
trailingTimeoutId = null;
|
|
121
|
+
}
|
|
122
|
+
if (timeSinceLastExecution >= delay) {
|
|
123
|
+
// Can execute immediately
|
|
124
|
+
if (leading) {
|
|
125
|
+
isFirstCall = false;
|
|
126
|
+
execute(args);
|
|
127
|
+
}
|
|
128
|
+
// Schedule trailing if needed
|
|
129
|
+
if (trailing && !leading) {
|
|
130
|
+
trailingTimeoutId = setTimeout(() => {
|
|
131
|
+
if (lastArgs) {
|
|
132
|
+
execute(lastArgs);
|
|
133
|
+
}
|
|
134
|
+
trailingTimeoutId = null;
|
|
135
|
+
}, delay);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Still within throttle window, but handle first call
|
|
140
|
+
if (isFirstCall && leading) {
|
|
141
|
+
isFirstCall = false;
|
|
142
|
+
execute(args);
|
|
143
|
+
}
|
|
144
|
+
else if (trailing) {
|
|
145
|
+
const remainingTime = delay - timeSinceLastExecution;
|
|
146
|
+
trailingTimeoutId = setTimeout(() => {
|
|
147
|
+
if (lastArgs) {
|
|
148
|
+
execute(lastArgs);
|
|
149
|
+
}
|
|
150
|
+
trailingTimeoutId = null;
|
|
151
|
+
}, remainingTime);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
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>;
|