// Слайдер изображений для лонгридов кейсов (одна картинка + стрелки + точки). // Разметка: //
//
//
//
// ... //
//
// // //
// // ... //
//
function clampIndex(i, len) { if (len <= 0) return 0; return Math.min(Math.max(i, 0), len - 1); } function loadSlide(slide) { if (!slide) return; const img = slide.querySelector("img[data-src]"); if (img && img.dataset.src) { img.src = img.dataset.src; delete img.dataset.src; } } function loadAround(slides, index) { loadSlide(slides[index]); loadSlide(slides[index - 1]); loadSlide(slides[index + 1]); } export function initCaseMediaSlider() { const roots = document.querySelectorAll("[data-case-media-slider]"); roots.forEach((root) => { const viewport = root.querySelector("[data-slider-viewport]"); const track = root.querySelector("[data-slider-track]"); const prevBtn = root.querySelector("[data-slider-prev]"); const nextBtn = root.querySelector("[data-slider-next]"); const navBtns = [prevBtn, nextBtn].filter(Boolean); // Имитация :active для иконки (swipe-обработчик viewport сбрасывает нативный :active) navBtns.forEach((btn) => { const press = () => btn.classList.add("is-pressed"); const release = () => btn.classList.remove("is-pressed"); btn.addEventListener("pointerdown", press); btn.addEventListener("mousedown", press); btn.addEventListener("pointerup", release); btn.addEventListener("mouseup", release); btn.addEventListener("pointerleave", release); btn.addEventListener("mouseleave", release); btn.addEventListener("pointercancel", release); }); const dotsWrap = root.querySelector("[data-slider-dots]"); if (!viewport || !track) return; // Авто-подстройка ширины под первую картинку — только для --natural (auto) слайдеров, // и только если --slider-natural-w не задана вручную. Базовый/wide управляются CSS. const firstImg = root.querySelector(".case-media-slider__slide img"); const applyAutoWidth = (img) => { if (root.style.getPropertyValue("--slider-natural-w")) return; const nw = img.naturalWidth || 0; if (nw > 0) root.style.setProperty("--slider-natural-w", `${nw}px`); }; if (root.classList.contains("case-media--natural") && firstImg) { if (firstImg.complete && firstImg.naturalWidth > 0) { applyAutoWidth(firstImg); } else { firstImg.addEventListener("load", () => applyAutoWidth(firstImg), { once: true }); } } const slides = Array.from(track.querySelectorAll(":scope > .case-media-slider__slide")); const dots = dotsWrap ? Array.from(dotsWrap.querySelectorAll(":scope > [data-slider-dot]")) : []; const count = slides.length; if (count === 0) return; let current = 0; const reduceMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; // Режим анимации переключения: fade (по умолчанию) / slide / none let mode = root.getAttribute("data-slider-anim") || "fade"; function applyMode(m) { mode = m; root.classList.remove( "case-media-slider--anim-slide", "case-media-slider--anim-fade", "case-media-slider--anim-none" ); root.classList.add(`case-media-slider--anim-${m}`); if (m === "fade") { track.style.transform = "none"; } else { track.style.transform = `translateX(${-100 * current}%)`; } } applyMode(mode); function goTo(index, behavior) { const next = clampIndex(index, count); current = next; if (mode !== "fade") { const offset = -100 * next; track.style.transform = `translateX(${offset}%)`; } else { track.style.transform = "none"; } track.style.transition = reduceMotion || behavior === "auto" ? "none" : ""; slides.forEach((s, i) => { s.classList.toggle("is-active", i === next); s.setAttribute("aria-hidden", i === next ? "false" : "true"); }); dots.forEach((d, i) => { const active = i === next; d.classList.toggle("is-active", active); d.setAttribute("aria-selected", active ? "true" : "false"); d.tabIndex = active ? 0 : -1; }); if (prevBtn) prevBtn.disabled = next === 0; if (nextBtn) nextBtn.disabled = next === count - 1; loadAround(slides, next); } prevBtn?.addEventListener("click", () => goTo(current - 1)); nextBtn?.addEventListener("click", () => goTo(current + 1)); dots.forEach((dot, i) => { dot.addEventListener("click", () => goTo(i)); }); // Клавиатура: стрелки при фокусе внутри слайдера. root.addEventListener("keydown", (e) => { if (e.key === "ArrowLeft") { e.preventDefault(); goTo(current - 1); } else if (e.key === "ArrowRight") { e.preventDefault(); goTo(current + 1); } }); // Поддержка свопа по клику на точку при навигации с клавиатуры. dotsWrap?.addEventListener("keydown", (e) => { if (e.key === "Home") { e.preventDefault(); goTo(0); } else if (e.key === "End") { e.preventDefault(); goTo(count - 1); } }); // Свайп на тач/мыши (только горизонтальный). let drag = null; viewport.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; drag = { x: e.clientX, moved: false }; }); viewport.addEventListener("pointermove", (e) => { if (!drag) return; if (Math.abs(e.clientX - drag.x) > 6) drag.moved = true; }); function endDrag(e) { if (!drag) return; const dx = e.clientX - drag.x; if (drag.moved && Math.abs(dx) > 40) { goTo(current + (dx < 0 ? 1 : -1)); } drag = null; } viewport.addEventListener("pointerup", endDrag); viewport.addEventListener("pointercancel", endDrag); // Инициализация. slides.forEach((s, i) => { s.setAttribute("aria-hidden", i === 0 ? "false" : "true"); s.classList.toggle("is-active", i === 0); }); goTo(0, "auto"); // UI-переключатель режима анимации (для демо-примера), если есть в разметке. const animSwitch = root.querySelector("[data-slider-anim-switch]"); if (animSwitch) { const btns = Array.from(animSwitch.querySelectorAll("button")); const sync = () => btns.forEach((b) => { b.setAttribute("aria-pressed", String(b.dataset.mode === mode)); }); sync(); btns.forEach((b) => { b.addEventListener("click", () => { applyMode(b.dataset.mode || "slide"); sync(); }); }); } }); }