← back to Designer Wallcoverings
pending-approval/infinite-scroll-hardening-2026-06-25/boost-sd-custom.PATCHED.js
328 lines
/*********************** Custom JS for Boost AI Search & Discovery ************************/
(function() {
'use strict';
// ── Vendor Handle Map (vendor name → collection handle) ────────────────
// Built from the brands page. Used to make vendor names clickable.
var VENDOR_HANDLES = {
'1838 Wallpapers': '1838-wallpapers',
'Andrew Martin': 'andrew-martin-wallpaper-collection',
'Anna French': 'anna-french',
'Anthology': 'anthology-wallcoverings',
'Armani/Casa': 'armani-casa-wallcoverings',
'Arte': 'arte',
'Borastapeter': 'borastapeter-scandinavian-wallpapers',
'Brewster': 'brewster-wallcoverings',
'Brunschwig & Fils': 'brunschwig-wallpapers',
'Carnegie': 'carnegie-wallcoverings',
'China Seas': 'china-seas-wallpaper',
'Christian Lacroix': 'christian-lacroix-wallcoverings',
'Clarke & Clarke': 'clarke-clarke',
'Cole & Son': 'cole-and-son',
'Colefax and Fowler': 'colefax-and-fowler',
'Cowtan & Tout': 'cowtan-tout-wallpaper-collection',
'de Gournay': 'de-gournay-wallcoverings',
'Designers Guild': 'designers-guild-wallcoverings',
'Donghia': 'donghia-wallcoverings',
'Elitis': 'elitis-wallcoverings',
'Fabricut': 'fabricut-wallcoverings',
'Farrow & Ball': 'farrow-ball-wallcoverings',
'Fornasetti': 'fornasetti',
'Franquemont London': 'franquemont-london',
'Fromental': 'fromental-wallcoverings',
'Gaston y Daniela': 'gaston-y-daniela',
'Graham & Brown': 'graham-and-brown-exclusive-wallpaper',
'Groundworks': 'groundworks-wallcoverings',
'Harlequin': 'harlequin-wallcoverings',
'Hygge & West': 'hygge-and-west',
'Innovations': 'innovations-usa',
'Koroseal': 'koroseal-wallpaper-collection',
'Kravet': 'kravet',
'Lee Jofa': 'lee-jofa',
'Maharam': 'maharam-wallcoverings',
'Matthew Williamson': 'matthew-williamson-wallcoverings',
'Maya Romanoff': 'maya-romanoff',
'MDC': 'mdc-wallcoverings',
'Milton & King': 'milton-and-king',
'Mind the Gap': 'mind-the-gap',
'Missoni Home': 'missoni-home',
'Mr Perswall': 'mr-perswall-wallcoverings',
'Mulberry': 'mulberry-wallcoverings',
'Nina Campbell': 'nina-campbell-wallcoverings',
'Nobilis': 'nobilis-wallcoverings',
'Osborne & Little': 'osborne-little-wallpapers',
'Phillip Jeffries': 'phillip-jeffries-wallcoverings',
'Pierre Frey': 'pierre-frey-wallcoverings',
'Philippe Romano Fabrics': 'philippe-romano-fabrics',
'Ralph Lauren': 'ralph-lauren-wallpaper',
'Rebel Walls': 'rebel-walls-murals',
'Roberto Cavalli': 'roberto-cavalli-wallpaper',
'Romo': 'romo-europe-wallpapaper-collections',
'Sanderson': 'sanderson-wallcoverings',
'Schumacher': 'schumacher-wallpaper',
'Stout Textiles': 'stout-wallcoverings',
'Stroheim': 'stroheim-wallcoverings',
'The Graduate Collection': 'the-graduate-collection',
'Thibaut': 'thibaut-wallcoverings',
'Threads': 'threads-wallcoverings',
'Versace': 'versace-wallpaper',
'Wolf Gordon': 'wolf-gordon-wallcoverings',
'York Wallcoverings': 'york-wallcoverings',
'Zoffany': 'zoffany-wallcoverings',
'Zuber': 'zuber-wallcoverings'
};
// Slugify vendor name as fallback if not in map
function vendorToHandle(vendor) {
if (VENDOR_HANDLES[vendor]) return VENDOR_HANDLES[vendor];
// Fallback: slugify
return vendor.toLowerCase()
.replace(/[&]/g, 'and')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
// ── Enhance Product Cards ─────────────────────────────────────────────
function enhanceProductCards() {
// Find vendor elements: <p> elements with class containing "vendor"
// inside the Boost filter block
var container = document.querySelector('.boost-sd__filter-block');
if (!container) return;
// Strategy: find all <p> elements whose className contains "vendor"
var allP = container.querySelectorAll('p[class*="vendor"]');
for (var i = 0; i < allP.length; i++) {
var vendorEl = allP[i];
// Skip if already enhanced
if (vendorEl.getAttribute('data-dw-linked')) continue;
vendorEl.setAttribute('data-dw-linked', '1');
var vendorName = vendorEl.textContent.trim();
if (!vendorName) continue;
var handle = vendorToHandle(vendorName);
var link = document.createElement('a');
link.href = '/collections/' + handle;
link.textContent = vendorName;
link.className = 'dw-vendor-link';
link.title = 'Browse all ' + vendorName;
link.addEventListener('click', function(e) {
e.stopPropagation(); // Don't trigger product card click
});
vendorEl.textContent = '';
vendorEl.appendChild(link);
}
// Add zoom-on-hover to product image wrappers that don't have a second image
var imgWrappers = container.querySelectorAll('[class*="product-image-wrapper"]');
for (var j = 0; j < imgWrappers.length; j++) {
var wrapper = imgWrappers[j];
if (wrapper.getAttribute('data-dw-hover')) continue;
wrapper.setAttribute('data-dw-hover', '1');
// If no second image, add zoom class
var hasSecond = wrapper.className.indexOf('has-second-image') > -1;
if (!hasSecond) {
wrapper.classList.add('dw-hover-zoom');
}
}
}
// ── Run on Boost render + MutationObserver ─────────────────────────────
// Boost renders async via React, so we watch for DOM changes
var observer = new MutationObserver(function(mutations) {
var shouldEnhance = false;
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length > 0) {
shouldEnhance = true;
break;
}
}
if (shouldEnhance) {
// Debounce slightly
clearTimeout(observer._timer);
observer._timer = setTimeout(enhanceProductCards, 150);
}
});
function startObserving() {
var target = document.querySelector('.boost-sd__filter-block');
if (target) {
observer.observe(target, { childList: true, subtree: true });
// Also run once immediately in case cards already rendered
enhanceProductCards();
} else {
// Boost hasn't loaded yet, wait
setTimeout(startObserving, 500);
}
}
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserving);
} else {
startObserving();
}
})();
/* Infinite Scroll belt-and-suspenders for Boost AI Collection/Search grids.
PRIMARY = snippets/boost-infinite-override.liquid (forces paginationType=
infinite_scroll). This fallback GUARANTEES the next page loads even when
Boost's native IntersectionObserver sentinel stalls — notably on MOBILE,
where a fling-to-bottom OVERSHOOTS the sentinel (it ends up just above the
fold) so Boost never fires and the grid froze after one page.
Hardened 2026-06-25: the reliable trigger is the sentinel
(.boost-sd__pagination-infinite-scroll-container-target) ENTERING the
viewport — Boost's empty container "button" is structural and clicking it
does nothing — so we bring the sentinel into view when the shopper is near
the bottom. A latch-guarded, count-aware pump stops at the true end of list
(and resumes the instant growth resumes) so it can neither strand mid-list
nor jitter past the end. */
(function () {
'use strict';
var loading = false, lastCount = -1, idle = 0, MAXIDLE = 8;
function count() { return document.querySelectorAll('.boost-sd__product-item').length; }
function sentinel() { return document.querySelector('.boost-sd__pagination-infinite-scroll-container-target'); }
function getNextButton() {
return document.querySelector('.boost-sd__load-more-button:not([disabled])') ||
document.querySelector('button[class*="load-more"]:not([disabled])') ||
document.querySelector('.boost-sd__pagination-button--next:not([disabled]):not(.boost-sd__pagination-button--disabled)') ||
document.querySelector('[class*="pagination"] a[rel="next"]');
}
// Trigger Boost's own loader. Primary = bring the sentinel into the viewport
// (handles the mobile fling overshoot). Fallback = click a real next button.
function pull() {
if (loading) return;
var s = sentinel();
if (s) {
loading = true;
try { s.scrollIntoView({ block: 'center' }); } catch (e) {}
setTimeout(function () { loading = false; }, 700);
return;
}
var b = getNextButton();
if (b) {
loading = true;
try { b.click(); } catch (e) {}
setTimeout(function () { loading = false; }, 700);
}
}
function nearBottom() {
return (document.documentElement.scrollHeight - (window.innerHeight + window.scrollY)) < 1500;
}
function onScroll() { if (nearBottom()) pull(); }
function init() {
var check = setInterval(function () {
if (document.querySelector('.boost-sd__product-list') ||
document.querySelector('.boost-sd__filter-block')) {
clearInterval(check);
window.addEventListener('scroll', onScroll, { passive: true });
// Hide ONLY the numbered pager, never Boost's own controls.
var st = document.createElement('style');
st.textContent =
'.boost-sd__pagination-list,' +
'.boost-sd__pagination-page-list,' +
'.boost-sd__pagination .boost-sd__pagination-item{display:none!important;}';
document.head.appendChild(st);
// Pump: only act when the shopper is near the bottom. Track product
// count; after MAXIDLE consecutive near-bottom ticks with no growth,
// assume end-of-list and stop pulling (resets the moment growth
// resumes), so it neither strands mid-list nor jitters at the end.
setInterval(function () {
if (!nearBottom()) { idle = 0; return; }
var c = count();
if (c !== lastCount) { lastCount = c; idle = 0; } else { idle++; }
if (idle <= MAXIDLE) pull();
}, 800);
}
}, 400);
setTimeout(function () { clearInterval(check); }, 15000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else { init(); }
})();
/* ============================================================================
DW card hover label (2026-06-22) — Boost product grid cards.
Default: clean image-only grid (full SEO title hidden).
On hover: reveal "Pattern, Color" (full title minus "wallcovering(s) | vendor")
+ the vendor on a smaller line. Full title stays in DOM for SEO.
Re-applies to infinite-scroll-appended / Boost-rerendered cards via observer.
========================================================================== */
(function () {
if (window.__dwCardHover) return;
window.__dwCardHover = true;
var STYLE_ID = 'dw-card-hover-style';
if (!document.getElementById(STYLE_ID)) {
var st = document.createElement('style');
st.id = STYLE_ID;
st.textContent =
'.boost-sd__product-item .boost-sd__product-title,' +
'.boost-sd__product-item .boost-sd__product-vendor{opacity:0;transition:opacity .15s;}' +
'.boost-sd__product-item:hover .dw-hover-label{opacity:1;}' +
'.dw-hover-label{opacity:0;transition:opacity .15s;padding:6px 2px 0;pointer-events:none;}' +
'.dw-hover-label .pat{font-size:13px;font-weight:600;color:#1a1a1a;line-height:1.2;}' +
'.dw-hover-label .ven{font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:#888;margin-top:2px;}';
document.head.appendChild(st);
}
function titleCase(s) {
return s.replace(/\w\S*/g, function (w) { return w.charAt(0).toUpperCase() + w.slice(1); });
}
function labelCard(card) {
if (card.querySelector('.dw-hover-label')) return;
var tEl = card.querySelector('.boost-sd__product-title');
var vEl = card.querySelector('.boost-sd__product-vendor');
if (!tEl) return;
var vendor = (vEl ? vEl.textContent : '').trim();
var name = tEl.textContent.split('|')[0].trim() // drop "| vendor"
.replace(/\s*wallcoverings?\b/ig, '') // drop "wallcovering(s)"
.replace(/[,\s]+$/, '').trim();
var lab = document.createElement('div');
lab.className = 'dw-hover-label';
lab.innerHTML = '<div class="pat">' + titleCase(name) + '</div>' +
(vendor ? '<div class="ven">' + titleCase(vendor) + '</div>' : '');
tEl.parentElement.appendChild(lab);
}
function run() {
var cards = document.querySelectorAll('.boost-sd__product-item');
for (var i = 0; i < cards.length; i++) labelCard(cards[i]);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run);
} else {
run();
}
// Re-apply when Boost renders a grid or infinite scroll appends more cards.
var t;
var obs = new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
if (muts[i].addedNodes && muts[i].addedNodes.length) {
clearTimeout(t);
t = setTimeout(run, 80);
return;
}
}
});
obs.observe(document.body, { childList: true, subtree: true });
})();