(function () { let mobileScrollTimer = null; function getProductVisual() { return [...document.querySelectorAll('.product_visual')].find((el) => { return !el.querySelector('.product_visual'); }); } function initProductImageSwiperMarkup() { const el = getProductVisual(); if (!el) return; if (el.dataset.scriptInitialized) return; el.dataset.scriptInitialized = 'true'; const gridItems = [...el.querySelectorAll('.product_img_grid_item')]; const modal = el.querySelector('[data-modal="product-images"]'); const mobileWrapper = el.querySelector('.product_swiper_wrap'); const instances = { modalSwiper: null, productSwiper: null, thumbSwiper: null, initPromise: null, }; let isSyncingSwipers = false; let isOpeningModal = false; const defaults = { slidesPerView: 'auto', slideToClickedSlide: false, grabCursor: true, keyboard: true, preventClicks: false, preventClicksPropagation: false, touchStartPreventDefault: false, pagination: { bulletClass: 'swiper-page-bullet', bulletActiveClass: 'is-active', }, }; function cloneProductSlide(gridItem, slideClass, index) { const slide = document.createElement('div'); slide.setAttribute('role', 'listitem'); slide.dataset.slideIndex = String(index); slide.className = `swiper-slide ${slideClass} w-dyn-item`; const productSlide = gridItem.querySelector('.product_slide')?.cloneNode(true); if (!productSlide) return slide; const img = productSlide.querySelector('img'); if (img) { img.setAttribute('fx-scale', 'false'); img.loading = index === 0 ? 'eager' : 'lazy'; img.fetchPriority = index === 0 ? 'high' : 'low'; } slide.appendChild(productSlide); return slide; } function cloneThumbSlide(gridItem, index) { const slide = document.createElement('div'); slide.setAttribute('role', 'listitem'); slide.dataset.slideIndex = String(index); slide.className = 'swiper-slide is-thumb w-dyn-item'; const sourceImg = gridItem.querySelector('img'); if (sourceImg) { const img = sourceImg.cloneNode(false); img.className = 'thumb_swiper_img'; img.loading = 'lazy'; img.removeAttribute('height'); img.setAttribute('width', '64'); img.setAttribute('fx-scale', 'false'); slide.appendChild(img); } return slide; } function fillWrapper(wrapper, slides) { if (!wrapper) return; wrapper.replaceChildren(...slides); } function buildPaginationBullets(paginationEl, count) { if (!paginationEl || !count) return; const bullets = Array.from({ length: count }, (_, index) => { const bullet = document.createElement('span'); bullet.className = 'swiper-page-bullet'; if (index === 0) { bullet.classList.add('is-active'); } return bullet; }); paginationEl.replaceChildren(...bullets); } function setInitialThumbActiveState() { if (!mobileWrapper) return; const thumbSlides = mobileWrapper.querySelectorAll('.thumb_swiper .swiper-slide'); thumbSlides.forEach((slide, index) => { slide.classList.toggle('is-active', index === 0); }); } function getSlideIndexFromEvent(e) { if (!mobileWrapper || !e.target) return 0; const thumbSlide = e.target.closest('.thumb_swiper .swiper-slide'); if (thumbSlide) { const index = Number(thumbSlide.dataset.slideIndex); return Number.isFinite(index) ? index : 0; } const productSlide = e.target.closest('.product_swiper .swiper-slide'); if (productSlide) { const index = Number(productSlide.dataset.slideIndex); return Number.isFinite(index) ? index : 0; } return instances.productSwiper?.activeIndex || 0; } function syncSwiper(sourceSwiper, targetSwiper) { if (!sourceSwiper || !targetSwiper) return; if (isSyncingSwipers || isOpeningModal) return; if (!modal?.open) return; const index = sourceSwiper.realIndex ?? sourceSwiper.activeIndex ?? 0; isSyncingSwipers = true; targetSwiper.slideTo(index, 0, false); targetSwiper.update(); targetSwiper.pagination?.update(); requestAnimationFrame(() => { isSyncingSwipers = false; }); } function bindSwiperSync() { if (!instances.productSwiper || !instances.modalSwiper) return; if (instances.modalSwiper.__productModalSyncBound) return; instances.modalSwiper.__productModalSyncBound = true; /* Important: Do NOT sync productSwiper -> modalSwiper. Product swiper uses slidesPerView: 'auto', so on mobile/iOS it can clamp activeIndex near the end of the carousel. If we sync that back into the modal, the modal gets moved to the wrong slide after opening. Modal is the source of truth while open. */ instances.modalSwiper.on('slideChange transitionEnd touchEnd', () => { if (!modal?.open) return; syncSwiper(instances.modalSwiper, instances.productSwiper); }); } function forceModalToIndex(index) { isSyncingSwipers = true; instances.modalSwiper?.update(); instances.modalSwiper?.slideTo(index, 0, false); instances.modalSwiper?.pagination?.update(); requestAnimationFrame(() => { isSyncingSwipers = false; }); } function forceProductToIndex(index) { instances.productSwiper?.slideTo(index, 0, false); instances.productSwiper?.pagination?.update(); instances.thumbSwiper?.update(); } function buildMobileSwiperMarkup() { if (!mobileWrapper || mobileWrapper.dataset.generated === 'true') return; fillWrapper( mobileWrapper.querySelector('.product_swiper .swiper-wrapper'), gridItems.map((item, index) => cloneProductSlide(item, 'is-product', index)), ); fillWrapper( mobileWrapper.querySelector('.thumb_swiper .swiper-wrapper'), gridItems.map((item, index) => cloneThumbSlide(item, index)), ); const productPaginationEl = mobileWrapper.querySelector('.product_swiper_pagination [swiper-nav-pagination]') || mobileWrapper.querySelector('.product_swiper_pagination .swiper-page-nav'); buildPaginationBullets(productPaginationEl, gridItems.length); setInitialThumbActiveState(); mobileWrapper.dataset.generated = 'true'; mobileWrapper.removeAttribute('aria-hidden'); mobileWrapper.querySelectorAll('.product_swiper .swiper-slide').forEach((slide, fallbackIndex) => { let startX = 0; let startY = 0; let moved = false; slide.addEventListener( 'touchstart', (e) => { const touch = e.touches && e.touches[0]; if (!touch) return; startX = touch.clientX; startY = touch.clientY; moved = false; }, { passive: true }, ); slide.addEventListener( 'touchmove', (e) => { const touch = e.touches && e.touches[0]; if (!touch) return; const diffX = Math.abs(touch.clientX - startX); const diffY = Math.abs(touch.clientY - startY); if (diffX > 5 || diffY > 5) moved = true; }, { passive: true }, ); slide.addEventListener('click', (e) => { if (!e.isTrusted) return; if (moved) return; const swiper = instances.productSwiper; if (swiper && swiper.allowClick === false) return; const clickedSlide = e.target.closest('.product_swiper .swiper-slide'); const clickedIndex = Number(clickedSlide?.dataset.slideIndex); const index = Number.isFinite(clickedIndex) ? clickedIndex : fallbackIndex; openModal(index, e); }); }); } function buildModalSwiperMarkup() { if (!modal || modal.dataset.generated === 'true') return; fillWrapper( modal.querySelector('.modal_swiper .swiper-wrapper'), gridItems.map((item, index) => cloneProductSlide(item, 'is-product-modal', index)), ); const modalPaginationEl = modal.querySelector('.modal_swiper_pagination [swiper-nav-pagination]') || modal.querySelector('.modal_swiper_pagination .swiper-page-nav'); buildPaginationBullets(modalPaginationEl, gridItems.length); modal.dataset.generated = 'true'; } function initMobileSwiper() { if (!mobileWrapper || instances.productSwiper) return; if (typeof Swiper === 'undefined') return; const thumbSwiperEl = mobileWrapper.querySelector('.thumb_swiper'); const swiperEl = mobileWrapper.querySelector('.product_swiper'); if (!thumbSwiperEl || !swiperEl) return; const paginationEl = mobileWrapper.querySelector('.product_swiper_pagination [swiper-nav-pagination]') || mobileWrapper.querySelector('.product_swiper_pagination .swiper-page-nav'); instances.thumbSwiper = new Swiper(thumbSwiperEl, { slidesPerView: 'auto', freeMode: true, watchSlidesProgress: true, }); instances.productSwiper = new Swiper(swiperEl, { ...defaults, observer: true, observeParents: true, pagination: { ...defaults.pagination, el: paginationEl, }, thumbs: { swiper: instances.thumbSwiper, slideThumbActiveClass: 'is-active', }, }); bindSwiperSync(); } function initModalSwiper() { if (!modal || instances.modalSwiper) return; if (typeof Swiper === 'undefined') return; const swiperEl = modal.querySelector('.modal_swiper'); if (!swiperEl) return; const paginationEl = modal.querySelector('.modal_swiper_pagination [swiper-nav-pagination]') || modal.querySelector('.modal_swiper_pagination .swiper-page-nav'); instances.modalSwiper = new Swiper(swiperEl, { ...defaults, // Modal/lightbox mora imeti jasen 1:1 index mapping. slidesPerView: 1, spaceBetween: 0, slideToClickedSlide: false, observer: true, observeParents: true, pagination: { ...defaults.pagination, el: paginationEl, }, navigation: { prevEl: modal.querySelector('[swiper-nav-prev]'), nextEl: modal.querySelector('[swiper-nav-next]'), disabledClass: 'is-disabled', }, }); bindSwiperSync(); } function updateSwipers() { requestAnimationFrame(() => { instances.productSwiper?.update(); instances.thumbSwiper?.update(); instances.modalSwiper?.update(); instances.productSwiper?.pagination?.update(); instances.modalSwiper?.pagination?.update(); }); } function ensureSwipers(index = 0, triggerEvent) { if (triggerEvent && triggerEvent.isTrusted !== true) { return Promise.resolve(); } buildMobileSwiperMarkup(); buildModalSwiperMarkup(); if (typeof window.loadSwiper !== 'function') { console.warn('window.loadSwiper is missing'); return Promise.resolve(); } if (!instances.initPromise) { instances.initPromise = window.loadSwiper().then(() => { initMobileSwiper(); initModalSwiper(); el.classList.add('is-swiper-ready'); updateSwipers(); return new Promise((resolve) => { requestAnimationFrame(() => { forceProductToIndex(index); if (modal?.open) { forceModalToIndex(index); } updateSwipers(); resolve(); }); }); }); } else { instances.initPromise = instances.initPromise.then(() => { return new Promise((resolve) => { requestAnimationFrame(() => { forceProductToIndex(index); if (modal?.open) { forceModalToIndex(index); } updateSwipers(); resolve(); }); }); }); } return instances.initPromise; } function openModal(index, triggerEvent) { if (!modal) return; if (triggerEvent && triggerEvent.isTrusted !== true) return; isOpeningModal = true; ensureSwipers(index, triggerEvent).then(() => { if (!modal.open) modal.showModal(); requestAnimationFrame(() => { instances.modalSwiper?.update(); requestAnimationFrame(() => { forceModalToIndex(index); // Product lahko uskladimo, ampak product nikoli ne sme prepisati modala. forceProductToIndex(index); requestAnimationFrame(() => { isOpeningModal = false; }); }); }); }); } function bindMobileLocalInit() { if (!mobileWrapper) return; const initOnly = (e) => { if (!e.isTrusted) return; const index = getSlideIndexFromEvent(e); ensureSwipers(index >= 0 ? index : 0); }; mobileWrapper.addEventListener('click', initOnly, { once: true, capture: true, }); } function bindMobileSwipeInit() { if (!mobileWrapper) return; let swipeInitTimer = null; const initOnSwipe = (e) => { if (!e.isTrusted) return; clearTimeout(swipeInitTimer); swipeInitTimer = setTimeout(() => { requestAnimationFrame(() => { const index = instances.productSwiper?.activeIndex || 0; ensureSwipers(index); }); }, 120); }; mobileWrapper.addEventListener('touchmove', initOnSwipe, { once: true, passive: true, }); mobileWrapper.addEventListener('pointermove', initOnSwipe, { once: true, passive: true, }); } gridItems.forEach((item, index) => { item.addEventListener('click', (e) => { if (!e.isTrusted) return; openModal(index, e); }); }); modal?.querySelectorAll('[modal-close]').forEach((trigger) => { trigger.addEventListener('click', (e) => { if (!e.isTrusted) return; modal.close(); }); }); el.addEventListener('product-swiper-load', (event) => { const index = event.detail?.index ?? 0; ensureSwipers(index); }); bindMobileLocalInit(); bindMobileSwipeInit(); buildMobileSwiperMarkup(); buildModalSwiperMarkup(); el.classList.add('is-swiper-markup-ready'); } function dispatchProductSwiperLoad(originalEvent, index = 0) { const el = getProductVisual(); if (!el || el.dataset.scriptInitialized !== 'true') return; el.dispatchEvent( new CustomEvent('product-swiper-load', { detail: { originalEvent, index, }, }), ); } function onInteraction(e) { if (!e || e.isTrusted !== true) return; cleanupInteractionListeners(); dispatchProductSwiperLoad(e, 0); } function onMobileScrollInteraction(e) { if (!e || e.isTrusted !== true) return; clearTimeout(mobileScrollTimer); mobileScrollTimer = setTimeout(() => { cleanupInteractionListeners(); requestAnimationFrame(() => { requestAnimationFrame(() => { dispatchProductSwiperLoad(e, 0); }); }); }, 300); } function cleanupInteractionListeners() { clearTimeout(mobileScrollTimer); window.removeEventListener('scroll', onInteraction, true); window.removeEventListener('scroll', onMobileScrollInteraction, true); document.removeEventListener('click', onInteraction, true); document.removeEventListener('keydown', onInteraction, true); } function bindInteractionListeners() { const isMobile = window.matchMedia('(max-width: 767px)').matches; if (isMobile) { window.addEventListener('scroll', onMobileScrollInteraction, { passive: true, capture: true, }); } else { window.addEventListener('scroll', onInteraction, { once: true, passive: true, capture: true, }); } document.addEventListener('click', onInteraction, { once: true, capture: true, }); document.addEventListener('keydown', onInteraction, { once: true, capture: true, }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initProductImageSwiperMarkup(); bindInteractionListeners(); }); } else { initProductImageSwiperMarkup(); bindInteractionListeners(); } })();