(function () {
'use strict';
const setGridVars = (grid) => {
grid.style.setProperty('--ka-grid-desktop', grid.dataset.colsDesktop || 4);
grid.style.setProperty('--ka-grid-tablet', grid.dataset.colsTablet || 3);
grid.style.setProperty('--ka-grid-mobile', grid.dataset.colsMobile || 2);
grid.style.setProperty('--ka-masonry-row', '8px');
grid.style.setProperty('--ka-masonry-gap', '16px');
};
const appendItems = (grid, html, replace = false) => {
const track = grid.querySelector('.ka-woo-products-grid__track') || grid;
const temp = document.createElement('div');
temp.innerHTML = html;
const items = temp.querySelectorAll('.ka-woo-products-grid__item');
if (replace) {
track.innerHTML = '';
}
items.forEach((item) => track.appendChild(item));
return items.length;
};
const reflowMasonry = (grid) => {
if (grid.dataset.layoutType !== 'masonry') return;
const styles = getComputedStyle(grid);
const rowHeight = parseFloat(styles.getPropertyValue('--ka-masonry-row')) || 8;
const gap = parseFloat(styles.getPropertyValue('--ka-masonry-gap')) || 16;
const items = grid.querySelectorAll('.ka-woo-products-grid__item');
items.forEach((item) => {
// The container's row gap is zero, so the spacing between cards has
// to come out of the span itself.
const span = Math.ceil((item.getBoundingClientRect().height + gap) / rowHeight);
item.style.gridRowEnd = `span ${span}`;
});
};
const setupMasonry = (grid) => {
if (grid.dataset.layoutType !== 'masonry') return;
if (typeof ResizeObserver === 'undefined') return;
const resizeObserver = new ResizeObserver(() => reflowMasonry(grid));
resizeObserver.observe(grid);
(grid.querySelector('.ka-woo-products-grid__track') || grid).querySelectorAll('img').forEach((img) => {
if (img.complete) {
reflowMasonry(grid);
} else {
img.addEventListener('load', () => reflowMasonry(grid));
}
});
reflowMasonry(grid);
};
const setupSlider = (grid) => {
if (grid.dataset.layoutType !== 'slider') return;
const parent = grid.parentElement;
if (!parent) return;
const track = grid.querySelector('.ka-woo-products-grid__track') || grid;
const existingNav = parent.querySelector('.ka-woo-products-grid__slider-nav');
if (existingNav) {
existingNav.remove();
}
const slides = Array.from(track.children);
if (!slides.length) return;
grid.classList.add('ka-woo-products-grid--slider');
slides.forEach((slide) => {
slide.classList.add('ka-woo-products-grid__slide');
});
let index = 0;
const loop = grid.dataset.sliderLoop === 'true';
const autoplay = grid.dataset.sliderAutoplay === 'true';
const speed = parseInt(grid.dataset.sliderAutoplaySpeed || '5000', 10);
const skin = grid.dataset.sliderSkin || 'arrows';
let timer;
let isDragging = false;
let startX = 0;
let currentX = 0;
const dots = [];
const update = () => {
const offset = index * 100;
track.style.transform = `translateX(-${offset}%)`;
dots.forEach((dot, idx) => {
dot.classList.toggle('is-active', idx === index);
});
};
const go = (dir) => {
const last = slides.length - 1;
let next = index + dir;
if (next > last) {
next = loop ? 0 : last;
} else if (next < 0) {
next = loop ? last : 0;
}
if (next === index && !loop) return;
index = next;
update();
};
const nav = document.createElement('div');
nav.className = 'ka-woo-products-grid__slider-nav';
const dotsWrap = document.createElement('div');
dotsWrap.className = 'ka-woo-products-grid__slider-dots';
slides.forEach((_, idx) => {
const dot = document.createElement('button');
dot.type = 'button';
dot.className = 'ka-woo-products-grid__slider-dot';
dot.addEventListener('click', () => {
index = idx;
update();
resetAutoplay();
});
dotsWrap.appendChild(dot);
dots.push(dot);
});
const controls = document.createElement('div');
controls.className = 'ka-woo-products-grid__slider-controls';
const prev = document.createElement('button');
prev.type = 'button';
prev.className = 'ka-woo-products-grid__slider-btn ka-woo-products-grid__slider-btn--prev';
prev.textContent = '‹';
prev.addEventListener('click', () => {
go(-1);
resetAutoplay();
});
const next = document.createElement('button');
next.type = 'button';
next.className = 'ka-woo-products-grid__slider-btn ka-woo-products-grid__slider-btn--next';
next.textContent = '›';
next.addEventListener('click', () => {
go(1);
resetAutoplay();
});
if (skin === 'arrows' || skin === 'both') {
controls.appendChild(prev);
controls.appendChild(next);
}
if (skin === 'dots' || skin === 'both') {
nav.appendChild(dotsWrap);
}
if (controls.children.length) {
nav.appendChild(controls);
}
parent.appendChild(nav);
const stopAutoplay = () => {
if (timer) {
window.clearInterval(timer);
timer = undefined;
}
};
const startAutoplay = () => {
if (!autoplay) return;
stopAutoplay();
timer = window.setInterval(() => go(1), speed);
};
const resetAutoplay = () => {
stopAutoplay();
startAutoplay();
};
grid.addEventListener('mouseenter', stopAutoplay);
grid.addEventListener('mouseleave', startAutoplay);
const getPointerX = (event) => (event.touches && event.touches.length ? event.touches[0].clientX : event.clientX);
const onPointerDown = (event) => {
isDragging = true;
startX = getPointerX(event);
currentX = startX;
track.style.transition = 'none';
event.preventDefault();
};
const onPointerMove = (event) => {
if (!isDragging) return;
currentX = getPointerX(event);
};
const onPointerUp = () => {
if (!isDragging) return;
const delta = currentX - startX;
track.style.transition = '';
if (Math.abs(delta) > 40) {
if (delta < 0) {
go(1);
} else {
go(-1);
}
resetAutoplay();
} else {
update();
}
isDragging = false;
};
track.addEventListener('mousedown', onPointerDown);
track.addEventListener('mousemove', onPointerMove);
track.addEventListener('mouseup', onPointerUp);
track.addEventListener('mouseleave', () => {
if (isDragging) {
onPointerUp();
}
});
track.addEventListener('touchstart', onPointerDown, { passive: true });
track.addEventListener('touchmove', onPointerMove, { passive: true });
track.addEventListener('touchend', onPointerUp);
track.addEventListener('touchcancel', onPointerUp);
update();
startAutoplay();
};
const buildFormData = (grid, page) => {
const boolVal = (value) => (value === 'true' || value === '1' ? '1' : '');
const form = new FormData();
form.append('action', 'ka_products_grid');
form.append('nonce', grid.dataset.nonce || '');
form.append('page', page);
form.append('per_page', grid.dataset.perPage || '8');
form.append('order', grid.dataset.order || 'DESC');
form.append('orderby', grid.dataset.orderby || 'date');
form.append('show_rating', boolVal(grid.dataset.showRating));
form.append('show_excerpt', boolVal(grid.dataset.showExcerpt));
form.append('excerpt_length', grid.dataset.excerptLength || '15');
form.append('show_badge', boolVal(grid.dataset.showBadge));
form.append('show_best_badge', boolVal(grid.dataset.showBestBadge));
form.append('show_custom_badge', boolVal(grid.dataset.showCustomBadge));
form.append('custom_badge_text', grid.dataset.customBadgeText || '');
form.append('show_brand', boolVal(grid.dataset.showBrand));
form.append('show_sku', boolVal(grid.dataset.showSku));
form.append('card_layout', grid.dataset.cardLayout || 'classic');
// Keep the listing scoped to the category or tag being browsed - the
// server cannot work that out from an admin-ajax request.
if (grid.dataset.archiveTaxonomy && grid.dataset.archiveTerm) {
form.append('archive_taxonomy', grid.dataset.archiveTaxonomy);
form.append('archive_term', grid.dataset.archiveTerm);
}
const filters = grid.dataset.filters ? grid.dataset.filters : '';
if (filters) {
form.append('filters', filters);
}
return form;
};
const fetchPage = (grid, page, replace = false) =>
fetch(grid.dataset.ajaxUrl, {
method: 'POST',
body: buildFormData(grid, page),
}).then((res) => res.json());
const bindLightbox = (grid, rebind = false) => {
if (!rebind && grid.dataset.lightboxBound === 'true') return;
grid.dataset.lightboxBound = 'true';
const overlayId = 'ka-woo-products-grid-lightbox';
let overlay = document.getElementById(overlayId);
if (!overlay) {
overlay = document.createElement('div');
overlay.id = overlayId;
overlay.className = 'ka-woo-products-grid__lightbox';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
}
const imgEl = overlay.querySelector('img');
const captionEl = overlay.querySelector('figcaption');
const closeBtn = overlay.querySelector('.ka-woo-products-grid__lightbox-close');
const close = () => {
overlay.classList.remove('is-open');
document.body.classList.remove('ka-woo-products-grid--lock');
};
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
close();
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
close();
}
});
grid.addEventListener('click', (e) => {
const link = e.target.closest('.ka-woo-products-grid__thumb');
if (!link || !grid.contains(link)) return;
const full = link.dataset.full || link.href;
if (!full) return;
e.preventDefault();
if (imgEl) {
imgEl.src = full;
}
if (captionEl) {
captionEl.textContent = link.dataset.caption || '';
}
overlay.classList.add('is-open');
document.body.classList.add('ka-woo-products-grid--lock');
});
};
const handleAjax = (grid) => {
// Sorting and filtering are wired up further down, and every grid needs
// them regardless of how it paginates. Bailing out here for "none" and
// "numbers" - the two default choices - meant the Sorting widget and
// the faceted filters silently did nothing on most grids.
const paginationType = grid.dataset.paginationType || 'none';
const isIncremental = 'load_more' === paginationType || 'infinite' === paginationType;
const parent = grid.parentElement;
if (!parent) return;
const btn = isIncremental ? parent.querySelector('.ka-woo-products-grid__load-more') : null;
let page = parseInt(grid.dataset.page || '1', 10);
const max = () => parseInt(grid.dataset.maxPages || '1', 10);
let sentinelObserver;
let sentinelEl;
let isProcessing = false;
const setButtonState = (state) => {
if (!btn) return;
const labelEl = btn.querySelector('.ka-woo-products-grid__load-more-label');
const spinner = btn.querySelector('.ka-woo-products-grid__spinner');
const defaultText = btn.dataset.defaultText || btn.textContent || '';
const loadingText = btn.dataset.loadingText || 'Loading…';
if (state === 'loading') {
btn.disabled = true;
if (labelEl) labelEl.textContent = loadingText;
spinner?.classList.add('is-visible');
btn.classList.add('is-loading');
} else if (state === 'ready') {
btn.disabled = false;
if (labelEl) labelEl.textContent = defaultText;
spinner?.classList.remove('is-visible');
btn.classList.remove('is-loading');
} else if (state === 'done') {
spinner?.classList.remove('is-visible');
btn.classList.remove('is-loading');
btn.remove();
}
};
const cleanupSentinel = () => {
if (sentinelObserver && sentinelEl) {
sentinelObserver.unobserve(sentinelEl);
}
if (sentinelEl) {
sentinelEl.remove();
}
};
const process = (replace = false) => {
if (isProcessing) return;
if (page >= max() && !replace) {
setButtonState('done');
cleanupSentinel();
return;
}
isProcessing = true;
setButtonState('loading');
fetchPage(grid, replace ? 1 : page + 1, replace)
.then((res) => {
if (!res || !res.success || !res.data) {
setButtonState('done');
cleanupSentinel();
return;
}
const added = appendItems(grid, res.data.html || '', replace);
const maxPages = res.data.max_pages ? parseInt(res.data.max_pages, 10) : max();
grid.dataset.maxPages = maxPages;
if (replace) {
page = 1;
grid.dataset.page = '1';
} else {
page += 1;
grid.dataset.page = page;
}
if (page >= maxPages || added === 0) {
setButtonState('done');
cleanupSentinel();
} else {
setButtonState('ready');
}
reflowMasonry(grid);
setupSlider(grid);
bindLightbox(grid, true);
})
.catch(() => {
setButtonState('ready');
})
.finally(() => {
isProcessing = false;
});
};
if (btn) {
btn.addEventListener('click', () => process(false));
}
if ('infinite' === paginationType && typeof IntersectionObserver !== 'undefined') {
sentinelEl = document.createElement('div');
sentinelEl.className = 'ka-woo-products-grid__sentinel';
parent.appendChild(sentinelEl);
sentinelObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
process(false);
}
});
},
{ rootMargin: '200px' }
);
sentinelObserver.observe(sentinelEl);
}
const applyFilters = (filters) => {
if (filters && typeof filters === 'object') {
grid.dataset.filters = JSON.stringify(filters);
} else {
grid.dataset.filters = '';
}
grid.dataset.page = '1';
process(true);
};
const applySorting = (orderby, order) => {
if (orderby) {
grid.dataset.orderby = orderby;
}
if (order) {
grid.dataset.order = order;
}
grid.dataset.page = '1';
process(true);
};
window.addEventListener('kaWooGridApplyFilters', (event) => {
const detail = event.detail || {};
if (detail.queryId && detail.queryId !== grid.dataset.queryId) {
return;
}
applyFilters(detail.filters || {});
});
document.addEventListener('kingaddons:filters:apply', (event) => {
const detail = event.detail || {};
if (!detail.queryId || detail.queryId !== grid.dataset.queryId) {
return;
}
applyFilters(detail.filters || {});
});
document.addEventListener('kingaddons:filters:reset', (event) => {
const detail = event.detail || {};
if (!detail.queryId || detail.queryId !== grid.dataset.queryId) {
return;
}
applyFilters({});
});
document.addEventListener('kingaddons:sorting:apply', (event) => {
const detail = event.detail || {};
if (detail.queryId && detail.queryId !== grid.dataset.queryId) {
return;
}
applySorting(detail.orderby, detail.order);
});
};
const initGrid = (grid) => {
if (grid.dataset.kngBound === 'true') return;
grid.dataset.kngBound = 'true';
setGridVars(grid);
setupMasonry(grid);
setupSlider(grid);
bindLightbox(grid);
handleAjax(grid);
};
const initAll = (context) => {
const root = context && context.querySelectorAll ? context : document;
root.querySelectorAll('.ka-woo-products-grid').forEach((grid) => initGrid(grid));
};
const observeNewGrids = () => {
if (!('MutationObserver' in window)) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (!(node instanceof HTMLElement)) return;
if (node.classList.contains('ka-woo-products-grid')) {
initGrid(node);
}
node.querySelectorAll?.('.ka-woo-products-grid').forEach((grid) => initGrid(grid));
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
};
document.addEventListener('DOMContentLoaded', () => {
initAll(document);
observeNewGrids();
});
const onElementorReady = () => {
if (!window.elementorFrontend || !window.elementorFrontend.hooks) {
return;
}
window.elementorFrontend.hooks.addAction('frontend/element_ready/woo_products_grid.default', (scope) => {
const root = scope && scope[0] ? scope[0] : scope;
initAll(root || document);
});
};
if (document.readyState === 'complete') {
onElementorReady();
} else {
window.addEventListener('DOMContentLoaded', onElementorReady);
}
})();