@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.
- package/dist/SvelteVirtualList.svelte +32 -181
- package/dist/utils/initialization.d.ts +103 -0
- package/dist/utils/initialization.js +114 -0
- package/dist/utils/resizeObserver.d.ts +122 -0
- package/dist/utils/resizeObserver.js +176 -0
- package/dist/utils/scrollCalculation.d.ts +47 -0
- package/dist/utils/scrollCalculation.js +173 -0
- package/package.json +2 -3
|
@@ -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 =
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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.
|
|
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",
|