2RE03-Design/js/carousel.js
2RE 53ca1fb834 Initial commit: 2RE03 portfolio site
Static HTML/CSS/JS portfolio for the design studio 2RE03: locally vendored Bootstrap 5.3.3, self-hosted Inter + Space Grotesk fonts, six pages, Collins-style scroll-snap carousels, a case-studies index with 10 detail pages, and a Web3Forms contact form.
2026-09-10 19:06:52 +08:00

156 lines
4.3 KiB
JavaScript

/* ==========================================================================
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);
});
})();