PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 All 41 releases
king-addons / includes / widgets / Woo_Product_Images_Gallery / script.js

script.js in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.87, at includes/widgets/Woo_Product_Images_Gallery/script.js

350 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 <div class="ka-woo-gallery__lightbox-body">
81 <div class="ka-woo-gallery__lightbox-top">
82 <div class="ka-woo-gallery__lightbox-counter"></div>
83 <div class="ka-woo-gallery__lightbox-nav">
84 <button class="ka-woo-gallery__lb-prev" aria-label="Previous">&#10094;</button>
85 <button class="ka-woo-gallery__lb-next" aria-label="Next">&#10095;</button>
86 <button type="button" class="ka-woo-gallery__lightbox-close" aria-label="Close">&times;</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 if (lbClose) {
220 lbClose.addEventListener('click', (event) => {
221 event.preventDefault();
222 event.stopPropagation();
223 closeLightbox();
224 });
225 }
226 lightbox.addEventListener('click', (event) => {
227 const isBackdrop = event.target.classList.contains('ka-woo-gallery__lightbox');
228 const isClose = event.target.closest('.ka-woo-gallery__lightbox-close');
229 if (isBackdrop || isClose) {
230 closeLightbox();
231 }
232 });
233 lightbox.querySelector('.ka-woo-gallery__lightbox-body')?.addEventListener('click', (e) => {
234 if (e.target.closest('.ka-woo-gallery__lightbox-close')) {
235 return;
236 }
237 e.stopPropagation();
238 });
239
240 if (lightboxThumbsEnabled && lightboxThumbs) {
241 slides.forEach((slide, idx) => {
242 const imgEl = slide.querySelector('img');
243 const src = slide.dataset.full || imgEl?.src || '';
244 const btn = document.createElement('button');
245 btn.type = 'button';
246 btn.className = 'ka-woo-gallery__lightbox-thumb';
247 btn.innerHTML = `<span style="background-image:url('${src}')"></span>`;
248 btn.addEventListener('click', (e) => {
249 e.stopPropagation();
250 setActive(idx);
251 openLightbox(idx);
252 });
253 lightboxThumbButtons.push(btn);
254 lightboxThumbs.appendChild(btn);
255 });
256 }
257
258 if (!lightboxCounterEnabled && lightboxCounter) {
259 lightboxCounter.style.display = 'none';
260 }
261
262 if (!lightboxThumbsEnabled && lightboxThumbs) {
263 lightboxThumbs.style.display = 'none';
264 }
265 }
266
267 slides.forEach((slide, idx) => {
268 slide.addEventListener('click', () => {
269 if (!lightboxEnabled) return;
270 openLightbox(idx);
271 });
272 if (zoomEnabled) {
273 enableZoom(slide);
274 }
275 });
276
277 if (lbPrev) lbPrev.addEventListener('click', (e) => { e.stopPropagation(); goTo(-1); openLightbox(activeIndex); });
278 if (lbNext) lbNext.addEventListener('click', (e) => { e.stopPropagation(); goTo(1); openLightbox(activeIndex); });
279
280 // Global key handler is bound once for all instances below.
281
282 // Autoplay
283 const startAutoplay = () => {
284 if (!autoplay || slides.length <= 1) return;
285 stopAutoplay();
286 timer = setInterval(() => {
287 const nextIndex = loop ? (activeIndex + 1) % slides.length : Math.min(activeIndex + 1, slides.length - 1);
288 setActive(nextIndex);
289 }, speed > 500 ? speed : 4000);
290 };
291 const stopAutoplay = () => {
292 if (timer) {
293 clearInterval(timer);
294 timer = null;
295 }
296 };
297 wrapper.addEventListener('mouseenter', stopAutoplay);
298 wrapper.addEventListener('mouseleave', startAutoplay);
299
300 setActive(0);
301 startAutoplay();
302 };
303
304 const bindGlobalKeyupOnce = () => {
305 if (window[KEYUP_FLAG]) {
306 return;
307 }
308 window[KEYUP_FLAG] = true;
309
310 document.addEventListener("keyup", (e) => {
311 const active = window.kaWooGalleryActive;
312 if (!active || typeof active.isOpen !== "function" || !active.isOpen()) {
313 return;
314 }
315 if (e.key === "Escape") {
316 active.close();
317 }
318 if (e.key === "ArrowLeft") {
319 active.prev();
320 }
321 if (e.key === "ArrowRight") {
322 active.next();
323 }
324 });
325 };
326
327 const initAll = (root) => {
328 const ctx = root && root.querySelectorAll ? root : document;
329 bindGlobalKeyupOnce();
330 ctx.querySelectorAll(".ka-woo-gallery").forEach(initGallery);
331 };
332
333 document.addEventListener("DOMContentLoaded", () => initAll(document));
334
335 $(window).on("elementor/frontend/init", function () {
336 elementorFrontend.hooks.addAction(
337 "frontend/element_ready/woo_product_images_gallery.default",
338 function ($scope) {
339 initAll($scope && $scope[0] ? $scope[0] : document);
340 }
341 );
342 });
343 })(jQuery);
344
345
346
347
348
349
350