← back to Dw Theme Boost Fix

assets/dw-filter-horizontal.js

170 lines

/* ============================================================================
   DW Horizontal Filter Bar — JS
   Turns the Boost AI Search & Discovery vertical filter sidebar into a
   horizontal, collapsible filter bar across the top of the product grid.

   What it does (theme-side only, no Boost-dashboard change):
     1. Waits for Boost's React-rendered .boost-sd__filter-tree to exist.
     2. Adds body.dw-filters-horizontal so dw-filter-horizontal.css applies the
        horizontal layout (each group becomes a chip with a dropdown panel).
     3. Each group title (.boost-sd__filter-title) toggles a `.dw-open` class on
        its .boost-sd__filter so only one dropdown shows at a time. Clicks on the
        actual option items still bubble to Boost, so filtering still works.
     4. Injects a single "Filters" pill into the Boost toolbar that collapses /
        expands the whole bar; collapsed state persists in localStorage.

   Replaces the old `dw-filters-collapsed` (hide-entirely) behavior on
   collection/search pages. Only runs where a Boost filter tree is present.
   ============================================================================ */
(function () {
  'use strict';

  var BODY = document.body;
  var LS_KEY = 'dw-hbar-collapsed';
  var wired = false;

  function isGridPage() {
    return /\/(collections|search)/.test(window.location.pathname);
  }

  function qs(sel, root) { return (root || document).querySelector(sel); }

  /* Toggle a single filter group open/closed (accordion: one at a time). */
  function bindGroup(filterEl) {
    if (filterEl.getAttribute('data-dw-hbar')) return;
    filterEl.setAttribute('data-dw-hbar', '1');

    var title = qs('.boost-sd__filter-title', filterEl);
    if (!title) return;

    title.addEventListener('click', function (e) {
      // Only handle clicks on the title chip itself, not on option rows that
      // Boost may have nested — let those reach Boost.
      if (e.target.closest('.boost-sd__filter-option') ||
          e.target.closest('.boost-sd__filter-options')) {
        return;
      }
      var alreadyOpen = filterEl.classList.contains('dw-open');
      // Close all siblings first
      var tree = filterEl.closest('.boost-sd__filter-tree');
      if (tree) {
        tree.querySelectorAll('.boost-sd__filter.dw-open').forEach(function (f) {
          if (f !== filterEl) f.classList.remove('dw-open');
        });
      }
      filterEl.classList.toggle('dw-open', !alreadyOpen);
    });
  }

  function bindAllGroups() {
    var groups = document.querySelectorAll('.boost-sd__filter-tree .boost-sd__filter');
    for (var i = 0; i < groups.length; i++) bindGroup(groups[i]);
  }

  /* Close any open dropdown when clicking outside the bar. */
  function bindOutsideClose() {
    document.addEventListener('click', function (e) {
      if (e.target.closest('.boost-sd__filter-tree')) return;
      document.querySelectorAll('.boost-sd__filter.dw-open').forEach(function (f) {
        f.classList.remove('dw-open');
      });
    });
  }

  /* Build the collapse/expand toggle pill and drop it in the Boost toolbar. */
  function buildToggle() {
    if (qs('#dw-hbar-toggle')) return;
    var toolbar = qs('.boost-sd__toolbar-item--product-count, .boost-sd__product-count');
    if (!toolbar || !toolbar.parentElement) return;

    var btn = document.createElement('button');
    btn.id = 'dw-hbar-toggle';
    btn.className = 'dw-hbar-toggle';
    btn.type = 'button';
    btn.setAttribute('aria-label', 'Toggle filter bar');

    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('stroke', 'currentColor');
    svg.setAttribute('stroke-width', '2');
    var ys = ['6', '12', '18'];
    for (var i = 0; i < ys.length; i++) {
      var l = document.createElementNS('http://www.w3.org/2000/svg', 'line');
      l.setAttribute('x1', '4'); l.setAttribute('y1', ys[i]);
      l.setAttribute('x2', '20'); l.setAttribute('y2', ys[i]);
      svg.appendChild(l);
    }
    btn.appendChild(svg);

    var label = document.createElement('span');
    label.id = 'dw-hbar-label';
    btn.appendChild(label);

    function syncLabel() {
      var collapsed = BODY.classList.contains('dw-hbar-collapsed');
      label.textContent = collapsed ? 'Filters' : 'Hide Filters';
    }

    btn.addEventListener('click', function () {
      var collapsed = BODY.classList.toggle('dw-hbar-collapsed');
      try { localStorage.setItem(LS_KEY, collapsed ? '1' : '0'); } catch (e) {}
      syncLabel();
    });

    toolbar.parentElement.insertBefore(btn, toolbar);
    syncLabel();
  }

  /* Apply the persisted collapsed state. Default: bar EXPANDED (visible). */
  function applyPersistedState() {
    var collapsed = false;
    try { collapsed = localStorage.getItem(LS_KEY) === '1'; } catch (e) {}
    BODY.classList.toggle('dw-hbar-collapsed', collapsed);
  }

  function wire() {
    if (wired) return true;
    var tree = qs('.boost-sd__filter-tree');
    if (!tree) return false;
    wired = true;

    // Remove the legacy hide-entirely behavior so the two don't fight.
    BODY.classList.remove('dw-filters-collapsed');
    BODY.classList.add('dw-filters-horizontal');

    applyPersistedState();
    bindAllGroups();
    bindOutsideClose();
    buildToggle();

    // Boost re-renders the tree on filter apply / page change — re-bind groups
    // and keep our toggle present.
    var obs = new MutationObserver(function () {
      clearTimeout(obs._t);
      obs._t = setTimeout(function () {
        bindAllGroups();
        buildToggle();
      }, 150);
    });
    var wrap = qs('.boost-sd__filter-tree-wrapper') || tree.parentElement || document.body;
    obs.observe(wrap, { childList: true, subtree: true });

    return true;
  }

  function start() {
    if (!isGridPage()) return;
    var tries = 0;
    var poll = setInterval(function () {
      if (wire() || tries++ > 80) clearInterval(poll);
    }, 250);
  }

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