[READ-ONLY] a fast, modern browser for the npm registry
at main 114 lines 3.4 kB view raw
1import type { MaybeRefOrGetter, Ref } from 'vue' 2 3export interface WindowVirtualizerHandle { 4 readonly scrollOffset: number 5 readonly viewportSize: number 6 findItemIndex: (offset: number) => number 7 getItemOffset: (index: number) => number 8 scrollToIndex: ( 9 index: number, 10 opts?: { align?: 'start' | 'center' | 'end'; smooth?: boolean }, 11 ) => void 12} 13 14export interface UseVirtualInfiniteScrollOptions { 15 /** Reference to the WindowVirtualizer component */ 16 listRef: Ref<WindowVirtualizerHandle | null> 17 /** Current item count */ 18 itemCount: Ref<number> 19 /** Whether there are more items to load */ 20 hasMore: Ref<boolean> 21 /** Whether currently loading */ 22 isLoading: Ref<boolean> 23 /** Page size for calculating current page (reactive) */ 24 pageSize: MaybeRefOrGetter<number> 25 /** Threshold in items before end to trigger load */ 26 threshold?: number 27 /** Callback to load more items */ 28 onLoadMore: () => void 29 /** Callback when visible page changes (for URL updates) */ 30 onPageChange?: (page: number) => void 31} 32 33/** 34 * Composable for handling infinite scroll with virtua's WindowVirtualizer 35 * Detects when user scrolls near the end and triggers loading more items 36 * Also tracks current visible page for URL persistence 37 */ 38export function useVirtualInfiniteScroll(options: UseVirtualInfiniteScrollOptions) { 39 const { 40 listRef, 41 itemCount, 42 hasMore, 43 isLoading, 44 pageSize, 45 threshold = 5, 46 onLoadMore, 47 onPageChange, 48 } = options 49 50 // Track last fetched count to prevent duplicate fetches 51 const fetchedCountRef = shallowRef(-1) 52 53 // Track current page to avoid unnecessary updates 54 const currentPage = shallowRef(1) 55 56 function handleScroll() { 57 const list = listRef.value 58 if (!list) return 59 60 // Calculate current visible page based on first visible item 61 const startIndex = list.findItemIndex(list.scrollOffset) 62 const currentPageSize = toValue(pageSize) 63 const newPage = Math.floor(startIndex / currentPageSize) + 1 64 65 if (newPage !== currentPage.value && onPageChange) { 66 currentPage.value = newPage 67 onPageChange(newPage) 68 } 69 70 // Don't fetch if already loading or no more items 71 if (isLoading.value || !hasMore.value) return 72 73 // Don't fetch if we already fetched at this count 74 const count = itemCount.value 75 if (fetchedCountRef.value >= count) return 76 77 // Check if we're near the end 78 const endOffset = list.scrollOffset + list.viewportSize 79 const endIndex = list.findItemIndex(endOffset) 80 81 if (endIndex + threshold >= count) { 82 fetchedCountRef.value = count 83 onLoadMore() 84 } 85 } 86 87 /** 88 * Scroll to a specific page (1-indexed) 89 * Call this after data is loaded to restore scroll position 90 */ 91 function scrollToPage(page: number) { 92 const list = listRef.value 93 if (!list || page < 1) return 94 95 const targetIndex = (page - 1) * toValue(pageSize) 96 list.scrollToIndex(targetIndex, { align: 'start' }) 97 currentPage.value = page 98 } 99 100 // Reset state when item count changes significantly (new search) 101 watch(itemCount, (newCount, oldCount) => { 102 // If count decreased or reset, clear the fetched tracking 103 if (newCount < oldCount || newCount === 0) { 104 fetchedCountRef.value = -1 105 currentPage.value = 1 106 } 107 }) 108 109 return { 110 handleScroll, 111 scrollToPage, 112 currentPage, 113 } 114}