Have a question, need a quote, or want to check stock? Send us a message below. If you’re after a quote, photos, drawings or measurements can help us respond faster.
Get In Touch
We supply DIY customers, builders, glaziers and trade professionals. Get in touch for product advice, availability or trade pricing.
tags, keep them
* around this code.
*/
(function () {
var LIST_SELECTOR = 'ul.products';
var TITLE_SELECTOR = '.woocommerce-loop-product__title';
var TOLERANCE = 6; // Cards whose tops are within 6px count as one row.
var rafId = 0;
function equalize() {
rafId = 0;
var titles = [];
document.querySelectorAll(LIST_SELECTOR).forEach(function (list) {
if (list.offsetParent === null) return; // Hidden grid, e.g. in a closed tab.
list.querySelectorAll(':scope > li.product ' + TITLE_SELECTOR).forEach(function (title) {
titles.push(title);
});
});
if (!titles.length) return;
// 1. Clear the previous heights (writes only).
titles.forEach(function (title) {
title.style.minHeight = '';
});
// 2. Measure every title in one pass (reads only, so the page is laid out once).
var rows = [];
titles.forEach(function (title) {
var card = title.closest('li.product');
var top = Math.round(card.getBoundingClientRect().top);
var height = Math.ceil(title.getBoundingClientRect().height);
var row = null;
for (var i = 0; i < rows.length; i++) {
if (rows[i].list === card.parentNode && Math.abs(rows[i].top - top) <= TOLERANCE) {
row = rows[i];
break;
}
}
if (!row) {
row = { list: card.parentNode, top: top, max: 0, items: [] };
rows.push(row);
}
row.items.push(title);
if (height > row.max) row.max = height;
});
// 3. Apply the tallest height in each row (writes only).
rows.forEach(function (row) {
if (row.items.length < 2) return;
row.items.forEach(function (title) {
title.style.minHeight = row.max + 'px';
});
});
}
function schedule() {
if (!rafId) rafId = requestAnimationFrame(equalize);
}
function init() {
if (!document.querySelector(LIST_SELECTOR)) return; // No product grid on this page.
schedule();
window.addEventListener('load', schedule);
if (document.fonts) {
if (document.fonts.ready) document.fonts.ready.then(schedule);
if (document.fonts.addEventListener) document.fonts.addEventListener('loadingdone', schedule);
}
// Re-run when a grid changes width, not when its height changes.
var watched = new WeakSet();
var widths = new WeakMap();
var resizeObserver = null;
if ('ResizeObserver' in window) {
resizeObserver = new ResizeObserver(function (entries) {
var changed = false;
entries.forEach(function (entry) {
var width = Math.round(entry.contentRect.width);
if (widths.get(entry.target) !== width) {
widths.set(entry.target, width);
changed = true;
}
});
if (changed) schedule();
});
} else {
window.addEventListener('resize', schedule);
}
function watchLists() {
if (!resizeObserver) return;
document.querySelectorAll(LIST_SELECTOR).forEach(function (list) {
if (!watched.has(list)) {
watched.add(list);
resizeObserver.observe(list);
}
});
}
watchLists();
// Re-run only when products are added or removed (Load More, filters).
function touchesProducts(node) {
return node.nodeType === 1 && (node.matches('li.product, ' + LIST_SELECTOR) || !!node.querySelector('li.product, ' + LIST_SELECTOR));
}
var mutationObserver = new MutationObserver(function (records) {
for (var i = 0; i < records.length; i++) {
var lists = [records[i].addedNodes, records[i].removedNodes];
for (var k = 0; k < 2; k++) {
for (var j = 0; j < lists[k].length; j++) {
if (touchesProducts(lists[k][j])) {
watchLists();
schedule();
return;
}
}
}
}
});
var roots = document.querySelectorAll('.woocommerce, .elementor-widget-wc-archive-products, .elementor-widget-woocommerce-products');
(roots.length ? roots : [document.body]).forEach(function (root) {
mutationObserver.observe(root, { childList: true, subtree: true });
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();