@humanspeak/svelte-virtual-list 0.2.6-beta.1 → 0.2.6-beta.3

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.
@@ -41,15 +41,16 @@
41
41
  - Only visible items + buffer are mounted in the DOM
42
42
  - Height caching and estimation for dynamic content
43
43
  - Handles resize events and dynamic content changes
44
- - Supports chunked initialization for very large lists
45
- - All scrolling logic is centralized in the scroll() method
44
+ - Optimized for very large lists through virtualization
45
+ - Modular architecture with extracted utility functions
46
46
  - Bi-directional support: mode="topToBottom" or "bottomToTop"
47
47
  - Designed for extensibility and easy debugging
48
48
 
49
49
  =============================
50
50
  == For Contributors ==
51
51
  =============================
52
- - Please keep all scrolling logic in the scroll() method
52
+ - Complex logic is extracted to dedicated utility files in src/lib/utils/
53
+ - Scroll positioning logic is in scrollCalculation.ts (well-tested)
53
54
  - Add new features behind feature flags or as optional props
54
55
  - Write tests for all new features (see /test and /tests/scroll)
55
56
  - Use TypeScript and Svelte 5 runes for all new code
@@ -110,7 +111,14 @@
110
111
  * - Added comprehensive documentation
111
112
  * - Optimized debug output to reduce noise
112
113
  *
113
- * 9. Future Improvements (Planned)
114
+ * 9. Architecture Refactoring
115
+ * - Extracted scroll calculation logic to scrollCalculation.ts utility
116
+ * - Extracted ResizeObserver utilities to resizeObserver.ts
117
+ * - Added comprehensive test coverage for extracted utilities
118
+ * - Improved separation of concerns and maintainability
119
+ * - Simplified initialization (removed unnecessary chunked processing)
120
+ *
121
+ * 10. Future Improvements (Planned)
114
122
  * - Add horizontal scrolling support
115
123
  * - Implement variable-sized item caching
116
124
  * - Add keyboard navigation support
@@ -128,14 +136,19 @@
128
136
  * - Debug output optimization
129
137
  * - Accurate size calculations with caching
130
138
  * - Responsive size adjustments
139
+ * - Modular architecture with testable utility functions
131
140
  *
132
141
  * Current Architecture:
133
142
  * - Four-layer DOM structure for optimal performance
134
143
  * - State management using Svelte 5's $state
135
144
  * - Reactive height and scroll calculations
136
145
  * - Configurable buffer zones for smooth scrolling
137
- * - Chunked processing system for large datasets
138
- * - Separated debug utilities for better testing
146
+ * - Modular utility system with dedicated helper files:
147
+ * * scrollCalculation.ts: Complex scroll positioning logic
148
+ * * resizeObserver.ts: ResizeObserver management utilities
149
+ * * heightCalculation.ts: Debounced height measurement
150
+ * * virtualList.ts: Core virtual list calculations
151
+ * * virtualListDebug.ts: Debug information utilities
139
152
  * - Height caching and estimation system
140
153
  * - Progressive size adjustment system
141
154
  */
@@ -152,16 +165,16 @@
152
165
  calculateScrollPosition,
153
166
  calculateTransformY,
154
167
  calculateVisibleRange,
155
- getScrollOffsetForIndex,
156
- processChunked,
157
168
  updateHeightAndScroll as utilsUpdateHeightAndScroll
158
169
  } from './utils/virtualList.js'
159
170
  import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
171
+ import { calculateScrollTarget } from './utils/scrollCalculation.js'
172
+
160
173
  import { BROWSER } from 'esm-env'
161
174
  import { onMount, tick } from 'svelte'
162
175
 
163
176
  const rafSchedule = createRafScheduler()
164
- const INTERNAL_DEBUG = true
177
+ const INTERNAL_DEBUG = false
165
178
  /**
166
179
  * Core configuration props with default values
167
180
  * @type {SvelteVirtualListProps}
@@ -215,8 +228,6 @@
215
228
  */
