@humanspeak/svelte-virtual-list 0.2.6-beta.6 → 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.
@@ -40,7 +40,8 @@ export const calculateScrollTarget = (params) => {
40
40
  height,
41
41
  scrollTop,
42
42
  firstVisibleIndex,
43
- lastVisibleIndex
43
+ lastVisibleIndex,
44
+ heightCache
44
45
  });
45
46
  }
46
47
  else {
@@ -60,31 +61,26 @@ export const calculateScrollTarget = (params) => {
60
61
  * Calculates scroll target for bottom-to-top mode
61
62
  */
62
63
  const calculateBottomToTopScrollTarget = (params) => {
63
- const { align, targetIndex, itemsLength, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex } = params;
64
- const totalHeight = itemsLength * calculatedItemHeight;
65
- const itemOffset = targetIndex * calculatedItemHeight;
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);
66
68
  const itemHeight = calculatedItemHeight;
67
69
  if (align === 'auto') {
68
70
  // If item is above the viewport, align to top
69
71
  if (targetIndex < firstVisibleIndex) {
70
72
  return Math.max(0, totalHeight - (itemOffset + itemHeight));
71
73
  }
72
- // If item is below the viewport, align to bottom
73
74
  else if (targetIndex > lastVisibleIndex - 1) {
75
+ // In bottomToTop, "below" means higher indices that need HIGHER scrollTop
74
76
  return Math.max(0, totalHeight - itemOffset - height);
75
77
  }
76
78
  else {
77
- // Item is visible but not aligned: align to nearest edge
78
79
  const itemTop = totalHeight - (itemOffset + itemHeight);
79
80
  const itemBottom = totalHeight - itemOffset;
80
81
  const distanceToTop = Math.abs(scrollTop - itemTop);
81
82
  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
- }
83
+ return distanceToTop < distanceToBottom ? itemTop : Math.max(0, itemBottom - height);
88
84
  }
89
85
  }
90
86
  else if (align === 'top') {
@@ -100,12 +96,7 @@ const calculateBottomToTopScrollTarget = (params) => {
100
96
  // Not visible, align to nearest edge
101
97
  const distanceToTop = Math.abs(scrollTop - itemTop);
102
98
  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
- }
99
+ return distanceToTop < distanceToBottom ? itemTop : Math.max(0, itemBottom - height);
109
100
  }
110
101
  else {
111
102
  // Already visible, do nothing
@@ -119,15 +110,29 @@ const calculateBottomToTopScrollTarget = (params) => {
119
110
  */
120
111
  const calculateTopToBottomScrollTarget = (params) => {
121
112
  const { align, targetIndex, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
113
+ console.log('[DEBUG] calculateTopToBottomScrollTarget:', {
114
+ align,
115
+ targetIndex,
116
+ calculatedItemHeight,
117
+ height,
118
+ scrollTop,
119
+ firstVisibleIndex,
120
+ lastVisibleIndex,
121
+ heightCacheKeys: Object.keys(heightCache).length
122
+ });
122
123
  if (align === 'auto') {
123
124
  // If item is above the viewport, align to top
124
125
  if (targetIndex < firstVisibleIndex) {
125
- return getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
126
+ const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
127
+ console.log(`[DEBUG] Item ${targetIndex} above viewport (${firstVisibleIndex}), scrolling to top:`, scrollTarget);
128
+ return scrollTarget;
126
129
  }
127
130
  // If item is below the viewport, align to bottom
128
131
  else if (targetIndex > lastVisibleIndex - 1) {
129
132
  const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
130
- return Math.max(0, itemBottom - height);
133
+ const scrollTarget = Math.max(0, itemBottom - height);
134
+ console.log(`[DEBUG] Item ${targetIndex} below viewport (${lastVisibleIndex}), scrolling to bottom:`, scrollTarget);
135
+ return scrollTarget;
131
136
  }
132
137
  else {
133
138
  // Item is visible but not aligned: align to nearest edge
@@ -135,6 +140,12 @@ const calculateTopToBottomScrollTarget = (params) => {
135
140
  const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
136
141
  const distanceToTop = Math.abs(scrollTop - itemTop);
137
142
  const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
143
+ console.log(`[DEBUG] Item ${targetIndex} visible, choosing nearest edge:`, {
144
+ itemTop,
145
+ itemBottom,
146
+ distanceToTop,
147
+ distanceToBottom
148
+ });
138
149
  if (distanceToTop < distanceToBottom) {
139
150
  return itemTop;
140
151
  }
@@ -144,7 +155,9 @@ const calculateTopToBottomScrollTarget = (params) => {
144
155
  }
145
156
  }
146
157
  else if (align === 'top') {
147
- return getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
158
+ const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
159
+ console.log(`[DEBUG] Align to top for index ${targetIndex}:`, scrollTarget);
160
+ return scrollTarget;
148
161
  }
149
162
  else if (align === 'bottom') {
150
163
  const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
@@ -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
+ };
@@ -28,7 +28,7 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
28
28
  * @param {SvelteVirtualListMode} mode - Scroll direction mode
29
29
  * @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
30
30
  */
31
- export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => SvelteVirtualListPreviousVisibleRange;
31
+ export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode, atBottom: boolean, wasAtBottomBeforeHeightChange: boolean, lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null, totalContentHeight?: number) => SvelteVirtualListPreviousVisibleRange;
32
32
  /**
33
33
  * Calculates the CSS transform value for positioning the virtual list items.
34
34
  *
@@ -41,9 +41,10 @@ export declare const calculateVisibleRange: (scrollTop: number, viewportHeight:
41
41
  * @param {number} visibleEnd - Index of the last visible item
42
42
  * @param {number} visibleStart - Index of the first visible item
43
43
  * @param {number} itemHeight - Height of each list item in pixels
44
+ * @param {number} viewportHeight - Height of the viewport in pixels
44
45
  * @returns {number} The calculated transform Y value in pixels
45
46
  */
46
- export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number) => number;
47
+ export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number, viewportHeight: number, totalContentHeight?: number) => number;
47
48
  /**
48
49
  * Updates the virtual list's height and scroll position when necessary.
49
50
  *
@@ -86,13 +87,20 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
86
87
  */
87
88
  export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
88
89
  start: number;
89
- }, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>, currentTotalHeight?: number, currentValidCount?: number) => {
90
+ end: number;
91
+ }, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>, currentTotalHeight?: number, currentValidCount?: number, mode?: SvelteVirtualListMode) => {
90
92
  newHeight: number;
91
93
  newLastMeasuredIndex: number;
92
94
  updatedHeightCache: Record<number, number>;
93
95
  clearedDirtyItems: Set<number>;
94
96
  newTotalHeight: number;
95
97
  newValidCount: number;
98
+ heightChanges: Array<{
99
+ index: number;
100
+ oldHeight: number;
101
+ newHeight: number;
102
+ delta: number;
103
+ }>;
96
104
  };
97
105
  /**
98
106
  * Processes large arrays in chunks to prevent UI blocking.
@@ -121,17 +129,6 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
121
129
  export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
122
130
  chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
123
131
  onComplete: () => void) => Promise<void>;
124
- /**
125
- * Builds a block sum array for fast offset calculation in large virtual lists.
126
- * Each entry in the array is the total height up to the end of that block (exclusive).
127
- *
128
- * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
129
- * @param {number} calculatedItemHeight - Estimated height for unmeasured items
130
- * @param {number} totalItems - Total number of items in the list
131
- * @param {number} blockSize - Number of items per block
132
- * @returns {number[]} Array of prefix sums at each block boundary
133
- */
134
- export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
135
132
  /**
136
133
  * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
137
134
  *