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_Products_Grid / 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_Products_Grid/script.js

570 lines 21.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function () {
2 'use strict';
3
4 const setGridVars = (grid) => {
5 grid.style.setProperty('--ka-grid-desktop', grid.dataset.colsDesktop || 4);
6 grid.style.setProperty('--ka-grid-tablet', grid.dataset.colsTablet || 3);
7 grid.style.setProperty('--ka-grid-mobile', grid.dataset.colsMobile || 2);
8 grid.style.setProperty('--ka-masonry-row', '8px');
9 grid.style.setProperty('--ka-masonry-gap', '16px');
10 };
11
12 const appendItems = (grid, html, replace = false) => {
13 const track = grid.querySelector('.ka-woo-products-grid__track') || grid;
14 const temp = document.createElement('div');
15 temp.innerHTML = html;
16 const items = temp.querySelectorAll('.ka-woo-products-grid__item');
17 if (replace) {
18 track.innerHTML = '';
19 }
20 items.forEach((item) => track.appendChild(item));
21 return items.length;
22 };
23
24 const reflowMasonry = (grid) => {
25 if (grid.dataset.layoutType !== 'masonry') return;
26 const styles = getComputedStyle(grid);
27 const rowHeight = parseFloat(styles.getPropertyValue('--ka-masonry-row')) || 8;
28 const gap = parseFloat(styles.getPropertyValue('--ka-masonry-gap')) || 16;
29 const items = grid.querySelectorAll('.ka-woo-products-grid__item');
30 items.forEach((item) => {
31 // The container's row gap is zero, so the spacing between cards has
32 // to come out of the span itself.
33 const span = Math.ceil((item.getBoundingClientRect().height + gap) / rowHeight);
34 item.style.gridRowEnd = `span ${span}`;
35 });
36 };
37
38 const setupMasonry = (grid) => {
39 if (grid.dataset.layoutType !== 'masonry') return;
40 if (typeof ResizeObserver === 'undefined') return;
41 const resizeObserver = new ResizeObserver(() => reflowMasonry(grid));
42 resizeObserver.observe(grid);
43 (grid.querySelector('.ka-woo-products-grid__track') || grid).querySelectorAll('img').forEach((img) => {
44 if (img.complete) {
45 reflowMasonry(grid);
46 } else {
47 img.addEventListener('load', () => reflowMasonry(grid));
48 }
49 });
50 reflowMasonry(grid);
51 };
52
53 const setupSlider = (grid) => {
54 if (grid.dataset.layoutType !== 'slider') return;
55 const parent = grid.parentElement;
56 if (!parent) return;
57 const track = grid.querySelector('.ka-woo-products-grid__track') || grid;
58 const existingNav = parent.querySelector('.ka-woo-products-grid__slider-nav');
59 if (existingNav) {
60 existingNav.remove();
61 }
62 const slides = Array.from(track.children);
63 if (!slides.length) return;
64
65 grid.classList.add('ka-woo-products-grid--slider');
66 slides.forEach((slide) => {
67 slide.classList.add('ka-woo-products-grid__slide');
68 });
69
70 let index = 0;
71 const loop = grid.dataset.sliderLoop === 'true';
72 const autoplay = grid.dataset.sliderAutoplay === 'true';
73 const speed = parseInt(grid.dataset.sliderAutoplaySpeed || '5000', 10);
74 const skin = grid.dataset.sliderSkin || 'arrows';
75 let timer;
76 let isDragging = false;
77 let startX = 0;
78 let currentX = 0;
79
80 const dots = [];
81
82 const update = () => {
83 const offset = index * 100;
84 track.style.transform = `translateX(-${offset}%)`;
85 dots.forEach((dot, idx) => {
86 dot.classList.toggle('is-active', idx === index);
87 });
88 };
89
90 const go = (dir) => {
91 const last = slides.length - 1;
92 let next = index + dir;
93 if (next > last) {
94 next = loop ? 0 : last;
95 } else if (next < 0) {
96 next = loop ? last : 0;
97 }
98 if (next === index && !loop) return;
99 index = next;
100 update();
101 };
102
103 const nav = document.createElement('div');
104 nav.className = 'ka-woo-products-grid__slider-nav';
105 const dotsWrap = document.createElement('div');
106 dotsWrap.className = 'ka-woo-products-grid__slider-dots';
107 slides.forEach((_, idx) => {
108 const dot = document.createElement('button');
109 dot.type = 'button';
110 dot.className = 'ka-woo-products-grid__slider-dot';
111 dot.addEventListener('click', () => {
112 index = idx;
113 update();
114 resetAutoplay();
115 });
116 dotsWrap.appendChild(dot);
117 dots.push(dot);
118 });
119
120 const controls = document.createElement('div');
121 controls.className = 'ka-woo-products-grid__slider-controls';
122 const prev = document.createElement('button');
123 prev.type = 'button';
124 prev.className = 'ka-woo-products-grid__slider-btn ka-woo-products-grid__slider-btn--prev';
125 prev.textContent = '‹';
126 prev.addEventListener('click', () => {
127 go(-1);
128 resetAutoplay();
129 });
130 const next = document.createElement('button');
131 next.type = 'button';
132 next.className = 'ka-woo-products-grid__slider-btn ka-woo-products-grid__slider-btn--next';
133 next.textContent = '›';
134 next.addEventListener('click', () => {
135 go(1);
136 resetAutoplay();
137 });
138
139 if (skin === 'arrows' || skin === 'both') {
140 controls.appendChild(prev);
141 controls.appendChild(next);
142 }
143 if (skin === 'dots' || skin === 'both') {
144 nav.appendChild(dotsWrap);
145 }
146 if (controls.children.length) {
147 nav.appendChild(controls);
148 }
149 parent.appendChild(nav);
150
151 const stopAutoplay = () => {
152 if (timer) {
153 window.clearInterval(timer);
154 timer = undefined;
155 }
156 };
157 const startAutoplay = () => {
158 if (!autoplay) return;
159 stopAutoplay();
160 timer = window.setInterval(() => go(1), speed);
161 };
162 const resetAutoplay = () => {
163 stopAutoplay();
164 startAutoplay();
165 };
166
167 grid.addEventListener('mouseenter', stopAutoplay);
168 grid.addEventListener('mouseleave', startAutoplay);
169
170 const getPointerX = (event) => (event.touches && event.touches.length ? event.touches[0].clientX : event.clientX);
171
172 const onPointerDown = (event) => {
173 isDragging = true;
174 startX = getPointerX(event);
175 currentX = startX;
176 track.style.transition = 'none';
177 event.preventDefault();
178 };
179
180 const onPointerMove = (event) => {
181 if (!isDragging) return;
182 currentX = getPointerX(event);
183 };
184
185 const onPointerUp = () => {
186 if (!isDragging) return;
187 const delta = currentX - startX;
188 track.style.transition = '';
189 if (Math.abs(delta) > 40) {
190 if (delta < 0) {
191 go(1);
192 } else {
193 go(-1);
194 }
195 resetAutoplay();
196 } else {
197 update();
198 }
199 isDragging = false;
200 };
201
202 track.addEventListener('mousedown', onPointerDown);
203 track.addEventListener('mousemove', onPointerMove);
204 track.addEventListener('mouseup', onPointerUp);
205 track.addEventListener('mouseleave', () => {
206 if (isDragging) {
207 onPointerUp();
208 }
209 });
210 track.addEventListener('touchstart', onPointerDown, { passive: true });
211 track.addEventListener('touchmove', onPointerMove, { passive: true });
212 track.addEventListener('touchend', onPointerUp);
213 track.addEventListener('touchcancel', onPointerUp);
214
215 update();
216 startAutoplay();
217 };
218
219 const buildFormData = (grid, page) => {
220 const boolVal = (value) => (value === 'true' || value === '1' ? '1' : '');
221 const form = new FormData();
222 form.append('action', 'ka_products_grid');
223 form.append('nonce', grid.dataset.nonce || '');
224 form.append('page', page);
225 form.append('per_page', grid.dataset.perPage || '8');
226 form.append('order', grid.dataset.order || 'DESC');
227 form.append('orderby', grid.dataset.orderby || 'date');
228 form.append('show_rating', boolVal(grid.dataset.showRating));
229 form.append('show_excerpt', boolVal(grid.dataset.showExcerpt));
230 form.append('excerpt_length', grid.dataset.excerptLength || '15');
231 form.append('show_badge', boolVal(grid.dataset.showBadge));
232 form.append('show_best_badge', boolVal(grid.dataset.showBestBadge));
233 form.append('show_custom_badge', boolVal(grid.dataset.showCustomBadge));
234 form.append('custom_badge_text', grid.dataset.customBadgeText || '');
235 form.append('show_brand', boolVal(grid.dataset.showBrand));
236 form.append('show_sku', boolVal(grid.dataset.showSku));
237 form.append('card_layout', grid.dataset.cardLayout || 'classic');
238 // Keep the listing scoped to the category or tag being browsed - the
239 // server cannot work that out from an admin-ajax request.
240 if (grid.dataset.archiveTaxonomy && grid.dataset.archiveTerm) {
241 form.append('archive_taxonomy', grid.dataset.archiveTaxonomy);
242 form.append('archive_term', grid.dataset.archiveTerm);
243 }
244
245 const filters = grid.dataset.filters ? grid.dataset.filters : '';
246 if (filters) {
247 form.append('filters', filters);
248 }
249 return form;
250 };
251
252 const fetchPage = (grid, page, replace = false) =>
253 fetch(grid.dataset.ajaxUrl, {
254 method: 'POST',
255 body: buildFormData(grid, page),
256 }).then((res) => res.json());
257
258 const syncRelatedWidgets = (grid, state) => {
259 const queryId = grid.dataset.queryId || '';
260 const pageArg = grid.dataset.pageArg || '';
261 const page = Math.max(1, parseInt(state.page, 10) || 1);
262
263 if (pageArg && window.history && window.history.replaceState) {
264 const url = new URL(window.location.href);
265 if (page <= 1) {
266 url.searchParams.delete(pageArg);
267 } else {
268 url.searchParams.set(pageArg, String(page));
269 }
270 window.history.replaceState({}, '', url);
271 }
272
273 document.dispatchEvent(new CustomEvent('kingaddons:grid:page', {
274 detail: {
275 queryId,
276 pageArg,
277 page,
278 maxPages: Math.max(1, parseInt(state.maxPages, 10) || 1),
279 foundPosts: Math.max(0, parseInt(state.foundPosts, 10) || 0),
280 perPage: Math.max(1, parseInt(state.perPage, 10) || 1),
281 },
282 }));
283 };
284
285 const bindLightbox = (grid, rebind = false) => {
286 if (!rebind && grid.dataset.lightboxBound === 'true') return;
287 grid.dataset.lightboxBound = 'true';
288 const overlayId = 'ka-woo-products-grid-lightbox';
289 let overlay = document.getElementById(overlayId);
290 if (!overlay) {
291 overlay = document.createElement('div');
292 overlay.id = overlayId;
293 overlay.className = 'ka-woo-products-grid__lightbox';
294 overlay.innerHTML = `
295 <button class="ka-woo-products-grid__lightbox-close" type="button" aria-label="Close">×</button>
296 <figure class="ka-woo-products-grid__lightbox-figure">
297 <img src="" alt="" />
298 <figcaption></figcaption>
299 </figure>
300 `;
301 document.body.appendChild(overlay);
302 }
303 const imgEl = overlay.querySelector('img');
304 const captionEl = overlay.querySelector('figcaption');
305 const closeBtn = overlay.querySelector('.ka-woo-products-grid__lightbox-close');
306
307 const close = () => {
308 overlay.classList.remove('is-open');
309 document.body.classList.remove('ka-woo-products-grid--lock');
310 };
311
312 closeBtn.addEventListener('click', close);
313 overlay.addEventListener('click', (e) => {
314 if (e.target === overlay) {
315 close();
316 }
317 });
318 document.addEventListener('keyup', (e) => {
319 if (e.key === 'Escape') {
320 close();
321 }
322 });
323
324 grid.addEventListener('click', (e) => {
325 const link = e.target.closest('.ka-woo-products-grid__thumb');
326 if (!link || !grid.contains(link)) return;
327 const full = link.dataset.full || link.href;
328 if (!full) return;
329 e.preventDefault();
330 if (imgEl) {
331 imgEl.src = full;
332 }
333 if (captionEl) {
334 captionEl.textContent = link.dataset.caption || '';
335 }
336 overlay.classList.add('is-open');
337 document.body.classList.add('ka-woo-products-grid--lock');
338 });
339 };
340
341 const handleAjax = (grid) => {
342 // Sorting and filtering are wired up further down, and every grid needs
343 // them regardless of how it paginates. Bailing out here for "none" and
344 // "numbers" - the two default choices - meant the Sorting widget and
345 // the faceted filters silently did nothing on most grids.
346 const paginationType = grid.dataset.paginationType || 'none';
347 const isIncremental = 'load_more' === paginationType || 'infinite' === paginationType;
348
349 const parent = grid.parentElement;
350 if (!parent) return;
351 const btn = isIncremental ? parent.querySelector('.ka-woo-products-grid__load-more') : null;
352 let page = parseInt(grid.dataset.page || '1', 10);
353 const max = () => parseInt(grid.dataset.maxPages || '1', 10);
354 let sentinelObserver;
355 let sentinelEl;
356 let isProcessing = false;
357
358 const setButtonState = (state) => {
359 if (!btn) return;
360 const labelEl = btn.querySelector('.ka-woo-products-grid__load-more-label');
361 const spinner = btn.querySelector('.ka-woo-products-grid__spinner');
362 const defaultText = btn.dataset.defaultText || btn.textContent || '';
363 const loadingText = btn.dataset.loadingText || 'Loading…';
364 if (state === 'loading') {
365 btn.disabled = true;
366 if (labelEl) labelEl.textContent = loadingText;
367 spinner?.classList.add('is-visible');
368 btn.classList.add('is-loading');
369 } else if (state === 'ready') {
370 btn.disabled = false;
371 if (labelEl) labelEl.textContent = defaultText;
372 spinner?.classList.remove('is-visible');
373 btn.classList.remove('is-loading');
374 } else if (state === 'done') {
375 spinner?.classList.remove('is-visible');
376 btn.classList.remove('is-loading');
377 btn.remove();
378 }
379 };
380
381 const cleanupSentinel = () => {
382 if (sentinelObserver && sentinelEl) {
383 sentinelObserver.unobserve(sentinelEl);
384 }
385 if (sentinelEl) {
386 sentinelEl.remove();
387 }
388 };
389
390 const process = (replace = false) => {
391 if (isProcessing) return;
392 if (page >= max() && !replace) {
393 setButtonState('done');
394 cleanupSentinel();
395 return;
396 }
397 isProcessing = true;
398 setButtonState('loading');
399 fetchPage(grid, replace ? 1 : page + 1, replace)
400 .then((res) => {
401 if (!res || !res.success || !res.data) {
402 setButtonState('done');
403 cleanupSentinel();
404 return;
405 }
406 const added = appendItems(grid, res.data.html || '', replace);
407 const maxPages = res.data.max_pages ? parseInt(res.data.max_pages, 10) : max();
408 grid.dataset.maxPages = maxPages;
409 if (replace) {
410 page = 1;
411 grid.dataset.page = '1';
412 syncRelatedWidgets(grid, {
413 page,
414 maxPages,
415 foundPosts: parseInt(res.data.found_posts, 10) || 0,
416 perPage: parseInt(res.data.per_page || grid.dataset.perPage || '8', 10),
417 });
418 } else {
419 page += 1;
420 grid.dataset.page = page;
421 }
422 if (page >= maxPages || added === 0) {
423 setButtonState('done');
424 cleanupSentinel();
425 } else {
426 setButtonState('ready');
427 }
428 reflowMasonry(grid);
429 setupSlider(grid);
430 bindLightbox(grid, true);
431 })
432 .catch(() => {
433 setButtonState('ready');
434 })
435 .finally(() => {
436 isProcessing = false;
437 });
438 };
439
440 if (btn) {
441 btn.addEventListener('click', () => process(false));
442 }
443
444 if ('infinite' === paginationType && typeof IntersectionObserver !== 'undefined') {
445 sentinelEl = document.createElement('div');
446 sentinelEl.className = 'ka-woo-products-grid__sentinel';
447 parent.appendChild(sentinelEl);
448 sentinelObserver = new IntersectionObserver(
449 (entries) => {
450 entries.forEach((entry) => {
451 if (entry.isIntersecting) {
452 process(false);
453 }
454 });
455 },
456 { rootMargin: '200px' }
457 );
458 sentinelObserver.observe(sentinelEl);
459 }
460
461 const applyFilters = (filters) => {
462 if (filters && typeof filters === 'object') {
463 grid.dataset.filters = JSON.stringify(filters);
464 } else {
465 grid.dataset.filters = '';
466 }
467 grid.dataset.page = '1';
468 process(true);
469 };
470
471 const applySorting = (orderby, order) => {
472 if (orderby) {
473 grid.dataset.orderby = orderby;
474 }
475 if (order) {
476 grid.dataset.order = order;
477 }
478 grid.dataset.page = '1';
479 process(true);
480 };
481
482 window.addEventListener('kaWooGridApplyFilters', (event) => {
483 const detail = event.detail || {};
484 if (detail.queryId && detail.queryId !== grid.dataset.queryId) {
485 return;
486 }
487 applyFilters(detail.filters || {});
488 });
489
490 document.addEventListener('kingaddons:filters:apply', (event) => {
491 const detail = event.detail || {};
492 if (!detail.queryId || detail.queryId !== grid.dataset.queryId) {
493 return;
494 }
495 applyFilters(detail.filters || {});
496 });
497
498 document.addEventListener('kingaddons:filters:reset', (event) => {
499 const detail = event.detail || {};
500 if (!detail.queryId || detail.queryId !== grid.dataset.queryId) {
501 return;
502 }
503 applyFilters({});
504 });
505
506 document.addEventListener('kingaddons:sorting:apply', (event) => {
507 const detail = event.detail || {};
508 if (detail.queryId && detail.queryId !== grid.dataset.queryId) {
509 return;
510 }
511 applySorting(detail.orderby, detail.order);
512 });
513 };
514
515 const initGrid = (grid) => {
516 if (grid.dataset.kngBound === 'true') return;
517 grid.dataset.kngBound = 'true';
518 setGridVars(grid);
519 setupMasonry(grid);
520 setupSlider(grid);
521 bindLightbox(grid);
522 handleAjax(grid);
523 };
524
525 const initAll = (context) => {
526 const root = context && context.querySelectorAll ? context : document;
527 root.querySelectorAll('.ka-woo-products-grid').forEach((grid) => initGrid(grid));
528 };
529
530 const observeNewGrids = () => {
531 if (!('MutationObserver' in window)) return;
532 const observer = new MutationObserver((mutations) => {
533 mutations.forEach((mutation) => {
534 mutation.addedNodes.forEach((node) => {
535 if (!(node instanceof HTMLElement)) return;
536 if (node.classList.contains('ka-woo-products-grid')) {
537 initGrid(node);
538 }
539 node.querySelectorAll?.('.ka-woo-products-grid').forEach((grid) => initGrid(grid));
540 });
541 });
542 });
543 observer.observe(document.body, { childList: true, subtree: true });
544 };
545
546 document.addEventListener('DOMContentLoaded', () => {
547 initAll(document);
548 observeNewGrids();
549 });
550
551 const onElementorReady = () => {
552 if (!window.elementorFrontend || !window.elementorFrontend.hooks) {
553 return;
554 }
555 window.elementorFrontend.hooks.addAction('frontend/element_ready/woo_products_grid.default', (scope) => {
556 const root = scope && scope[0] ? scope[0] : scope;
557 initAll(root || document);
558 });
559 };
560
561 if (document.readyState === 'complete') {
562 onElementorReady();
563 } else {
564 window.addEventListener('DOMContentLoaded', onElementorReady);
565 }
566 })();
567
568
569
570