import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; /** * Reusable Infinite Scroll Composable * @param {import('vue').Ref} sourceData - The full source array of data * @param {number} pageSize - Number of items to load per "page" * @param {Object} options - Additional options * @returns {Object} { visibleItems, targetRef, loadMore, reset } */ export function useInfiniteScroll(sourceData, pageSize = 20, options = {}) { const visibleCount = ref(pageSize); const targetRef = ref(null); // Compute visible items based on current count const visibleItems = computed(() => { const data = sourceData.value || []; return data.slice(0, visibleCount.value); }); // Check if there are more items to load const hasMore = computed(() => { const data = sourceData.value || []; return visibleCount.value < data.length; }); const loadMore = () => { if (hasMore.value) { visibleCount.value += pageSize; } }; const reset = () => { visibleCount.value = pageSize; }; // Setup IntersectionObserver let observer = null; onMounted(() => { observer = new IntersectionObserver((entries) => { const entry = entries[0]; if (entry.isIntersecting && hasMore.value) { loadMore(); } }, { root: null, rootMargin: '100px', // Preload before reaching bottom threshold: 0.1, ...options }); if (targetRef.value) { observer.observe(targetRef.value); } }); onUnmounted(() => { if (observer) observer.disconnect(); }); // Watch for targetRef changes (e.g., if valid status changes) watch(targetRef, (el) => { if (observer) { observer.disconnect(); if (el) observer.observe(el); } }); // Reset when source data changes significantly (optional, depending on use case) // watch(sourceData, () => { // reset(); // Uncomment if you want to reset scroll on data refresh // }); return { visibleItems, targetRef, loadMore, reset, hasMore }; }