Files
Kikomiko/kikomiko/dist/js/promo-slider.js
T

157 lines
4.2 KiB
JavaScript

/**
* KIKO-style homepage promo carousel — progress bars, pause, video sync.
*/
export function initPromoSlider() {
const root = document.querySelector("[data-promo-slider]");
if (!(root instanceof HTMLElement)) return;
const allSlides = [...root.querySelectorAll("[data-phs-slide]")];
const allBars = [...root.querySelectorAll("[data-phs-goto]")];
const allFills = [...root.querySelectorAll("[data-phs-fill]")];
const isShown = (el) => el instanceof HTMLElement && getComputedStyle(el).display !== "none";
const visibleIdx = allSlides
.map((slide, i) => (isShown(slide) ? i : -1))
.filter((i) => i >= 0);
const slides = visibleIdx.map((i) => allSlides[i]);
const bars = visibleIdx.map((i) => allBars[i]);
const fills = visibleIdx.map((i) => allFills[i]);
const toggle = root.querySelector("[data-phs-toggle]");
const iconPause = root.querySelector(".phs-icon-pause");
const iconPlay = root.querySelector(".phs-icon-play");
if (!slides.length) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let index = 0;
let paused = reduce;
let raf = 0;
let startedAt = 0;
let elapsed = 0;
const durationOf = (i) => {
const raw = Number(slides[i]?.getAttribute("data-phs-duration") || 6500);
return Number.isFinite(raw) && raw > 0 ? raw : 6500;
};
const setToggleUi = () => {
if (toggle instanceof HTMLElement) {
toggle.setAttribute("aria-pressed", paused ? "true" : "false");
toggle.setAttribute("aria-label", paused ? "پخش اسلایدر" : "توقف اسلایدر");
}
if (iconPause instanceof HTMLElement) iconPause.hidden = paused;
if (iconPlay instanceof HTMLElement) iconPlay.hidden = !paused;
};
const syncVideo = (activeIndex) => {
slides.forEach((slide, i) => {
const video = slide.querySelector("[data-phs-video]");
if (!(video instanceof HTMLVideoElement)) return;
if (i === activeIndex && !paused) {
video.muted = true;
const p = video.play();
if (p && typeof p.catch === "function") p.catch(() => {});
} else {
try {
video.pause();
} catch {
/* ignore */
}
}
});
};
const setProgress = (i, ratio) => {
fills.forEach((fill, fi) => {
if (!(fill instanceof HTMLElement)) return;
let w = 0;
if (fi < i) w = 1;
else if (fi === i) w = Math.min(1, Math.max(0, ratio));
fill.style.transform = `scaleX(${w})`;
});
};
const show = (next) => {
const i = ((next % slides.length) + slides.length) % slides.length;
index = i;
elapsed = 0;
startedAt = performance.now();
slides.forEach((slide, si) => {
const on = si === i;
slide.classList.toggle("is-active", on);
slide.setAttribute("aria-hidden", on ? "false" : "true");
});
bars.forEach((bar, bi) => bar.classList.toggle("is-active", bi === i));
setProgress(i, 0);
syncVideo(i);
};
const tick = (now) => {
raf = 0;
if (paused) return;
elapsed = now - startedAt;
const dur = durationOf(index);
setProgress(index, elapsed / dur);
if (elapsed >= dur) {
show(index + 1);
}
raf = requestAnimationFrame(tick);
};
const start = () => {
if (paused || reduce) return;
if (!raf) {
startedAt = performance.now() - elapsed;
raf = requestAnimationFrame(tick);
}
};
const stop = () => {
if (raf) {
cancelAnimationFrame(raf);
raf = 0;
}
};
bars.forEach((bar, i) => {
bar.addEventListener("click", () => {
stop();
show(i);
start();
});
});
toggle?.addEventListener("click", () => {
paused = !paused;
setToggleUi();
if (paused) {
stop();
elapsed = performance.now() - startedAt;
syncVideo(index);
} else {
start();
syncVideo(index);
}
});
// Pause autoplay when off-screen
const io = new IntersectionObserver(
(entries) => {
const e = entries[0];
if (!e) return;
if (!e.isIntersecting) {
stop();
syncVideo(-1);
} else if (!paused) {
start();
syncVideo(index);
}
},
{ threshold: 0.2 }
);
io.observe(root);
setToggleUi();
show(0);
if (!reduce) start();
}