|
|
@@ -0,0 +1,121 @@
|
|
|
+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<Array>|Function} source - The rendered items (ref or getter)
|
|
|
+ * @returns {{ trackRef: Ref, activeIndex: Ref<number>, pageCount: Ref<number>, 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,
|
|
|
+ };
|
|
|
+};
|