216
229
  let heightCache = $state<Record<number, number>>({}) // Cache of measured item heights
217
230
  let dirtyItems = $state(new Set<number>()) // Set of item indices that need height recalculation
218
- const chunkSize = $state(50) // Number of items to process in each chunk
219
- let processedItems = $state(0) // Number of items processed during initialization
220
231
 
221
232
  let prevVisibleRange = $state<SvelteVirtualListPreviousVisibleRange | null>(null)
222
233
  let prevHeight = $state<number>(0)
@@ -530,53 +541,6 @@
530
541
  )
531
542
  }
532
543
 
533
- /**
534
- * Initializes large datasets in chunks to prevent UI blocking.
535
- *
536
- * This function processes items in smaller chunks using setTimeout to yield
537
- * to the main thread, allowing other UI operations to remain responsive.
538
- * Progress is tracked and reported through the processedItems state.
539
- *
540
- * For datasets larger than 1000 items, this method is automatically used
541
- * instead of immediate initialization. The chunk size is controlled by the
542
- * component's chunkSize state (default: 50).
543
- *
544
- * @async
545
- * @example
546
- * ```typescript
547
- * // Component initialization
548
- * $effect(() => {
549
- * if (BROWSER && items.length > 1000) {
550
- * initializeChunked()
551
- * } else {
552
- * initialized = true
553
- * }
554
- * })
555
- * ```
556
- *
557
- * @throws {Error} If processChunked fails to complete initialization
558
- * @returns {Promise<void>} Resolves when all chunks have been processed
559
- */
560
- const initializeChunked = async () => {
561
- if (!items.length) return
562
-
563
- await processChunked(
564
- items,
565
- chunkSize,
566
- (processed) => (processedItems = processed),
567
- () => (initialized = true)
568
- )
569
- }
570
-
571
- // Modify the mount effect to use chunked initialization
572
- $effect(() => {
573
- if (BROWSER && items.length > 1000) {
574
- initializeChunked()
575
- } else {
576
- initialized = true
577
- }
578
- })
579
-
580
544
  // Create itemResizeObserver immediately when in browser
581
545
  if (BROWSER) {
582
546
  // Watch for individual item size changes
@@ -749,144 +713,30 @@
749
713
  }
750
714
 
751
715
  const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems()
752
- let scrollTarget: number | null = null
753
716
 
