import { nextTick, onBeforeUnmount, onMounted, ref, unref, watch } from "vue"; const ITEM_MODE_RATIO = 0.8; const closestCardIndex = (cards, scrollLeft) => { const origin = cards[0].offsetLeft; let closest = 0; let closestDistance = Infinity; cards.forEach((card, index) => { const distance = Math.abs(card.offsetLeft - origin - scrollLeft); if (distance < closestDistance) { closestDistance = distance; closest = index; } }); return closest; }; /** * @param {Ref|Function} source - The rendered items (ref or getter) * @returns {{ trackRef: Ref, activeIndex: Ref, pageCount: Ref, onTrackScroll: Function }} */ export const useCarouselIndicator = (source) => { const activeIndex = ref(0); const pageCount = ref(0); const trackRef = ref(null); let resizeObserver = null; const resolveItems = () => { const value = typeof source === "function" ? source() : unref(source); return Array.isArray(value) ? value : []; }; const measure = () => { const track = trackRef.value; if (!track || !track.clientWidth) return; const cards = Array.from(track.children).filter( (element) => element.nodeType === Node.ELEMENT_NODE, ); if (!cards.length) { activeIndex.value = 0; pageCount.value = 0; return; } const isItemMode = cards[0].offsetWidth >= track.clientWidth * ITEM_MODE_RATIO; pageCount.value = isItemMode ? cards.length : Math.max(1, Math.ceil(track.scrollWidth / track.clientWidth)); if (pageCount.value <= 1) { activeIndex.value = 0; return; } const maxScroll = track.scrollWidth - track.clientWidth; if (maxScroll > 0 && track.scrollLeft >= maxScroll - 1) { activeIndex.value = pageCount.value - 1; return; } const index = isItemMode ? closestCardIndex(cards, track.scrollLeft) : Math.round(track.scrollLeft / track.clientWidth); activeIndex.value = Math.min(Math.max(index, 0), pageCount.value - 1); }; const stopObserving = () => { resizeObserver?.disconnect(); resizeObserver = null; }; const startObserving = (track) => { if (!track || typeof ResizeObserver === "undefined") return; resizeObserver = new ResizeObserver(() => measure()); resizeObserver.observe(track); }; watch( trackRef, (track) => { stopObserving(); if (track) startObserving(track); }, { flush: "post" }, ); watch( () => resolveItems().length, () => nextTick(measure), ); onMounted(() => nextTick(measure)); onBeforeUnmount(stopObserving); return { activeIndex, onTrackScroll: measure, pageCount, trackRef, }; };