// -------------------------------------------------
// Product grid rotator (collection: /cans)
// -------------------------------------------------
const ListingRotator = (() => {
const WRAP_SELECTOR = '.product-list-image-wrapper .product-list-item-image .grid-image-wrapper';
const ROTATE_MS = 2000;
const states = new Map();
let intervalId = null;
let observer = null;
const ensureSrc = (img) => {
if (!img.getAttribute('src') && img.dataset?.src) {
img.setAttribute('src', img.dataset.src);
}
img.style.display = '';
};
const collectImages = (wrapper) => {
const imgs = Array.from(wrapper.querySelectorAll('img'));
const usable = imgs.filter((img) => img.getAttribute('src') || img.dataset?.src);
return uniqueBy(usable, (img) => (img.getAttribute('src') || img.dataset?.src || '').trim());
};
const setActive = (imgs, idx) => {
imgs.forEach((img, i) => {
const active = i === idx;
img.classList.toggle('nbf-active', active);
img.style.opacity = active ? 1 : 0;
});
};
const inView = (el) => {
const rect = el.getBoundingClientRect();
const vh = win.innerHeight || doc.documentElement.clientHeight;
const vw = win.innerWidth || doc.documentElement.clientWidth;
return rect.bottom > 0 && rect.right > 0 && rect.top < vh && rect.left < vw;
};
const makeArrowButton = (dir, handler) => {
const btn = doc.createElement('button');
btn.type = 'button';
btn.className = `nbf-rotator-btn ${dir === 'prev' ? 'nbf-rotator-prev' : 'nbf-rotator-next'}`;
btn.setAttribute('aria-label', dir === 'prev' ? 'Previous image' : 'Next image');
btn.innerHTML =
dir === 'prev'
? ''
: '';
btn.addEventListener('click', handler);
return btn;
};
const attachArrows = (wrapper, state) => {
if (wrapper.querySelector('.nbf-rotator-ui')) return;
const ui = doc.createElement('div');
ui.className = 'nbf-rotator-ui';
const go = (delta) => {
state.idx = (state.idx + delta + state.imgs.length) % state.imgs.length;
setActive(state.imgs, state.idx);
};
const pauseTemporarily = () => {
state.paused = true;
setTimeout(() => {
state.paused = false;
}, 1000);
};
const prev = makeArrowButton('prev', (evt) => {
evt.preventDefault();
evt.stopPropagation();
pauseTemporarily();
go(-1);
});
const next = makeArrowButton('next', (evt) => {
evt.preventDefault();
evt.stopPropagation();
pauseTemporarily();
go(1);
});
ui.append(prev, next);
wrapper.appendChild(ui);
};
const setupWrapper = (wrapper) => {
if (wrapper.classList.contains('nbf-rotator-ready')) return;
const imgs = collectImages(wrapper);
if (imgs.length < 2) return;
imgs.forEach(ensureSrc);
wrapper.classList.add('nbf-rotator', 'nbf-rotator-ready');
const state = { idx: 0, imgs, paused: false };
states.set(wrapper, state);
setActive(imgs, 0);
wrapper.addEventListener('mouseenter', () => {
const st = states.get(wrapper);
if (st) st.paused = true;
});
wrapper.addEventListener('mouseleave', () => {
const st = states.get(wrapper);
if (st) st.paused = false;
});
attachArrows(wrapper, state);
};
const initAll = () => {
doc.querySelectorAll(WRAP_SELECTOR).forEach(setupWrapper);
};
const startTimer = () => {
if (intervalId) clearInterval(intervalId);
intervalId = win.setInterval(() => {
for (const [wrapper, state] of states) {
if (!doc.body.contains(wrapper)) {
states.delete(wrapper);
continue;
}
if (state.paused) continue;
if (!inView(wrapper)) continue;
state.idx = (state.idx + 1) % state.imgs.length;
setActive(state.imgs, state.idx);
}
}, ROTATE_MS);
};
const observeDom = () => {
if (observer) return;
observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes?.forEach((node) => {
if (!(node instanceof HTMLElement)) return;
if (node.matches?.(WRAP_SELECTOR)) setupWrapper(node);
node.querySelectorAll?.(WRAP_SELECTOR).forEach(setupWrapper);
});
});
});
observer.observe(doc.body, { childList: true, subtree: true });
};
const cleanup = () => {
for (const [wrapper] of states) {
if (!doc.body.contains(wrapper)) states.delete(wrapper);
}
};
const init = () => {
if (!location.pathname.endsWith('/cans')) return;
runAfterLoad(() => {
setTimeout(() => {
initAll();
startTimer();
observeDom();
win.setInterval(cleanup, 10000);
}, 1000);
});
};
return { init };
})();