Skip to content
Choosing a selection results in a full page refresh.
Opens in a new window.
¡ª those keep /
pinned at
// `overflow:hidden`, so window.scrollY never changes and the icon would be
// stuck at its default color forever. `scrollEl` tracks whichever element
// actually receives scroll events; falls back to window when nothing else does.
var scrollEl = null;
function currentScrollTop() {
// Never trust scrollEl exclusively ¡ª if it was ever mis-detected (a
// transient element present only at page-load) its scrollTop stays stuck
// at 0 forever and would otherwise permanently override a perfectly good
// window.scrollY reading on themes where the window itself scrolls
// normally. Taking whichever is larger means a wrong guess can never
// suppress the real value, on any theme.
var winTop = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
var elTop = scrollEl ? scrollEl.scrollTop : 0;
return Math.max(winTop, elTop);
}
// Best-effort seed in case the page loads already scrolled inside a
// non-window container ¡ª the capture-phase listener below self-corrects
// for everything after the first real scroll, this just avoids a flash
// of the wrong color before that happens.
function detectScrollContainer() {
var all = document.body.querySelectorAll('*');
for (var i = 0; i < all.length; i++) {
var el = all[i];
var cs = getComputedStyle(el);
if (cs.overflowY !== 'auto' && cs.overflowY !== 'scroll') continue;
if (el.scrollHeight <= el.clientHeight + 50) continue;
if (el.clientHeight <= window.innerHeight * 0.5) continue;
// Reject hidden/off-canvas elements (e.g. a closed mobile nav submenu) ¡ª
// they can satisfy the size checks above while never actually being the
// thing the shopper scrolls, and unlike a mis-detected *visible* element
// they never self-correct via a real scroll event either.
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
var r = el.getBoundingClientRect();
if (r.right <= 0 || r.left >= window.innerWidth || r.bottom <= 0 || r.top >= window.innerHeight) continue;
return el;
}
return null;
}
function isHomePage() {
// Set server-side from Liquid's request.page_type ¡ª reliable regardless of
// locale-prefixed URLs (e.g. /en, /fr) where pathname/body-class checks fail.
if (root.dataset.isHomePage) return root.dataset.isHomePage === 'true';
if (window.Shopify && window.Shopify.template) return window.Shopify.template === 'index';
if (document.body.classList.contains('template-index')) return true;
var p = window.location.pathname;
return p === '/' || p === '';
}
function isMenuOpen() {
// Many themes force the header to a solid background while a nav
// dropdown/mega-menu is open (via CSS reacting to aria-expanded),
// independent of actual scroll position ¡ª without this check the icon
// is left in its transparent-header color, invisible against the
// now-solid backdrop.
return !!document.querySelector('header [aria-expanded="true"], nav [aria-expanded="true"], [role="navigation"] [aria-expanded="true"]');
}
function updateScrollState() {
// Non-home pages: only pin to the static otherPagesColor choice when the
// merchant restricted scroll-based switching to the home page.
if (scrollHomePageOnly && !isHomePage()) {
root.classList.toggle('wh-scrolled', otherPagesColor === 'scrolled');
return;
}
// Otherwise (home page, or homePageOnly is off): switch based on real scroll position.
var scrolled = currentScrollTop() > scrollThreshold || isMenuOpen();
root.classList.toggle('wh-scrolled', scrolled);
}
function handleScroll(e) {
// Scroll events don't bubble, but capturing on document catches them from
// any descendant scroll container too ¡ª that's how we learn which element
// (if any besides window) is the one actually scrolling on this theme.
// Requiring genuine vertical overflow (not just a numeric scrollTop) is
// essential here ¡ª horizontal-only widgets (image carousels, trust-badge
// strips, etc.) have scrollHeight === clientHeight but still fire real
// 'scroll' events as they auto-scroll sideways. Without this check any of
// them can hijack scrollEl away from the real page container and pin it
// at scrollTop 0 forever, permanently stranding the icon in its
// not-scrolled color no matter how far the page itself has scrolled.
var target = e && e.target;
if (target && target !== document && target !== window &&
target !== document.documentElement && target !== document.body &&
typeof target.scrollTop === 'number' &&
target.scrollHeight > target.clientHeight + 50) {
scrollEl = target;
}
updateScrollState();
}
if (scrollColorsEnabled) {
scrollEl = detectScrollContainer();
window.addEventListener('scroll', handleScroll, { passive: true });
document.addEventListener('scroll', handleScroll, { passive: true, capture: true });
updateScrollState();
// Re-check when any nav item's expanded state changes (mega-menu open/close)
new MutationObserver(updateScrollState).observe(document.body, {
attributes: true,
attributeFilter: ['aria-expanded'],
subtree: true,
});
}
// ©¤©¤ INIT ©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤©¤
// Launch mode is confirmed by Liquid server-side (wh_launch_mode == 'Header'),
// so no API fetch or async mode check is needed here.
function init() {
root.style.display = '';
fetchCount();
// True once the icon has been relocated into its header slot.
function isPlaced() {
var slot = root.closest('[data-wl-slot]');
return !!(slot && document.contains(root));
}
// Safe to call repeatedly ¡ª relocateIcon() no-ops once we're already placed.
function place() {
if (!isPlaced()) relocateIcon();
if (scrollColorsEnabled) updateScrollState();
}
// Fast attempts, plus later backstops. On slow devices/networks the theme
// header (e.g. Prestige's
custom element) may not be laid out
// until well after 2.5s. Without the later retries the icon is stranded at
// the bottom of and never appears in the header ¡ª which is why it
// showed up on some phones but not others.
function scheduleAttempts() {
place();
[400, 1000, 2500, 4000, 8000, 15000].forEach(function (ms) {
setTimeout(place, ms);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scheduleAttempts);
} else {
scheduleAttempts();
}
// Resilient watcher:
// ? re-inject if the theme removes our icon (SPA nav / header re-render), and
// ? keep hunting for the cart icon until we are actually placed ¡ª this is what
// makes the header icon appear reliably regardless of how slowly the device
// renders the header.
var debounce = null;
var stopHuntAt = Date.now() + 20000; // stop actively hunting after 20s
new MutationObserver(function () {
if (!document.contains(root)) {
// Icon was removed by a theme re-render ¡ª put it back.
clearTimeout(debounce);
debounce = setTimeout(place, 100);
} else if (!isPlaced() && Date.now() < stopHuntAt) {
// Still stranded (cart wasn't ready during the timed attempts) ¡ª retry as
// the header mutates its way into existence.
clearTimeout(debounce);
debounce = setTimeout(place, 120);
}
}).observe(document.body, { childList: true, subtree: true });
}
init();
})();