754
- if (mode === 'bottomToTop') {
755
- const totalHeight = items.length * calculatedItemHeight
756
- const itemOffset = targetIndex * calculatedItemHeight
757
- const itemHeight = calculatedItemHeight
758
- if (align === 'auto') {
759
- // If item is above the viewport, align to top
760
- if (targetIndex < firstVisibleIndex) {
761
- scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
762
- // If item is below the viewport, align to bottom
763
- } else if (targetIndex > lastVisibleIndex - 1) {
764
- scrollTarget = Math.max(0, totalHeight - itemOffset - height)
765
- } else {
766
- // Item is visible but not aligned: align to nearest edge
767
- // Calculate the offset of the item relative to the viewport
768
- const itemTop = totalHeight - (itemOffset + itemHeight)
769
- const itemBottom = totalHeight - itemOffset
770
- const distanceToTop = Math.abs(scrollTop - itemTop)
771
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
772
- if (distanceToTop < distanceToBottom) {
773
- // Closer to top, align to top
774
- scrollTarget = itemTop
775
- } else {
776
- // Closer to bottom, align to bottom
777
- scrollTarget = Math.max(0, itemBottom - height)
778
- }
779
- }
780
- } else if (align === 'top') {
781
- // Align to top
782
- scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
783
- } else if (align === 'bottom') {
784
- // Align to bottom
785
- scrollTarget = Math.max(0, totalHeight - itemOffset - height)
786
- } else if (align === 'nearest') {
787
- // If not visible, align to nearest edge; if visible, do nothing
788
- const itemTop = totalHeight - (itemOffset + itemHeight)
789
- const itemBottom = totalHeight - itemOffset
790
- if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
791
- // Not visible, align to nearest edge
792
- const distanceToTop = Math.abs(scrollTop - itemTop)
793
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
794
- if (distanceToTop < distanceToBottom) {
795
- scrollTarget = itemTop
796
- } else {
797
- scrollTarget = Math.max(0, itemBottom - height)
798
- }
799
- } else {
800
- // Already visible, do nothing
801
- return
802
- }
803
- }
804
- } else {
805
- // topToBottom (default)
806
- if (align === 'auto') {
807
- // If item is above the viewport, align to top
808
- if (targetIndex < firstVisibleIndex) {
809
- scrollTarget = getScrollOffsetForIndex(
810
- heightCache,
811
- calculatedItemHeight,
812
- targetIndex
813
- )
814
- // If item is below the viewport, align to bottom
815
- } else if (targetIndex > lastVisibleIndex - 1) {
816
- const itemBottom = getScrollOffsetForIndex(
817
- heightCache,
818
- calculatedItemHeight,
819
- targetIndex + 1
820
- )
821
- scrollTarget = Math.max(0, itemBottom - height)
822
- } else {
823
- // Item is visible but not aligned: align to nearest edge
824
- const itemTop = getScrollOffsetForIndex(
825
- heightCache,
826
- calculatedItemHeight,
827
- targetIndex
828
- )
829
- const itemBottom = getScrollOffsetForIndex(
830
- heightCache,
831
- calculatedItemHeight,
832
- targetIndex + 1
833
- )
834
- const distanceToTop = Math.abs(scrollTop - itemTop)
835
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
836
- if (distanceToTop < distanceToBottom) {
837
- // Closer to top, align to top
838
- scrollTarget = itemTop
839
- } else {
840
- // Closer to bottom, align to bottom
841
- scrollTarget = Math.max(0, itemBottom - height)
842
- }
843
- }
844
- } else if (align === 'top') {
845
- scrollTarget = getScrollOffsetForIndex(
846
- heightCache,
847
- calculatedItemHeight,
848
- targetIndex
849
- )
850
- } else if (align === 'bottom') {
851
- const itemBottom = getScrollOffsetForIndex(
852
- heightCache,
853
- calculatedItemHeight,
854
- targetIndex + 1
855
- )
856
- scrollTarget = Math.max(0, itemBottom - height)
857
- } else if (align === 'nearest') {
858
- const itemTop = getScrollOffsetForIndex(
859
- heightCache,
860
- calculatedItemHeight,
861
- targetIndex
862
- )
863
- const itemBottom = getScrollOffsetForIndex(
864
- heightCache,
865
- calculatedItemHeight,
866
- targetIndex + 1
867
- )
868
- if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
869
- // Not visible, align to nearest edge
870
- const distanceToTop = Math.abs(scrollTop - itemTop)
871
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
872
- if (distanceToTop < distanceToBottom) {
873
- scrollTarget = itemTop
874
- } else {
875
- scrollTarget = Math.max(0, itemBottom - height)
876
- }
877
- } else {
878
- // Already visible, do nothing
879
- return
880
- }
881
- }
882
- }
717
+ // Use extracted scroll calculation utility
718
+ const scrollTarget = calculateScrollTarget({
719
+ mode,
720
+ align,
721
+ targetIndex,
722
+ itemsLength: items.length,
723
+ calculatedItemHeight,
724
+ height,
725
+ scrollTop,
726
+ firstVisibleIndex,
727
+ lastVisibleIndex,
728
+ heightCache
729
+ })
883
730
 
