← back to Dw Theme Compact Toolbar

assets/dw-card-hover.js

167 lines

/* ============================================================================
   DW card label  v5  (2026-06-27) — collection / search product grids.
   v5 (Steve): the name/vendor label was a DARK absolute OVERLAY riding the
       bottom of the image (a "pill" on top of the thumbnail). It is now a
       SMALLER, light, in-flow pill placed DIRECTLY UNDER the image — no
       overlap, no dark gradient, always visible (better for trade shoppers
       scanning a dense grid). Smaller type + tighter padding than v4.
   v4: also hides the on-image quick-add / quick-view hover buttons (DTD B).

   The native Boost text band stays clipped (SEO/a11y in DOM). Works on the
   theme markup (.product-list-item*) AND Boost AI markup
   (.boost-sd__product-item), re-applying to lazy / infinite-scroll /
   re-rendered cards via a MutationObserver.

   Source asset: assets/dw-card-hover.js  (loaded by layout/theme.liquid).
   v1 backup: assets/dw-card-hover.js.v1-bak
   ========================================================================== */
(function () {
  if (window.__dwCardHover) return;
  window.__dwCardHover = true;

  var CARD   = '.product-list-item, .boost-sd__product-item';
  var TITLE  = '.product-list-item-title, .boost-sd__product-title';
  var VENDOR = '.product-list-item-vendor, .boost-sd__product-vendor';
  var THUMB  = '.product-list-item-thumbnail, .boost-sd__product-image-wrapper, [class*="boost-sd__product-image"]:not(img)';

  var STYLE_ID = 'dw-card-hover-style';
  if (!document.getElementById(STYLE_ID)) {
    var st = document.createElement('style');
    st.id = STYLE_ID;
    st.textContent = [
      /* image wrapper = positioning context for the overlay */
      '.product-list-item .product-list-item-thumbnail,',
      '.boost-sd__product-item .boost-sd__product-image-wrapper,',
      '.boost-sd__product-item [class*="boost-sd__product-image"]:not(img){position:relative;}',

      /* collapse the native text band to zero height. Boost keeps vendor/title/
         price as siblings, so each is clipped directly. Kept in DOM for SEO/a11y. */
      '.product-list-item .product-list-item-details,',
      '.boost-sd__product-item .boost-sd__product-info,',
      '.boost-sd__product-item .boost-sd__product-title,',
      '.boost-sd__product-item .boost-sd__product-vendor,',
      '.boost-sd__product-item [class*="boost-sd__product-price"],',
      '.boost-sd__product-item [class*="boost-sd__format-price"]{',
        'position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;',
        'overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;}',

      /* hide the on-image quick-add / quick-view hover buttons (DTD verdict B,
         2026-06-22): DW is trade/luxe — variant + MOQ selection happens on the
         PDP and the card image already links there, so these hover CTAs are
         redundant clutter against the pure image + name overlay. */
      '.boost-sd__product-item .boost-sd__button--show-on-hover,',
      '.boost-sd__product-item [class*="boost-sd__btn-quick"],',
      '.boost-sd__product-item [class*="boost-sd__product-action"]{display:none!important;}',

      /* === DW pill v5 (2026-06-27): the name/vendor label was an absolute dark
         overlay riding the bottom of the IMAGE. Per Steve it is now a COMPACT
         pill placed DIRECTLY UNDER the image, in normal flow (no overlap, no
         dark gradient). Smaller type + tighter padding than the v4 overlay. === */
      '.dw-hover-overlay{display:block;width:100%;box-sizing:border-box;z-index:2;',
        'padding:6px 8px 8px;text-align:center;pointer-events:none;',
        'font-family:-apple-system,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}',
      /* the compact pat pill: small, rounded, sits just below the image */
      '.dw-hover-overlay .pat{display:inline-block;max-width:100%;',
        'font-size:clamp(10px,0.78vw,12px);font-weight:600;line-height:1.2;',
        'color:#1a1a1a;letter-spacing:.01em;',
        'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.dw-hover-overlay .ven{font-size:9px;letter-spacing:.14em;text-transform:uppercase;',
        'color:#8a8278;margin-top:2px;line-height:1.2;}',
      '.dw-hover-overlay .ven::before{content:none;}',
      /* reduced motion: nothing to animate now (static pill) */
      '@media (prefers-reduced-motion:reduce){.dw-hover-overlay{transition:none;}}'
    ].join('');
    document.head.appendChild(st);
  }

  function esc(s) {
    return s.replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }

  // Boost renders titles lowercase; Title-Case for a luxe look. Small stop-words
  // (and/or/of/on/in/the/a/an/to/with) stay lowercase unless they lead the line.
  var STOP = { and:1, or:1, of:1, on:1, in:1, the:1, a:1, an:1, to:1, with:1 };
  function titleCase(s) {
    var i = 0;
    return s.replace(/\w[\w']*/g, function (w) {
      i++;
      var lower = w.toLowerCase();
      if (i > 1 && STOP[lower]) return lower;
      return w.charAt(0).toUpperCase() + w.slice(1);
    });
  }

  function patternName(title) {
    var t = title;
    if (t.indexOf('|') !== -1) t = t.split('|')[0];          // "Name | Vendor"
    else {                                                    // "Name By Designer"
      var by = t.split(/\s+by\s+/i);
      if (by.length > 1) t = by[0];
    }
    t = t.replace(/\s*wallcoverings?\b/ig, '')                // drop "wallcovering(s)"
         .replace(/[,\s]+$/, '').trim();
    return titleCase(t);
  }

  function labelCard(card) {
    if (card.querySelector('.dw-hover-overlay')) return;
    var tEl = card.querySelector(TITLE);
    if (!tEl) return;
    var name = patternName(tEl.textContent || '');
    if (!name) return;
    var vEl = card.querySelector(VENDOR);
    var vendor = (vEl ? vEl.textContent : '').replace(/\s+/g, ' ').trim();

    var imgWrap = card.querySelector(THUMB);
    if (!imgWrap) {
      var img = card.querySelector('img');
      imgWrap = img ? img.parentElement : null;
    }
    if (!imgWrap) return;

    var ov = document.createElement('div');
    ov.className = 'dw-hover-overlay';
    ov.innerHTML = '<div class="pat">' + esc(name) + '</div>' +
      (vendor ? '<div class="ven">' + esc(vendor) + '</div>' : '');

    /* v5: place the compact pill DIRECTLY UNDER the image, in normal flow.
       Insert it as a sibling right after the image-wrapper's nearest block
       container so it never overlaps the thumbnail. Walk up from the image
       wrapper to the card-level image column, then insert after that column. */
    var imageCol = imgWrap.closest(
      '.boost-sd__product-item-grid-view-layout-image, .product-list-item-thumbnail'
    ) || imgWrap;
    if (imageCol.parentNode) {
      imageCol.parentNode.insertBefore(ov, imageCol.nextSibling);
    } else {
      imgWrap.appendChild(ov);
    }
  }

  function run() {
    var cards = document.querySelectorAll(CARD);
    for (var i = 0; i < cards.length; i++) labelCard(cards[i]);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', run);
  } else {
    run();
  }

  // Re-apply when the grid re-renders or lazy/infinite scroll appends 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 });
})();