137 lines
3.5 KiB
JavaScript
137 lines
3.5 KiB
JavaScript
/**
|
|
* Collaboration framed carousel — dots + arrows (no pause/progress bars).
|
|
* Distinct from the full-bleed promo hero under the atelier section.
|
|
*/
|
|
export function initCollabBanners() {
|
|
const root = document.querySelector("[data-collab-banners]");
|
|
if (!(root instanceof HTMLElement)) return;
|
|
|
|
const slides = [...root.querySelectorAll("[data-collab-slide]")];
|
|
const dots = [...root.querySelectorAll("[data-collab-goto]")];
|
|
const nextBtn = root.querySelector("[data-collab-next]");
|
|
const prevBtn = root.querySelector("[data-collab-prev]");
|
|
if (slides.length < 1) return;
|
|
|
|
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
let index = 0;
|
|
let timer = 0;
|
|
let paused = reduce;
|
|
|
|
const durationOf = (i) => {
|
|
const raw = Number(slides[i]?.getAttribute("data-cbs-duration") || 7000);
|
|
return Number.isFinite(raw) && raw > 0 ? raw : 7000;
|
|
};
|
|
|
|
const show = (next) => {
|
|
const i = ((next % slides.length) + slides.length) % slides.length;
|
|
index = i;
|
|
slides.forEach((slide, si) => {
|
|
const on = si === i;
|
|
slide.classList.toggle("is-active", on);
|
|
slide.setAttribute("aria-hidden", on ? "false" : "true");
|
|
});
|
|
dots.forEach((dot, di) => {
|
|
const on = di === i;
|
|
dot.classList.toggle("is-active", on);
|
|
dot.setAttribute("aria-current", on ? "true" : "false");
|
|
});
|
|
root.dataset.active = String(i);
|
|
};
|
|
|
|
const stop = () => {
|
|
window.clearInterval(timer);
|
|
timer = 0;
|
|
};
|
|
|
|
const start = () => {
|
|
stop();
|
|
if (paused || reduce || slides.length < 2) return;
|
|
timer = window.setInterval(() => {
|
|
show(index + 1);
|
|
}, durationOf(index));
|
|
};
|
|
|
|
const goTo = (i) => {
|
|
show(i);
|
|
start();
|
|
};
|
|
|
|
dots.forEach((dot) => {
|
|
dot.addEventListener("click", () => {
|
|
const i = Number(dot.getAttribute("data-collab-goto"));
|
|
if (!Number.isFinite(i)) return;
|
|
goTo(i);
|
|
});
|
|
});
|
|
|
|
nextBtn?.addEventListener("click", () => goTo(index + 1));
|
|
prevBtn?.addEventListener("click", () => goTo(index - 1));
|
|
|
|
root.addEventListener("mouseenter", () => {
|
|
paused = true;
|
|
stop();
|
|
});
|
|
root.addEventListener("mouseleave", () => {
|
|
paused = reduce;
|
|
start();
|
|
});
|
|
root.addEventListener("focusin", () => {
|
|
paused = true;
|
|
stop();
|
|
});
|
|
root.addEventListener("focusout", (e) => {
|
|
if (e.relatedTarget instanceof Node && root.contains(e.relatedTarget)) return;
|
|
paused = reduce;
|
|
start();
|
|
});
|
|
|
|
let touchX = 0;
|
|
let locked = "";
|
|
root.addEventListener(
|
|
"touchstart",
|
|
(e) => {
|
|
const t = e.changedTouches[0];
|
|
if (!t) return;
|
|
touchX = t.clientX;
|
|
locked = "";
|
|
},
|
|
{ passive: true }
|
|
);
|
|
root.addEventListener(
|
|
"touchmove",
|
|
(e) => {
|
|
const t = e.changedTouches[0];
|
|
if (!t || locked === "y") return;
|
|
const dx = t.clientX - touchX;
|
|
if (!locked && Math.abs(dx) > 12) locked = "x";
|
|
},
|
|
{ passive: true }
|
|
);
|
|
root.addEventListener(
|
|
"touchend",
|
|
(e) => {
|
|
const t = e.changedTouches[0];
|
|
if (!t || locked !== "x") return;
|
|
const dx = t.clientX - touchX;
|
|
if (Math.abs(dx) < 48) return;
|
|
if (dx < 0) goTo(index + 1);
|
|
else goTo(index - 1);
|
|
},
|
|
{ passive: true }
|
|
);
|
|
|
|
const io = new IntersectionObserver(
|
|
(entries) => {
|
|
const e = entries[0];
|
|
if (!e) return;
|
|
if (!e.isIntersecting) stop();
|
|
else if (!paused) start();
|
|
},
|
|
{ threshold: 0.25 }
|
|
);
|
|
io.observe(root);
|
|
|
|
show(0);
|
|
if (!reduce) start();
|
|
}
|