884
- if (scrollTarget !== null) {
885
- viewportElement.scrollTo({
886
- top: scrollTarget,
887
- behavior: smoothScroll ? 'smooth' : 'auto'
888
- })
731
+ // Handle early return for 'nearest' alignment when item is already visible
732
+ if (scrollTarget === null) {
733
+ return
889
734
  }
735
+
736
+ viewportElement.scrollTo({
737
+ top: scrollTarget,
738
+ behavior: smoothScroll ? 'smooth' : 'auto'
739
+ })
890
740
  }
891
741
 
892
742
  /**
@@ -985,7 +835,7 @@
985
835
  {@const debugInfo = createDebugInfo(
986
836
  visibleItems(),
987
837
  items.length,
988
- processedItems,
838
+ Object.keys(heightCache).length,
989
839
  calculatedItemHeight
990
840
  )}
991
841
  {debugFunction
@@ -47,7 +47,14 @@
47
47
  * - Added comprehensive documentation
48
48
  * - Optimized debug output to reduce noise
49
49
  *
50
- * 9. Future Improvements (Planned)
50
+ * 9. Architecture Refactoring
51
+ * - Extracted scroll calculation logic to scrollCalculation.ts utility
52
+ * - Extracted ResizeObserver utilities to resizeObserver.ts
53
+ * - Added comprehensive test coverage for extracted utilities
54
+ * - Improved separation of concerns and maintainability
55
+ * - Simplified initialization (removed unnecessary chunked processing)
56
+ *
57
+ * 10. Future Improvements (Planned)
51
58
  * - Add horizontal scrolling support
52
59
  * - Implement variable-sized item caching
53
60
  * - Add keyboard navigation support
@@ -65,14 +72,19 @@
65
72
  * - Debug output optimization
66
73
  * - Accurate size calculations with caching
67
74
  * - Responsive size adjustments
75
+ * - Modular architecture with testable utility functions
68
76
  *
69
77
  * Current Architecture:
70
78
  * - Four-layer DOM structure for optimal performance
71
79
  * - State management using Svelte 5's $state
72
80
  * - Reactive height and scroll calculations
73
81
  * - Configurable buffer zones for smooth scrolling
74
- * - Chunked processing system for large datasets
75
- * - Separated debug utilities for better testing
82
+ * - Modular utility system with dedicated helper files:
83
+ * * scrollCalculation.ts: Complex scroll positioning logic
84
+ * * resizeObserver.ts: ResizeObserver management utilities
85
+ * * heightCalculation.ts: Debounced height measurement
86
+ * * virtualList.ts: Core virtual list calculations
87
+ * * virtualListDebug.ts: Debug information utilities
76
88
  * - Height caching and estimation system
77
89
  * - Progressive size adjustment system
78
90
  */
@@ -120,15 +132,16 @@ import { type SvelteVirtualListProps, type SvelteVirtualListScrollOptions } from
120
132
  * - Only visible items + buffer are mounted in the DOM
121
133
  * - Height caching and estimation for dynamic content
122
134
  * - Handles resize events and dynamic content changes
123
- * - Supports chunked initialization for very large lists
124
- * - All scrolling logic is centralized in the scroll() method
135
+ * - Optimized for very large lists through virtualization
136
+ * - Modular architecture with extracted utility functions
125
137
  * - Bi-directional support: mode="topToBottom" or "bottomToTop"
126
138
  * - Designed for extensibility and easy debugging
127
139
  *
128
140
  * =============================
129
141
  * == For Contributors ==
130
142
  * =============================
131
- * - Please keep all scrolling logic in the scroll() method
143
+ * - Complex logic is extracted to dedicated utility files in src/lib/utils/
144
+ * - Scroll positioning logic is in scrollCalculation.ts (well-tested)
132
145
  * - Add new features behind feature flags or as optional props
133
146
  * - Write tests for all new features (see /test and /tests/scroll)
134
147
  * - Use TypeScript and Svelte 5 runes for all new code
