// Interactive, hydration-safe content carousel. component Carousel { props { @event initialize = function @event change = function @event previous = function @event next = function @event play = function @event pause = function @event reachStart = function @event reachEnd = function @event dragStart = function @event dragEnd = function size = "default" color = "primary" title = "" description = "" items = [] activeIndex = 0 slidesPerView = 1 gap = "0.75rem" showPagination = false isAutoPlay = false autoplayInterval = 4000 isInfiniteLoop = false isRTL = false isCentered = false isDraggable = false isAutoHeight = false isSnap = false showCounter = false thumbnails = "none" ariaLabel = "Content carousel" variant = "default" class = "" } state currentIndex = activeIndex state playing = false state dragOrigin = null state dragOffset = 0 state autoplayTimer = null state carouselRoot = null functions { function slideCount() { return items.length } function maximumIndex(count, visible) { count = slideCount() visible = Math.max(1, Number(slidesPerView) || 1) if (isCentered || isSnap) { return Math.max(0, count - 1) } return Math.max(0, count - visible) } function normalizedIndex(index, maximum) { maximum = maximumIndex() if (slideCount() === 0) { return 0 } if (isInfiniteLoop) { if (index < 0) { return maximum } if (index > maximum) { return 0 } } return Math.max(0, Math.min(index, maximum)) } function rememberRoot(sourceEvent, root) { if (!sourceEvent || !sourceEvent.currentTarget) { return } root = sourceEvent.currentTarget.closest(".wire-next--carousel") if (root) { carouselRoot = root } } function syncNavigation(sourceEvent, index, root, viewport, slides, slide, thumbnails, thumbnail, rail) { rememberRoot(sourceEvent) root = carouselRoot if (!root) { return } if (isSnap) { viewport = root.querySelector(".wire-next__carousel-viewport") slides = root.querySelectorAll(".wire-next__carousel-slide") slide = slides[index] if (viewport && slide) { viewport.scrollTo({ left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2, behavior: "smooth" }) } } thumbnails = root.querySelectorAll(".wire-next__carousel-thumbnails button") if (thumbnails[index]) { thumbnail = thumbnails[index] rail = thumbnail.parentElement rail.scrollTo({ left: thumbnail.offsetLeft - (rail.clientWidth - thumbnail.clientWidth) / 2, top: thumbnail.offsetTop - (rail.clientHeight - thumbnail.clientHeight) / 2, behavior: "smooth" }) } } function handleSnapScroll(sourceEvent, slides, viewport, viewportCenter, closestIndex, closestDistance, slide, slideCenter, distance, previousIndex) { if (!isSnap) { return } rememberRoot(sourceEvent) viewport = sourceEvent.currentTarget slides = viewport.querySelectorAll(".wire-next__carousel-slide") viewportCenter = viewport.scrollLeft + viewport.clientWidth / 2 closestIndex = currentIndex closestDistance = 1000000000 slides.forEach(function (candidate, index) { slideCenter = candidate.offsetLeft + candidate.clientWidth / 2 distance = Math.abs(slideCenter - viewportCenter) if (distance < closestDistance) { closestDistance = distance closestIndex = index } }) closestIndex = normalizedIndex(closestIndex) if (closestIndex !== currentIndex) { previousIndex = currentIndex currentIndex = closestIndex $emit("change", { index: currentIndex, previousIndex: previousIndex, item: items[currentIndex], reason: "snap" }) slides = carouselRoot.querySelectorAll(".wire-next__carousel-thumbnails button") slide = slides[currentIndex] if (slide) { viewport = slide.parentElement viewport.scrollTo({ left: slide.offsetLeft - (viewport.clientWidth - slide.clientWidth) / 2, top: slide.offsetTop - (viewport.clientHeight - slide.clientHeight) / 2, behavior: "smooth" }) } } } function selectSlide(index, reason, sourceEvent, previousIndex) { if (slideCount() === 0) { return } previousIndex = currentIndex currentIndex = normalizedIndex(Number(index)) syncNavigation(sourceEvent, currentIndex) if (previousIndex === currentIndex && reason !== "initialize") { return } $emit("change", { index: currentIndex, previousIndex: previousIndex, item: items[currentIndex], reason: reason || "select" }) if (currentIndex === 0) { $emit("reachStart", { index: currentIndex }) } if (currentIndex === maximumIndex()) { $emit("reachEnd", { index: currentIndex }) } } function previousSlide(sourceEvent, previousIndex) { previousIndex = currentIndex selectSlide(currentIndex - 1, "previous", sourceEvent) $emit("previous", { index: currentIndex, previousIndex: previousIndex, item: items[currentIndex] }) } function nextSlide(sourceEvent, previousIndex) { previousIndex = currentIndex selectSlide(currentIndex + 1, "next", sourceEvent) $emit("next", { index: currentIndex, previousIndex: previousIndex, item: items[currentIndex] }) } function handleKeydown(sourceEvent) { if (sourceEvent.key === "ArrowLeft") { sourceEvent.preventDefault() if (isRTL) { nextSlide(sourceEvent) } else { previousSlide(sourceEvent) } } if (sourceEvent.key === "ArrowRight") { sourceEvent.preventDefault() if (isRTL) { previousSlide(sourceEvent) } else { nextSlide(sourceEvent) } } if (sourceEvent.key === "Home") { sourceEvent.preventDefault() selectSlide(0, "keyboard", sourceEvent) } if (sourceEvent.key === "End") { sourceEvent.preventDefault() selectSlide(maximumIndex(), "keyboard", sourceEvent) } } function beginDrag(sourceEvent) { if (!isDraggable || isSnap) { return } sourceEvent.preventDefault() dragOrigin = sourceEvent.clientX dragOffset = 0 sourceEvent.currentTarget.setPointerCapture(sourceEvent.pointerId) $emit("dragStart", { index: currentIndex, x: dragOrigin }) } function moveDrag(sourceEvent) { if (!isDraggable || isSnap || dragOrigin === null) { return } sourceEvent.preventDefault() dragOffset = sourceEvent.clientX - dragOrigin } function cancelDrag() { dragOrigin = null dragOffset = 0 } function endDrag(sourceEvent, distance) { if (!isDraggable || isSnap || dragOrigin === null) { return } sourceEvent.preventDefault() distance = dragOffset || sourceEvent.clientX - dragOrigin dragOrigin = null dragOffset = 0 if (Math.abs(distance) > 20) { if ((distance < 0 && !isRTL) || (distance > 0 && isRTL)) { nextSlide(sourceEvent) } else { previousSlide(sourceEvent) } } $emit("dragEnd", { index: currentIndex, distance: distance }) } function advanceAutoplay() { if (currentIndex >= maximumIndex()) { selectSlide(0, "autoplay") } else { selectSlide(currentIndex + 1, "autoplay") } } function startAutoplay() { if (!isAutoPlay || playing || slideCount() < 2) { return } playing = true autoplayTimer = setInterval(advanceAutoplay, Math.max(1000, Number(autoplayInterval))) $emit("play", { index: currentIndex, interval: autoplayInterval }) } function pauseAutoplay() { if (autoplayTimer) { clearInterval(autoplayTimer) } autoplayTimer = null if (playing) { $emit("pause", { index: currentIndex }) } playing = false } } lifecycle { mount { $emit("initialize", { index: currentIndex, count: slideCount() }) startAutoplay() } unmount { if (autoplayTimer) { clearInterval(autoplayTimer) } } } view { } }