/** * TT-Core Infinite Scroll Composable (Vue 3) * Provides infinite scrolling functionality */ /** * Create an infinite scroll composable * @param {Ref} items - Reactive reference to items array * @param {Object} options - Scroll options * @returns {Object} - Composable with visible items and methods */ export function useInfiniteScroll(items, options = {}) { const { ref, computed, onMounted, onBeforeUnmount, onUpdated } = Vue; const visibleCount = ref(options.initialCount || 50); const incrementBy = options.incrementBy || 50; const sentinelRef = ref(null); let scrollObserver = null; const visibleItems = computed(() => { return items.value.slice(0, visibleCount.value); }); const hasMore = computed(() => { return visibleCount.value < items.value.length; }); const loadMore = () => { if (hasMore.value) { visibleCount.value += incrementBy; } }; const resetVisibleCount = () => { visibleCount.value = options.initialCount || 50; }; const setupScrollObserver = () => { scrollObserver = new IntersectionObserver( ([entry]) => { if (entry && entry.isIntersecting) { loadMore(); } }, { root: options.root || null, threshold: 0.1 } ); if (sentinelRef.value) { scrollObserver.observe(sentinelRef.value); } }; onMounted(() => { setupScrollObserver(); }); onBeforeUnmount(() => { if (scrollObserver) { scrollObserver.disconnect(); scrollObserver = null; } }); onUpdated(() => { // Reconnect observer when DOM updates if (scrollObserver && sentinelRef.value) { scrollObserver.disconnect(); scrollObserver.observe(sentinelRef.value); } }); return { sentinelRef, visibleItems, visibleCount, hasMore, loadMore, resetVisibleCount }; } /** * Create an infinite scroll mixin (backward compatibility) * @param {Object} options - Scroll options * @returns {Object} - Vue mixin */ export function createInfiniteScrollMixin(options = {}) { return { data() { return { visibleCount: options.initialCount || 50, incrementBy: options.incrementBy || 50, scrollObserver: null }; }, computed: { visibleItems() { const items = this[options.itemsKey || 'items'] || []; return items.slice(0, this.visibleCount); } }, mounted() { this.setupScrollObserver(); }, beforeUnmount() { if (this.scrollObserver) { this.scrollObserver.disconnect(); this.scrollObserver = null; } }, updated() { // Reconnect observer when DOM updates if (this.scrollObserver && this.$refs.sentinel) { this.scrollObserver.disconnect(); this.scrollObserver.observe(this.$refs.sentinel); } }, methods: { setupScrollObserver() { this.scrollObserver = new IntersectionObserver( ([entry]) => { if (entry && entry.isIntersecting) { this.loadMore(); } }, { root: this.$refs.tableWrap || null, threshold: 0.1 } ); if (this.$refs.sentinel) { this.scrollObserver.observe(this.$refs.sentinel); } }, loadMore() { const items = this[options.itemsKey || 'items'] || []; if (this.visibleCount < items.length) { this.visibleCount += this.incrementBy; } }, resetVisibleCount() { this.visibleCount = options.initialCount || 50; } } }; }