@@ -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
+ };
@@ -40,9 +40,9 @@ export declare function shouldShowDebugInfo(prevRange: {
40
40
  *
41
41
  * This utility function generates a structured debug object that captures the complete
42
42
  * state of a virtual list at any given moment. It includes critical metrics such as
43
- * visible item count, viewport boundaries, total items, processing progress, and
44
- * height calculations. This information is essential for performance monitoring,
45
- * debugging scroll behavior, and optimizing virtual list configurations.
43
+ * visible item count, viewport boundaries, total items, processed items with measured
44
+ * heights, and height calculations. This information is essential for performance
45
+ * monitoring, debugging scroll behavior, and optimizing virtual list configurations.
46
46
  *
47
47
  * Performance considerations:
48
48
  * - All calculations are O(1)
@@ -51,7 +51,7 @@ export declare function shouldShowDebugInfo(prevRange: {
51
51
  *
52
52
  * @param visibleRange - Current visible range object containing start and end indices
53
53
  * @param totalItems - Total number of items in the virtual list
54
- * @param processedItems - Number of items that have been processed/measured
54
+ * @param processedItems - Number of items with measured heights (heightCache.length)
55
55
  * @param averageItemHeight - Current calculated average height per item in pixels
56
56
  * @returns {SvelteVirtualListDebugInfo} A structured debug information object
57
57
  *
@@ -59,8 +59,8 @@ export declare function shouldShowDebugInfo(prevRange: {
59
59
  * const debugInfo = createDebugInfo(
60
60
  * { start: 0, end: 10 },
61
61
  * 1000,
62
- * 100,
63
- * 50
62
+ * 50,
63
+ * 45
64
64
  * );
65
65
  * console.log('Virtual List State:', debugInfo);
66
66
  *
@@ -39,9 +39,9 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
39
39
  *
40
40
  * This utility function generates a structured debug object that captures the complete
41
41
  * state of a virtual list at any given moment. It includes critical metrics such as
42
- * visible item count, viewport boundaries, total items, processing progress, and
43
- * height calculations. This information is essential for performance monitoring,
44
- * debugging scroll behavior, and optimizing virtual list configurations.
42
+ * visible item count, viewport boundaries, total items, processed items with measured
43
+ * heights, and height calculations. This information is essential for performance
44
+ * monitoring, debugging scroll behavior, and optimizing virtual list configurations.
45
45
  *
46
46
  * Performance considerations:
47
47
  * - All calculations are O(1)
@@ -50,7 +50,7 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
50
50
  *
51
51
  * @param visibleRange - Current visible range object containing start and end indices
52
52
  * @param totalItems - Total number of items in the virtual list
53
- * @param processedItems - Number of items that have been processed/measured
53
+ * @param processedItems - Number of items with measured heights (heightCache.length)
54
54
  * @param averageItemHeight - Current calculated average height per item in pixels
55
55
  * @returns {SvelteVirtualListDebugInfo} A structured debug information object
56
56
  *
@@ -58,8 +58,8 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
58
58
  * const debugInfo = createDebugInfo(
59
59
  * { start: 0, end: 10 },
60
60
  * 1000,
61
- * 100,
62
- * 50
61
+ * 50,
62
+ * 45
63
63
  * );
64
64
  * console.log('Virtual List State:', debugInfo);
65
65
  *
@@ -71,7 +71,7 @@ export function createDebugInfo(visibleRange, totalItems, processedItems, averag
71
71
  startIndex: visibleRange.start,
72
72
  endIndex: visibleRange.end,
73
73
  totalItems,
74
- processedItems,
74
+ processedItems, // Number of items with measured heights in heightCache
75
75
  averageItemHeight
76
76
  };
77
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.2.6-beta.1",
3
+ "version": "0.2.6-beta.3",
4
4
  "description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -73,8 +73,7 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "esm-env": "^1.2.2",
77
- "runed": "^0.31.1"
76
+ "esm-env": "^1.2.2"
78
77
  },
79
78
  "devDependencies": {
80
79
  "@eslint/compat": "^1.3.1",