import { computed, onBeforeUnmount, onMounted, ref, unref, watch, type ComputedRef, type Ref } from 'vue'; type ReactiveValue = Ref | ComputedRef; type UseIncrementalListOptions = { pageSize?: number; enabled?: ReactiveValue; resetKeys?: Array | unknown>; }; export function useIncrementalList( items: ReactiveValue, options: UseIncrementalListOptions = {}, ) { const pageSize = options.pageSize ?? 24; const enabled = computed(() => unref(options.enabled ?? true)); const visibleCount = ref(pageSize); const loadMoreSentinel = ref(null); let observer: IntersectionObserver | null = null; const visibleItems = computed(() => ( enabled.value ? items.value.slice(0, visibleCount.value) : items.value )); const canLoadMore = computed(() => ( enabled.value && visibleCount.value < items.value.length )); const remainingCount = computed(() => Math.max(items.value.length - visibleCount.value, 0)); function loadMore() { visibleCount.value = Math.min(visibleCount.value + pageSize, items.value.length); } function resetVisibleCount() { visibleCount.value = pageSize; } function disconnectObserver() { observer?.disconnect(); observer = null; } function connectObserver() { disconnectObserver(); if (!canLoadMore.value || !loadMoreSentinel.value) { return; } observer = new IntersectionObserver((entries) => { if (!entries.some((entry) => entry.isIntersecting)) { return; } loadMore(); }, { rootMargin: '240px 0px', }); observer.observe(loadMoreSentinel.value); } const resetSignature = computed(() => JSON.stringify([ enabled.value, items.value.length, ...((options.resetKeys ?? []).map((entry) => unref(entry as ReactiveValue))), ])); watch(resetSignature, resetVisibleCount, { immediate: true }); watch(canLoadMore, () => { if (!canLoadMore.value) { disconnectObserver(); } }); onMounted(() => { watch([loadMoreSentinel, canLoadMore], connectObserver, { immediate: true }); }); onBeforeUnmount(disconnectObserver); return { canLoadMore, loadMore, loadMoreSentinel, remainingCount, visibleCount, visibleItems, }; }