/* ========================================================================== 2RE03 carousel Collins-style horizontal scroll-snap gallery. The scrolling itself is native CSS (scroll-snap) + touch/trackpad, so it works without JavaScript. This script adds: - prev/next buttons (disabled at the ends) - keyboard arrow support (when the track is focused) - drag-to-scroll for mouse/pointer devices ========================================================================== */ (function () { "use strict"; var roots = document.querySelectorAll("[data-carousel]"); if (!roots.length) { return; } function gapSize(track) { var cs = window.getComputedStyle(track); var g = parseFloat(cs.columnGap || cs.gap); return isNaN(g) ? 0 : g; } Array.prototype.forEach.call(roots, function (root) { var track = root.querySelector("[data-carousel-track]"); if (!track) { return; } var prev = root.querySelector("[data-carousel-prev]"); var next = root.querySelector("[data-carousel-next]"); function step() { var item = track.querySelector(".carousel__item"); if (!item) { return track.clientWidth * 0.8; } return item.getBoundingClientRect().width + gapSize(track); } function move(dir) { track.scrollBy({ left: dir * step(), behavior: "smooth" }); } if (prev) { prev.addEventListener("click", function () { move(-1); }); } if (next) { next.addEventListener("click", function () { move(1); }); } track.addEventListener("keydown", function (event) { if (event.key === "ArrowRight") { event.preventDefault(); move(1); } else if (event.key === "ArrowLeft") { event.preventDefault(); move(-1); } }); function syncButtons() { var max = track.scrollWidth - track.clientWidth - 1; if (prev) { prev.disabled = track.scrollLeft <= 0; } if (next) { next.disabled = track.scrollLeft >= max; } } track.addEventListener("scroll", syncButtons, { passive: true }); window.addEventListener("resize", syncButtons); syncButtons(); /* --- Drag to scroll (mouse / pen; touch scrolls natively) ------------- */ var dragging = false; var moved = false; var startX = 0; var startLeft = 0; track.addEventListener("dragstart", function (event) { event.preventDefault(); }); track.addEventListener("pointerdown", function (event) { if (event.pointerType === "touch") { return; } dragging = true; moved = false; startX = event.clientX; startLeft = track.scrollLeft; }); track.addEventListener("pointermove", function (event) { if (!dragging) { return; } var dx = event.clientX - startX; if (!moved && Math.abs(dx) > 6) { moved = true; track.classList.add("is-dragging"); try { track.setPointerCapture(event.pointerId); } catch (err) {} } if (moved) { event.preventDefault(); track.scrollLeft = startLeft - dx; } }); function snapToNearest() { var items = track.querySelectorAll(".carousel__item"); if (!items.length) { return; } var trackLeft = track.getBoundingClientRect().left; var best = null; var bestDist = Infinity; Array.prototype.forEach.call(items, function (item) { var dist = Math.abs(item.getBoundingClientRect().left - trackLeft); if (dist < bestDist) { bestDist = dist; best = item; } }); if (best) { track.scrollBy({ left: best.getBoundingClientRect().left - trackLeft, behavior: "smooth" }); } } function endDrag(event) { if (!dragging) { return; } dragging = false; track.classList.remove("is-dragging"); try { track.releasePointerCapture(event.pointerId); } catch (err) {} if (moved) { snapToNearest(); } } track.addEventListener("pointerup", endDrag); track.addEventListener("pointercancel", endDrag); /* Swallow the click that follows a drag (e.g. on linked cards). */ track.addEventListener("click", function (event) { if (moved) { event.preventDefault(); event.stopPropagation(); moved = false; } }, true); }); })();