Files
Kikomiko/kikomiko/dist/js/collab-banners.js
T

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