/** * Horizontal product rails with under-rail dots + 3s autoplay ([data-product-slider]). * RTL-safe scrollLeft (Chrome negative vs Firefox positive). */ export function initProductSliders() { document.querySelectorAll("[data-product-slider]").forEach((root) => { const rail = root.querySelector("[data-slider-rail]"); const dotsHost = root.querySelector("[data-slider-dots]"); if (!(rail instanceof HTMLElement)) return; const rtl = getComputedStyle(rail).direction === "rtl" || document.documentElement.dir === "rtl"; const AUTO_MS = 3000; // Chrome/Safari RTL: scrollLeft goes negative; Firefox RTL: positive. let rtlNegative = false; if (rtl) { const prevLeft = rail.scrollLeft; rail.scrollLeft = -1; rtlNegative = rail.scrollLeft < 0; rail.scrollLeft = prevLeft; } const step = () => { const card = rail.querySelector(".product-card"); const w = card instanceof HTMLElement ? card.getBoundingClientRect().width : 200; const styles = getComputedStyle(rail); const gap = Number.parseFloat(styles.columnGap || styles.gap || "1") || 1; return Math.max(w + gap, 120); }; function maxScroll() { return Math.max(0, rail.scrollWidth - rail.clientWidth); } function canScroll() { return maxScroll() > 4; } /** Absolute distance from the logical start (0…max), independent of engine sign. */ function scrollPos() { return Math.abs(rail.scrollLeft); } function scrollToPos(pos, smooth) { const max = maxScroll(); const target = Math.min(max, Math.max(0, pos)); let left = target; if (rtl) left = rtlNegative ? -target : target; rail.scrollTo({ left, behavior: smooth ? "smooth" : "auto" }); } function pageCount() { const max = maxScroll(); if (max <= 4) return 1; return Math.floor(max / step()) + 1; } function currentPage() { const pages = pageCount(); if (pages <= 1) return 0; const s = step(); return Math.min(pages - 1, Math.round(scrollPos() / s)); } function goToPage(index, smooth = true) { const pages = pageCount(); const i = Math.max(0, Math.min(pages - 1, index)); const max = maxScroll(); if (i >= pages - 1) scrollToPos(max, smooth); else scrollToPos(i * step(), smooth); } function goNext() { const max = maxScroll(); if (max <= 4) return; const pages = pageCount(); const next = currentPage() + 1; if (next >= pages) goToPage(0, true); else goToPage(next, true); } function renderDots() { if (!(dotsHost instanceof HTMLElement)) return; const pages = pageCount(); if (pages <= 1) { dotsHost.hidden = true; dotsHost.replaceChildren(); return; } dotsHost.hidden = false; const active = currentPage(); if (dotsHost.childElementCount !== pages) { const frag = document.createDocumentFragment(); for (let i = 0; i < pages; i++) { const btn = document.createElement("button"); btn.type = "button"; btn.className = "slider-dot"; btn.setAttribute("role", "tab"); btn.setAttribute("aria-label", `صفحه ${i + 1}`); btn.addEventListener("click", (e) => { e.preventDefault(); goToPage(i, true); startAuto(); }); frag.appendChild(btn); } dotsHost.replaceChildren(frag); } Array.from(dotsHost.children).forEach((el, i) => { if (!(el instanceof HTMLElement)) return; const on = i === active; el.classList.toggle("is-active", on); el.setAttribute("aria-selected", on ? "true" : "false"); if (on) el.setAttribute("aria-current", "true"); else el.removeAttribute("aria-current"); }); } let timer = 0; function stopAuto() { window.clearInterval(timer); timer = 0; } function startAuto() { stopAuto(); if (!canScroll()) return; timer = window.setInterval(goNext, AUTO_MS); } root.addEventListener("mouseenter", stopAuto); root.addEventListener("mouseleave", startAuto); root.addEventListener( "focusin", (e) => { if (e.target instanceof Element && e.target.closest(".slider-dot")) { /* keep running */ } else { stopAuto(); } }, true ); root.addEventListener("focusout", () => { window.setTimeout(() => { if (!root.contains(document.activeElement)) startAuto(); }, 0); }); rail.addEventListener( "pointerdown", () => { stopAuto(); }, { passive: true } ); rail.addEventListener( "pointerup", () => { startAuto(); }, { passive: true } ); rail.addEventListener( "scroll", () => { renderDots(); }, { passive: true } ); window.addEventListener( "resize", () => { renderDots(); startAuto(); }, { passive: true } ); // Layout may not be final on first paint (images / fonts). const kick = () => { renderDots(); startAuto(); }; kick(); window.requestAnimationFrame(kick); window.setTimeout(kick, 400); window.addEventListener("load", kick, { once: true }); }); }