(function () {
const AUTOPLAY_MS = 4000;
const DRAG_THRESHOLD = 12; // px before we treat it as a drag
const TAP_MAX_TIME = 350; // ms. if it ends quickly and under threshold, treat as tap
const S = {
component: ".timed-slider_component",
navButton: ".timed-slider_nav-button",
slidesWrap: ".timed-slider_slides",
slide: ".timed-slider_slide",
bar: ".timed-slider_progress-bar",
activeClass: "is--active",
};
document.querySelectorAll(S.component).forEach(initSlider);
function initSlider(root) {
const wrap = root.querySelector(S.slidesWrap);
const slides = Array.from(root.querySelectorAll(S.slide));
const buttons = Array.from(root.querySelectorAll(S.navButton));
const bars = buttons.map(b => b.querySelector(S.bar));
if (!wrap || slides.length === 0 || buttons.length !== slides.length) return;
const DEFAULT_TRANSITION = "transform .6s cubic-bezier(.22,.7,.26,1)";
if (!wrap.style.transition) {
wrap.style.display = "grid";
wrap.style.gridAutoFlow = "column";
wrap.style.gridAutoColumns = "100%";
wrap.style.willChange = "transform";
wrap.style.transition = DEFAULT_TRANSITION;
}
wrap.style.touchAction = "pan-y";
let index = Math.max(0, buttons.findIndex(b => b.classList.contains(S.activeClass)));
if (index === -1) index = 0;
let rafId = null;
let startTime = 0;
function go(i, resetProgress = true) {
index = (i + slides.length) % slides.length;
setActive(index);
wrap.style.transition = DEFAULT_TRANSITION;
wrap.style.transform = `translateX(${(-100 * index)}%)`;
if (resetProgress) startProgress();
}
function setActive(i) {
buttons.forEach((btn, bi) => {
btn.classList.toggle(S.activeClass, bi === i);
btn.setAttribute("aria-current", bi === i ? "true" : "false");
});
bars.forEach((bar) => {
if (!bar) return;
bar.style.width = "0%";
});
}
function startProgress() {
cancelAnimationFrame(rafId);
const bar = bars[index];
if (!bar) return;
startTime = performance.now();
function step(now) {
const t = Math.min(1, (now - startTime) / AUTOPLAY_MS);
bar.style.width = (t * 100).toFixed(3) + "%";
if (t >= 1) {
go(index + 1);
return;
}
rafId = requestAnimationFrame(step);
}
rafId = requestAnimationFrame(step);
}
buttons.forEach((btn, i) => {
btn.addEventListener("click", () => go(i));
});
// Drag and swipe with proper tap detection
let dragging = false;
let suppressNextClick = false;
let startX = 0;
let deltaX = 0;
let baseX = 0;
let pointerId = null;
let downAt = 0;
function slideWidth() {
return slides[0].getBoundingClientRect().width;
}
function isInteractive(el) {
if (!el) return false;
const tag = el.tagName;
if (["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "LABEL"].includes(tag)) return true;
return el.closest("a,button,[role='button'],input,select,textarea,label") != null;
}
function onPointerDown(e) {
// Allow vertical scroll to start from slider, and allow taps on interactive items
pointerId = e.pointerId ?? null;
downAt = performance.now();
startX = e.clientX ?? (e.touches && e.touches[0].clientX) ?? 0;
baseX = -index * slideWidth();
deltaX = 0;
dragging = false; // we only flip to true after threshold
cancelAnimationFrame(rafId);
// We will capture only once drag starts, not immediately
window.addEventListener("pointermove", onPointerMove, { passive: true });
window.addEventListener("pointerup", onPointerUp, { once: true });
window.addEventListener("pointercancel", onPointerCancel, { once: true });
}
function onPointerMove(e) {
const x = e.clientX ?? (e.touches && e.touches[0].clientX) ?? startX;
deltaX = x - startX;
// Only start dragging after threshold and when the gesture is primarily horizontal
if (!dragging && Math.abs(deltaX) > DRAG_THRESHOLD) {
// If the press started on an interactive element, still allow drag once the user clearly drags
dragging = true;
suppressNextClick = true; // we will suppress the immediate click after a real drag
wrap.style.transition = "none";
try {
if (wrap.setPointerCapture && pointerId != null) wrap.setPointerCapture(pointerId);
} catch (_) {}
}
if (dragging) {
wrap.style.transform = `translateX(${baseX + deltaX}px)`;
}
}
function onPointerUp() {
window.removeEventListener("pointermove", onPointerMove);
const elapsed = performance.now() - downAt;
if (!dragging) {
// It was a tap. No suppression unless we barely crossed threshold extremely quickly.
const wasTinyMove = Math.abs(deltaX) <= DRAG_THRESHOLD;
const wasQuick = elapsed <= TAP_MAX_TIME;
suppressNextClick = !(wasTinyMove && wasQuick) ? false : false;
startProgress();
return;
}
// Snap if we were dragging
const width = slideWidth();
const threshold = width * 0.2;
let target = index;
if (deltaX <= -threshold) target = index + 1;
else if (deltaX >= threshold) target = index - 1;
go(target);
}
function onPointerCancel() {
window.removeEventListener("pointermove", onPointerMove);
dragging = false;
suppressNextClick = false;
startProgress();
}
// Only suppress clicks if a real drag happened
wrap.addEventListener(
"click",
function (e) {
if (!suppressNextClick) return;
// If user meant to click an interactive element but just dragged, block this one click
e.preventDefault();
e.stopPropagation();
suppressNextClick = false; // only consume once
},
true // capture to intercept before links fire
);
wrap.addEventListener("pointerdown", onPointerDown);
window.addEventListener("resize", () => {
wrap.style.transition = "none";
wrap.style.transform = `translateX(${(-100 * index)}%)`;
startProgress();
});
// Init
go(index);
}
})();