@humanspeak/svelte-virtual-list 0.2.6-beta.1 → 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.
@@ -152,16 +152,16 @@
152
152
  calculateScrollPosition,
153
153
  calculateTransformY,
154
154
  calculateVisibleRange,
155
- getScrollOffsetForIndex,
156
- processChunked,
157
155
  updateHeightAndScroll as utilsUpdateHeightAndScroll
158
156
  } from './utils/virtualList.js'
159
157
  import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
158
+ import { calculateScrollTarget } from './utils/scrollCalculation.js'
159
+ import { initializeVirtualList } from './utils/initialization.js'
160
160
  import { BROWSER } from 'esm-env'
161
161
  import { onMount, tick } from 'svelte'
162
162
 
163
163
  const rafSchedule = createRafScheduler()
164
- const INTERNAL_DEBUG = true
164
+ const INTERNAL_DEBUG = false
165
165
  /**
166
166
  * Core configuration props with default values
167
167
  * @type {SvelteVirtualListProps}
@@ -530,50 +530,15 @@
530
530
  )
531
531
  }
532
532
 
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
533
+ // Initialize the virtual list when items change
572
534
  $effect(() => {
573
- if (BROWSER && items.length > 1000) {
574
- initializeChunked()
575
- } else {
576
- initialized = true
535
+ if (BROWSER) {
536
+ initializeVirtualList({
537
+ items,
538
+ chunkSize,
539
+ onProgress: (processed) => (processedItems = processed),
540
+ onComplete: () => (initialized = true)
541
+ })
577
542
  }
578
543
  })
579
544
 
@@ -749,144 +714,30 @@
749
714
  }
750
715
 
751
716
  const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems()
752
- let scrollTarget: number | null = null
753
717
 
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
- }
718
+ // Use extracted scroll calculation utility
719
+ const scrollTarget = calculateScrollTarget({
720
+ mode,
721
+ align,
722
+ targetIndex,
723
+ itemsLength: items.length,
724
+ calculatedItemHeight,
725
+ height,
726
+ scrollTop,
727
+ firstVisibleIndex,
728
+ lastVisibleIndex,
729
+ heightCache
730
+ })
883
731
 
884
- if (scrollTarget !== null) {
885
- viewportElement.scrollTo({
886
- top: scrollTarget,
887
- behavior: smoothScroll ? 'smooth' : 'auto'
888
- })
732
+ // Handle early return for 'nearest' alignment when item is already visible
733
+ if (scrollTarget === null) {
734
+ return
889
735
  }
736
+
737
+ viewportElement.scrollTo({
738
+ top: scrollTarget,
739
+ behavior: smoothScroll ? 'smooth' : 'auto'
740
+ })
890
741
  }
891
742
 
892
743
  /**
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Configuration for virtual list initialization
3
+ */
4
+ export interface InitializationConfig {
5
+ /** Array of items to initialize */
6
+ items: unknown[];
7
+ /** Number of items to process in each chunk */
8
+ chunkSize: number;
9
+ /** Threshold above which to use chunked initialization */
10
+ chunkThreshold?: number;
11
+ /** Callback called with progress updates during chunked initialization */
12
+ onProgress?: (processedItems: number, totalItems: number) => void;
13
+ /** Callback called when initialization is complete */
14
+ onComplete?: () => void;
15
+ }
16
+ /**
17
+ * Determines whether to use chunked initialization based on item count and threshold.
18
+ *
19
+ * @param itemCount - Number of items to initialize
20
+ * @param threshold - Threshold above which chunked initialization is used (default: 1000)
21
+ * @returns True if chunked initialization should be used
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const useChunked = shouldUseChunkedInitialization(5000) // true
26
+ * const useImmediate = shouldUseChunkedInitialization(500) // false
27
+ * ```
28
+ */
29
+ export declare const shouldUseChunkedInitialization: (itemCount: number, threshold?: number) => boolean;
30
+ /**
31
+ * Initializes a virtual list with items, using chunked processing for large datasets.
32
+ *
33
+ * This function automatically determines whether to use immediate or chunked initialization
34
+ * based on the number of items. For large datasets, it processes items in chunks to
35
+ * prevent UI blocking, yielding to the main thread between chunks.
36
+ *
37
+ * @param config - Configuration object for initialization
38
+ * @returns Promise that resolves when initialization is complete
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { initializeVirtualList } from './initialization.js'
43
+ *
44
+ * // Initialize with progress tracking
45
+ * await initializeVirtualList({
46
+ * items: largeDataset,
47
+ * chunkSize: 50,
48
+ * onProgress: (processed, total) => {
49
+ * console.log(`Progress: ${processed}/${total}`)
50
+ * },
51
+ * onComplete: () => {
52
+ * console.log('Initialization complete!')
53
+ * }
54
+ * })
55
+ * ```
56
+ */
57
+ export declare const initializeVirtualList: (config: InitializationConfig) => Promise<void>;
58
+ /**
59
+ * Calculates the optimal chunk size for initialization based on item count and device capabilities.
60
+ *
61
+ * This function provides a heuristic for determining an appropriate chunk size that balances
62
+ * performance and responsiveness. It considers both the total number of items and the
63
+ * estimated processing time per item.
64
+ *
65
+ * @param itemCount - Total number of items to process
66
+ * @param baseChunkSize - Base chunk size to use as a starting point (default: 50)
67
+ * @returns Recommended chunk size
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const chunkSize = calculateOptimalChunkSize(10000) // Returns optimized chunk size
72
+ * const smallChunkSize = calculateOptimalChunkSize(100) // Returns smaller chunk size
73
+ * ```
74
+ */
75
+ export declare const calculateOptimalChunkSize: (itemCount: number, baseChunkSize?: number) => number;
76
+ /**
77
+ * Progress information for initialization
78
+ */
79
+ export interface InitializationProgress {
80
+ /** Number of items processed */
81
+ processed: number;
82
+ /** Total number of items */
83
+ total: number;
84
+ /** Percentage complete (0-100) */
85
+ percentage: number;
86
+ /** Whether initialization is complete */
87
+ isComplete: boolean;
88
+ }
89
+ /**
90
+ * Creates a progress tracking object for initialization.
91
+ *
92
+ * @param processed - Number of items processed
93
+ * @param total - Total number of items
94
+ * @returns Progress information object
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const progress = createProgressInfo(750, 1000)
99
+ * console.log(progress.percentage) // 75
100
+ * console.log(progress.isComplete) // false
101
+ * ```
102
+ */
103
+ export declare const createProgressInfo: (processed: number, total: number) => InitializationProgress;
@@ -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
+ };
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.2",
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",