| 1 |
/** |
| 2 |
* Woo Product Images Gallery widget behavior. |
| 3 |
* |
| 4 |
* Provides gallery navigation, optional lightbox, and optional zoom. |
| 5 |
*/ |
| 6 |
(function ($) { |
| 7 |
"use strict"; |
| 8 |
|
| 9 |
const INIT_FLAG = "kaWooGalleryInit"; |
| 10 |
const KEYUP_FLAG = "kaWooGalleryKeyupBound"; |
| 11 |
|
| 12 |
// Global active lightbox controller (single active at a time). |
| 13 |
window.kaWooGalleryActive = window.kaWooGalleryActive || null; |
| 14 |
|
| 15 |
const lockScroll = () => { |
| 16 |
document.body.classList.add('ka-woo-gallery--lock'); |
| 17 |
}; |
| 18 |
|
| 19 |
const unlockScroll = () => { |
| 20 |
document.body.classList.remove('ka-woo-gallery--lock'); |
| 21 |
}; |
| 22 |
|
| 23 |
const enableZoom = (slide) => { |
| 24 |
if (!slide || slide.dataset.zoomBound === 'yes') return; |
| 25 |
if (window.matchMedia('(hover: none)').matches) return; // avoid on touch-only |
| 26 |
const full = slide.dataset.full; |
| 27 |
if (!full) return; |
| 28 |
const lens = document.createElement('div'); |
| 29 |
lens.className = 'ka-woo-gallery__zoom-lens'; |
| 30 |
lens.style.backgroundImage = `url(${full})`; |
| 31 |
slide.appendChild(lens); |
| 32 |
slide.dataset.zoomBound = 'yes'; |
| 33 |
|
| 34 |
const move = (event) => { |
| 35 |
const rect = slide.getBoundingClientRect(); |
| 36 |
const x = event.clientX - rect.left; |
| 37 |
const y = event.clientY - rect.top; |
| 38 |
const xPercent = Math.max(0, Math.min(100, (x / rect.width) * 100)); |
| 39 |
const yPercent = Math.max(0, Math.min(100, (y / rect.height) * 100)); |
| 40 |
lens.style.left = `${x - lens.offsetWidth / 2}px`; |
| 41 |
lens.style.top = `${y - lens.offsetHeight / 2}px`; |
| 42 |
lens.style.backgroundPosition = `${xPercent}% ${yPercent}%`; |
| 43 |
lens.classList.add('is-visible'); |
| 44 |
}; |
| 45 |
|
| 46 |
slide.addEventListener('mousemove', move); |
| 47 |
slide.addEventListener('mouseenter', () => lens.classList.add('is-visible')); |
| 48 |
slide.addEventListener('mouseleave', () => lens.classList.remove('is-visible')); |
| 49 |
}; |
| 50 |
|
| 51 |
const initGallery = (wrapper) => { |
| 52 |
if (!wrapper || !wrapper.dataset) return; |
| 53 |
if (wrapper.dataset[INIT_FLAG] === "1") return; |
| 54 |
wrapper.dataset[INIT_FLAG] = "1"; |
| 55 |
|
| 56 |
const slides = wrapper.querySelectorAll('.ka-woo-gallery__slide'); |
| 57 |
const thumbs = wrapper.querySelectorAll('.ka-woo-gallery__thumb'); |
| 58 |
if (!slides.length) return; |
| 59 |
|
| 60 |
const layout = wrapper.dataset.layout || 'slider'; |
| 61 |
const mobileLayout = wrapper.dataset.mobileLayout || ''; |
| 62 |
const loop = wrapper.dataset.loop === 'yes'; |
| 63 |
const autoplay = wrapper.dataset.autoplay === 'yes'; |
| 64 |
const speed = parseInt(wrapper.dataset.speed || '4000', 10); |
| 65 |
const captions = wrapper.dataset.captions === 'yes'; |
| 66 |
const lightboxEnabled = wrapper.dataset.lightbox === 'yes'; |
| 67 |
const zoomEnabled = wrapper.classList.contains('ka-woo-gallery--zoom'); |
| 68 |
const lightboxSkin = wrapper.dataset.lightboxSkin || 'dark'; |
| 69 |
const lightboxCounterEnabled = wrapper.dataset.lightboxCounter === 'yes'; |
| 70 |
const lightboxThumbsEnabled = wrapper.dataset.lightboxThumbs === 'yes'; |
| 71 |
let activeIndex = 0; |
| 72 |
let timer = null; |
| 73 |
|
| 74 |
const overlayId = `ka-woo-gallery-lightbox-${Math.random().toString(36).slice(2)}`; |
| 75 |
|
| 76 |
const lightbox = document.createElement('div'); |
| 77 |
lightbox.className = 'ka-woo-gallery__lightbox'; |
| 78 |
lightbox.id = overlayId; |
| 79 |
lightbox.innerHTML = ` |
| 80 |
<button type="button" class="ka-woo-gallery__lightbox-close" aria-label="Close">×</button> |
| 81 |
<div class="ka-woo-gallery__lightbox-body"> |
| 82 |
<div class="ka-woo-gallery__lightbox-top"> |
| 83 |
<div class="ka-woo-gallery__lightbox-counter"></div> |
| 84 |
<div class="ka-woo-gallery__lightbox-nav"> |
| 85 |
<button class="ka-woo-gallery__lb-prev" aria-label="Previous">❮</button> |
| 86 |
<button class="ka-woo-gallery__lb-next" aria-label="Next">❯</button> |
| 87 |
</div> |
| 88 |
</div> |
| 89 |
<div class="ka-woo-gallery__lightbox-media"> |
| 90 |
<img alt="" /> |
| 91 |
<div class="ka-woo-gallery__lightbox-caption"></div> |
| 92 |
</div> |
| 93 |
<div class="ka-woo-gallery__lightbox-thumbs"></div> |
| 94 |
</div> |
| 95 |
`; |
| 96 |
if (lightboxEnabled) { |
| 97 |
document.body.appendChild(lightbox); |
| 98 |
lightbox.classList.add('ka-woo-gallery__lightbox--' + lightboxSkin); |
| 99 |
} |
| 100 |
|
| 101 |
const lightboxImg = lightbox.querySelector('img'); |
| 102 |
const lightboxCaption = lightbox.querySelector('.ka-woo-gallery__lightbox-caption'); |
| 103 |
const lightboxThumbs = lightbox.querySelector('.ka-woo-gallery__lightbox-thumbs'); |
| 104 |
const lightboxCounter = lightbox.querySelector('.ka-woo-gallery__lightbox-counter'); |
| 105 |
const lbPrev = lightbox.querySelector('.ka-woo-gallery__lb-prev'); |
| 106 |
const lbNext = lightbox.querySelector('.ka-woo-gallery__lb-next'); |
| 107 |
const lbClose = lightbox.querySelector('.ka-woo-gallery__lightbox-close'); |
| 108 |
const lightboxThumbButtons = []; |
| 109 |
|
| 110 |
const updateCounter = () => { |
| 111 |
if (!lightboxCounterEnabled || !lightboxCounter) return; |
| 112 |
lightboxCounter.textContent = `${activeIndex + 1} / ${slides.length}`; |
| 113 |
}; |
| 114 |
|
| 115 |
const syncLightboxThumbs = () => { |
| 116 |
if (!lightboxThumbsEnabled || !lightboxThumbButtons.length) return; |
| 117 |
lightboxThumbButtons.forEach((btn, i) => btn.classList.toggle('is-active', i === activeIndex)); |
| 118 |
}; |
| 119 |
|
| 120 |
const setActive = (index) => { |
| 121 |
activeIndex = index; |
| 122 |
// The mobile layout overrides the desktop one in both directions: |
| 123 |
// a slider can become a grid on a phone, and a grid can become a |
| 124 |
// slider. Only the first half was handled. |
| 125 |
const isMobile = window.matchMedia('(max-width: 767px)').matches; |
| 126 |
let isGrid = layout === 'grid' || layout === 'masonry'; |
| 127 |
if (isMobile && 'grid' === mobileLayout) { |
| 128 |
isGrid = true; |
| 129 |
} else if (isMobile && 'slider' === mobileLayout) { |
| 130 |
isGrid = false; |
| 131 |
} |
| 132 |
slides.forEach((slide, i) => { |
| 133 |
slide.style.display = i === activeIndex || isGrid ? 'block' : 'none'; |
| 134 |
}); |
| 135 |
thumbs.forEach((thumb, i) => { |
| 136 |
thumb.classList.toggle('is-active', i === activeIndex); |
| 137 |
}); |
| 138 |
dots.forEach((dot, i) => { |
| 139 |
dot.classList.toggle('is-active', i === activeIndex); |
| 140 |
}); |
| 141 |
if (lightboxEnabled && captions && lightboxCaption) { |
| 142 |
const cap = slides[activeIndex]?.dataset.caption || ''; |
| 143 |
lightboxCaption.textContent = cap; |
| 144 |
lightboxCaption.style.display = cap ? 'block' : 'none'; |
| 145 |
} |
| 146 |
updateCounter(); |
| 147 |
syncLightboxThumbs(); |
| 148 |
if (zoomEnabled) { |
| 149 |
enableZoom(slides[activeIndex]); |
| 150 |
} |
| 151 |
}; |
| 152 |
|
| 153 |
thumbs.forEach((thumb, index) => { |
| 154 |
thumb.addEventListener('click', () => setActive(index)); |
| 155 |
}); |
| 156 |
|
| 157 |
const prev = wrapper.querySelector('.ka-woo-gallery__arrow--prev'); |
| 158 |
const next = wrapper.querySelector('.ka-woo-gallery__arrow--next'); |
| 159 |
const dots = wrapper.querySelectorAll('.ka-woo-gallery__dot'); |
| 160 |
|
| 161 |
const goTo = (delta) => { |
| 162 |
const total = slides.length; |
| 163 |
let target = activeIndex + delta; |
| 164 |
if (loop) { |
| 165 |
target = (target + total) % total; |
| 166 |
} else { |
| 167 |
target = Math.min(Math.max(target, 0), total - 1); |
| 168 |
} |
| 169 |
setActive(target); |
| 170 |
}; |
| 171 |
|
| 172 |
if (prev) prev.addEventListener('click', () => goTo(-1)); |
| 173 |
if (next) next.addEventListener('click', () => goTo(1)); |
| 174 |
dots.forEach((dot, idx) => dot.addEventListener('click', () => setActive(idx))); |
| 175 |
|
| 176 |
// Lightbox |
| 177 |
const openLightbox = (index) => { |
| 178 |
if (!lightboxEnabled || !lightboxImg) return; |
| 179 |
const slide = slides[index]; |
| 180 |
if (!slide) return; |
| 181 |
const src = slide.dataset.full || ''; |
| 182 |
if (!src) return; |
| 183 |
activeIndex = index; |
| 184 |
lightboxImg.src = src; |
| 185 |
if (captions && lightboxCaption) { |
| 186 |
const cap = slide.dataset.caption || ''; |
| 187 |
lightboxCaption.textContent = cap; |
| 188 |
lightboxCaption.style.display = cap ? 'block' : 'none'; |
| 189 |
} |
| 190 |
updateCounter(); |
| 191 |
syncLightboxThumbs(); |
| 192 |
lightbox.classList.add('is-open'); |
| 193 |
lockScroll(); |
| 194 |
|
| 195 |
// Set global active controller |
| 196 |
window.kaWooGalleryActive = { |
| 197 |
isOpen: () => lightbox.classList.contains("is-open"), |
| 198 |
close: closeLightbox, |
| 199 |
prev: () => { |
| 200 |
goTo(-1); |
| 201 |
openLightbox(activeIndex); |
| 202 |
}, |
| 203 |
next: () => { |
| 204 |
goTo(1); |
| 205 |
openLightbox(activeIndex); |
| 206 |
}, |
| 207 |
}; |
| 208 |
}; |
| 209 |
|
| 210 |
const closeLightbox = () => { |
| 211 |
lightbox.classList.remove('is-open'); |
| 212 |
unlockScroll(); |
| 213 |
if (window.kaWooGalleryActive && window.kaWooGalleryActive.close === closeLightbox) { |
| 214 |
window.kaWooGalleryActive = null; |
| 215 |
} |
| 216 |
}; |
| 217 |
|
| 218 |
if (lightboxEnabled) { |
| 219 |
lightbox.addEventListener('click', (event) => { |
| 220 |
const isBody = event.target.classList.contains('ka-woo-gallery__lightbox'); |
| 221 |
const isClose = event.target === lbClose; |
| 222 |
if (isBody || isClose) { |
| 223 |
closeLightbox(); |
| 224 |
} |
| 225 |
}); |
| 226 |
lightbox.querySelector('.ka-woo-gallery__lightbox-body')?.addEventListener('click', (e) => e.stopPropagation()); |
| 227 |
|
| 228 |
if (lightboxThumbsEnabled && lightboxThumbs) { |
| 229 |
slides.forEach((slide, idx) => { |
| 230 |
const imgEl = slide.querySelector('img'); |
| 231 |
const src = slide.dataset.full || imgEl?.src || ''; |
| 232 |
const btn = document.createElement('button'); |
| 233 |
btn.type = 'button'; |
| 234 |
btn.className = 'ka-woo-gallery__lightbox-thumb'; |
| 235 |
btn.innerHTML = `<span style="background-image:url('${src}')"></span>`; |
| 236 |
btn.addEventListener('click', (e) => { |
| 237 |
e.stopPropagation(); |
| 238 |
setActive(idx); |
| 239 |
openLightbox(idx); |
| 240 |
}); |
| 241 |
lightboxThumbButtons.push(btn); |
| 242 |
lightboxThumbs.appendChild(btn); |
| 243 |
}); |
| 244 |
} |
| 245 |
|
| 246 |
if (!lightboxCounterEnabled && lightboxCounter) { |
| 247 |
lightboxCounter.style.display = 'none'; |
| 248 |
} |
| 249 |
|
| 250 |
if (!lightboxThumbsEnabled && lightboxThumbs) { |
| 251 |
lightboxThumbs.style.display = 'none'; |
| 252 |
} |
| 253 |
} |
| 254 |
|
| 255 |
slides.forEach((slide, idx) => { |
| 256 |
slide.addEventListener('click', () => { |
| 257 |
if (!lightboxEnabled) return; |
| 258 |
openLightbox(idx); |
| 259 |
}); |
| 260 |
if (zoomEnabled) { |
| 261 |
enableZoom(slide); |
| 262 |
} |
| 263 |
}); |
| 264 |
|
| 265 |
if (lbPrev) lbPrev.addEventListener('click', (e) => { e.stopPropagation(); goTo(-1); openLightbox(activeIndex); }); |
| 266 |
if (lbNext) lbNext.addEventListener('click', (e) => { e.stopPropagation(); goTo(1); openLightbox(activeIndex); }); |
| 267 |
|
| 268 |
// Global key handler is bound once for all instances below. |
| 269 |
|
| 270 |
// Autoplay |
| 271 |
const startAutoplay = () => { |
| 272 |
if (!autoplay || slides.length <= 1) return; |
| 273 |
stopAutoplay(); |
| 274 |
timer = setInterval(() => { |
| 275 |
const nextIndex = loop ? (activeIndex + 1) % slides.length : Math.min(activeIndex + 1, slides.length - 1); |
| 276 |
setActive(nextIndex); |
| 277 |
}, speed > 500 ? speed : 4000); |
| 278 |
}; |
| 279 |
const stopAutoplay = () => { |
| 280 |
if (timer) { |
| 281 |
clearInterval(timer); |
| 282 |
timer = null; |
| 283 |
} |
| 284 |
}; |
| 285 |
wrapper.addEventListener('mouseenter', stopAutoplay); |
| 286 |
wrapper.addEventListener('mouseleave', startAutoplay); |
| 287 |
|
| 288 |
setActive(0); |
| 289 |
startAutoplay(); |
| 290 |
}; |
| 291 |
|
| 292 |
const bindGlobalKeyupOnce = () => { |
| 293 |
if (window[KEYUP_FLAG]) { |
| 294 |
return; |
| 295 |
} |
| 296 |
window[KEYUP_FLAG] = true; |
| 297 |
|
| 298 |
document.addEventListener("keyup", (e) => { |
| 299 |
const active = window.kaWooGalleryActive; |
| 300 |
if (!active || typeof active.isOpen !== "function" || !active.isOpen()) { |
| 301 |
return; |
| 302 |
} |
| 303 |
if (e.key === "Escape") { |
| 304 |
active.close(); |
| 305 |
} |
| 306 |
if (e.key === "ArrowLeft") { |
| 307 |
active.prev(); |
| 308 |
} |
| 309 |
if (e.key === "ArrowRight") { |
| 310 |
active.next(); |
| 311 |
} |
| 312 |
}); |
| 313 |
}; |
| 314 |
|
| 315 |
const initAll = (root) => { |
| 316 |
const ctx = root && root.querySelectorAll ? root : document; |
| 317 |
bindGlobalKeyupOnce(); |
| 318 |
ctx.querySelectorAll(".ka-woo-gallery").forEach(initGallery); |
| 319 |
}; |
| 320 |
|
| 321 |
document.addEventListener("DOMContentLoaded", () => initAll(document)); |
| 322 |
|
| 323 |
$(window).on("elementor/frontend/init", function () { |
| 324 |
elementorFrontend.hooks.addAction( |
| 325 |
"frontend/element_ready/woo_product_images_gallery.default", |
| 326 |
function ($scope) { |
| 327 |
initAll($scope && $scope[0] ? $scope[0] : document); |
| 328 |
} |
| 329 |
); |
| 330 |
}); |
| 331 |
})(jQuery); |
| 332 |
|
| 333 |
|
| 334 |
|
| 335 |
|
| 336 |
|
| 337 |
|
| 338 |
|