157 lines
4.2 KiB
JavaScript
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();
|
|
}
|