first deployment of kikomiko with url photos
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8d98304137
commit
ae3ed7c7b7
Vendored
+156
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user