const carouselScrollEpsilon = 2; const fallbackCarouselOverlap = 96; const itemOverlapRatio = 0.45; type TopPickCarousel = { track: HTMLElement; previous: HTMLButtonElement; next: HTMLButtonElement; previousFade: HTMLElement; nextFade: HTMLElement; }; const getTopPickCarousel = (root: HTMLElement): TopPickCarousel | null => { const track = root.querySelector("[data-top-pick-track]"); const previous = root.querySelector("[data-top-pick-prev]"); const next = root.querySelector("[data-top-pick-next]"); const previousFade = root.querySelector("[data-top-pick-prev-fade]"); const nextFade = root.querySelector("[data-top-pick-next-fade]"); if (!track || !previous || !next || !previousFade || !nextFade) { return null; } return { track, previous, next, previousFade, nextFade }; }; const topPickCarousels = (root: ParentNode = document): HTMLElement[] => [ ...root.querySelectorAll("[data-top-pick-carousel]"), ]; const maxScrollLeft = (track: HTMLElement): number => Math.max(0, track.scrollWidth - track.clientWidth); const updateTopPickCarousel = (root: HTMLElement): void => { const carousel = getTopPickCarousel(root); if (!carousel) { return; } const maxScroll = maxScrollLeft(carousel.track); const canScroll = maxScroll > carouselScrollEpsilon; const hasPrevious = canScroll && carousel.track.scrollLeft > carouselScrollEpsilon; const hasNext = canScroll && carousel.track.scrollLeft < maxScroll - carouselScrollEpsilon; carousel.previous.disabled = false; carousel.next.disabled = false; carousel.previous.classList.toggle("hidden", !hasPrevious); carousel.previous.classList.toggle("inline-flex", hasPrevious); carousel.previous.setAttribute("aria-hidden", String(!hasPrevious)); carousel.previous.tabIndex = hasPrevious ? 0 : -1; carousel.previousFade.classList.toggle("hidden", !hasPrevious); carousel.next.classList.toggle("hidden", !hasNext); carousel.next.classList.toggle("inline-flex", hasNext); carousel.next.setAttribute("aria-hidden", String(!hasNext)); carousel.next.tabIndex = hasNext ? 0 : -1; carousel.nextFade.classList.toggle("hidden", !hasNext); }; const updateTopPickCarousels = (root: ParentNode = document): void => { topPickCarousels(root).forEach(updateTopPickCarousel); }; const carouselScrollAmount = (track: HTMLElement): number => { const firstItem = track.querySelector("[data-top-pick-item]"); if (!firstItem) { return Math.max(160, track.clientWidth - fallbackCarouselOverlap); } const itemWidth = firstItem.getBoundingClientRect().width; const overlap = Math.max(fallbackCarouselOverlap, itemWidth * itemOverlapRatio); return Math.max(itemWidth, track.clientWidth - Math.min(itemWidth, overlap)); }; const scrollTopPickCarousel = (root: HTMLElement, direction: -1 | 1): void => { const carousel = getTopPickCarousel(root); if (!carousel) { return; } const currentScroll = carousel.track.scrollLeft; const targetScroll = direction < 0 ? Math.max(0, currentScroll - carouselScrollAmount(carousel.track)) : Math.min( maxScrollLeft(carousel.track), currentScroll + carouselScrollAmount(carousel.track), ); carousel.track.scrollTo({ left: targetScroll, behavior: "smooth" }); window.setTimeout(() => { updateTopPickCarousel(root); }, 350); }; document.addEventListener( "click", (event: MouseEvent): void => { const { target } = event; if (!(target instanceof Element)) { return; } const previous = target.closest("[data-top-pick-prev]"); if (previous) { event.preventDefault(); event.stopPropagation(); const root = previous.closest("[data-top-pick-carousel]"); if (root) { scrollTopPickCarousel(root, -1); } return; } const next = target.closest("[data-top-pick-next]"); if (!next) { return; } event.preventDefault(); event.stopPropagation(); const root = next.closest("[data-top-pick-carousel]"); if (root) { scrollTopPickCarousel(root, 1); } }, true, ); document.addEventListener( "scroll", (event: Event): void => { const { target } = event; if (!(target instanceof HTMLElement) || !target.matches("[data-top-pick-track]")) { return; } const root = target.closest("[data-top-pick-carousel]"); if (root) { updateTopPickCarousel(root); } }, true, ); onReady(() => { updateTopPickCarousels(); }); onHtmxLoad((root) => { updateTopPickCarousels(root); }); window.addEventListener("resize", () => { updateTopPickCarousels(); }); import { onHtmxLoad, onReady } from "./utils";