← back to Designer Wallcoverings

pending-approval/toolbar-consolidation-assets/snippets__collection-toolbar.liquid

331 lines

{%- comment -%}
  Collection Toolbar — Horizontal sort + grid density slider + search
  Replaces Shopify's native sorting/filtering UI with a unified toolbar.

  Features:
  - Sort dropdown (Newest, Color, Style, SKU, Title, Price)
  - Grid density slider (3–12 columns)
  - localStorage persistence
  - Responsive mobile stacking

  Revision: 2026-06-22
{%- endcomment -%}

<div class="collection-toolbar">
  <style>
    .collection-toolbar {
      display: flex;
      gap: 16px;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      padding: 20px 0;
      border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1));
      margin-bottom: 24px;
    }

    .toolbar-left {
      display: flex;
      gap: 14px;
      align-items: center;
      flex-wrap: nowrap;
      min-width: 0;
    }

    .toolbar-label {
      font-size: 11px;
      letter-spacing: 0.22em;
      text-transform: uppercase;
      font-weight: 600;
      color: var(--text-muted, rgba(0,0,0,0.6));
    }

    #collectionSort {
      background: transparent;
      border: 1px solid var(--border-color, rgba(0,0,0,0.12));
      padding: 6px 10px;
      font-family: inherit;
      font-size: 12px;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      font-weight: 500;
      color: var(--text-color, inherit);
      cursor: pointer;
      transition: all 0.2s ease;
      min-width: 140px;
      max-width: 100%;
      overflow: visible;
    }

    #collectionSort:hover {
      border-color: var(--accent-color, rgba(0,0,0,0.3));
    }

    #collectionSort:focus {
      outline: 2px solid var(--accent-color, #000);
      outline-offset: 2px;
    }

    .density-control {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 6px 0;
    }

    .density-control input[type="range"] {
      flex: 0 0 120px;
      -webkit-appearance: none;
      appearance: none;
      width: 120px;
      height: 4px;
      background: var(--border-color, rgba(0,0,0,0.15));
      outline: none;
      cursor: pointer;
      border-radius: 2px;
    }

    .density-control input[type="range"]::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 13px;
      height: 13px;
      background: var(--accent-color, #000);
      border-radius: 50%;
      cursor: pointer;
      transition: transform 0.2s ease;
    }

    .density-control input[type="range"]::-webkit-slider-thumb:hover {
      transform: scale(1.2);
    }

    .density-control input[type="range"]::-moz-range-thumb {
      width: 13px;
      height: 13px;
      background: var(--accent-color, #000);
      border-radius: 50%;
      border: 0;
      cursor: pointer;
      transition: transform 0.2s ease;
    }

    .density-control input[type="range"]::-moz-range-thumb:hover {
      transform: scale(1.2);
    }

    .density-display {
      font-size: 11px;
      letter-spacing: 0.18em;
      text-transform: uppercase;
      font-weight: 600;
      color: var(--text-muted, rgba(0,0,0,0.6));
      min-width: 60px;
      text-align: right;
    }

    .toolbar-search {
      flex: 0 1 200px;
      display: flex;
      align-items: center;
      gap: 8px;
      border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1));
      padding: 4px 0;
    }

    .toolbar-search input {
      flex: 1;
      background: transparent;
      border: 0;
      padding: 4px 0;
      font-family: inherit;
      font-size: 12px;
      letter-spacing: 0.04em;
      color: var(--text-color, inherit);
      outline: none;
    }

    .toolbar-search input::placeholder {
      color: var(--text-muted, rgba(0,0,0,0.4));
    }

    .toolbar-search svg {
      width: 14px;
      height: 14px;
      stroke: var(--text-muted, rgba(0,0,0,0.4));
      fill: none;
      stroke-width: 1.5;
      flex-shrink: 0;
    }

    /* Mobile responsive — hide density slider on mobile */
    @media (max-width: 768px) {
      .collection-toolbar {
        gap: 12px;
      }

      .toolbar-left {
        flex: 1 1 100%;
        order: 1;
        flex-wrap: wrap;
      }

      .toolbar-search {
        flex: 1 1 100%;
        order: 2;
        margin-top: 8px;
      }

      .density-control {
        display: none !important;
        visibility: hidden !important;
        width: 0 !important;
        height: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        border: 0 !important;
      }
    }

    /* Extra-small devices — ensure slider is completely hidden */
    @media (max-width: 480px) {
      .density-control {
        display: none !important;
        visibility: hidden !important;
        pointer-events: none !important;
      }
    }
  </style>

  <div class="toolbar-left">
    <label for="collectionSort" class="toolbar-label">Sort</label>
    <select id="collectionSort" aria-label="Sort products">
      <option value="newest">Newest</option>
      <option value="color">Color</option>
      <option value="style">Style</option>
      <option value="sku">SKU A→Z</option>
      <option value="title">Title A→Z</option>
      <option value="price-asc">Price ↑</option>
      <option value="price-desc">Price ↓</option>
    </select>

    <div class="density-control">
      <span class="toolbar-label">Grid</span>
      <input type="range" id="densitySlider" min="3" max="12" value="4" aria-label="Grid density">
      <div class="density-display"><span id="densityLabel">4</span> col</div>
    </div>
  </div>

  {% if section.settings.show_search %}
    <div class="toolbar-search">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
        <circle cx="11" cy="11" r="8"></circle>
        <path d="m21 21-4.35-4.35"></path>
      </svg>
      <input type="text" id="collectionSearch" placeholder="Search products…" aria-label="Search products">
    </div>
  {% endif %}
</div>

<script>
(function() {
  const sortSelect = document.getElementById('collectionSort');
  const densitySlider = document.getElementById('densitySlider');
  const densityLabel = document.getElementById('densityLabel');
  const searchInput = document.getElementById('collectionSearch');
  const STORAGE_KEY_SORT = 'dw_collection_sort';
  const STORAGE_KEY_DENSITY = 'dw_collection_density';
  const STORAGE_KEY_SEARCH = 'dw_collection_search';

  // Hard guard: if the canonical toolbar markup isn't on this page, bail out
  // entirely so a missing optional control (e.g. search when disabled) can never
  // throw "Cannot read properties of null". 2026-06-23 consolidation.
  if (!sortSelect || !densitySlider || !densityLabel) return;

  // Load saved state
  try {
    const savedSort = localStorage.getItem(STORAGE_KEY_SORT);
    if (savedSort) sortSelect.value = savedSort;

    const savedDensity = localStorage.getItem(STORAGE_KEY_DENSITY);
    if (savedDensity) {
      densitySlider.value = savedDensity;
      densityLabel.textContent = savedDensity;
      updateGridDensity(parseInt(savedDensity));
    }

    const savedSearch = localStorage.getItem(STORAGE_KEY_SEARCH);
    if (savedSearch && searchInput) searchInput.value = savedSearch;
  } catch (e) {}

  // Sort change
  sortSelect.addEventListener('change', function() {
    try { localStorage.setItem(STORAGE_KEY_SORT, this.value); } catch (e) {}
    applySort(this.value);
  });

  // Density slider
  densitySlider.addEventListener('input', function() {
    const cols = parseInt(this.value);
    densityLabel.textContent = cols;
    try { localStorage.setItem(STORAGE_KEY_DENSITY, cols); } catch (e) {}
    updateGridDensity(cols);
  });

  // Search (optional — only present when section.settings.show_search is on)
  if (searchInput) {
    searchInput.addEventListener('input', debounce(function() {
      try { localStorage.setItem(STORAGE_KEY_SEARCH, this.value); } catch (e) {}
      applySearch(this.value);
    }, 300));
  }

  function updateGridDensity(cols) {
    const productGrid = document.querySelector('.collection-products') || document.querySelector('[data-collection-grid]') || document.querySelector('[role="list"]') || document.querySelector('[data-product-grid]');
    if (productGrid) {
      productGrid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
      productGrid.setAttribute('data-cols', cols);
    }
  }

  function applySort(mode) {
    const url = new URL(window.location);
    if (mode === 'newest') {
      url.searchParams.delete('sort_by');
    } else {
      // Map DW sort modes to Shopify's sort_by values
      const sortMap = {
        'title': 'title-ascending',
        'price-asc': 'price-ascending',
        'price-desc': 'price-descending'
      };
      url.searchParams.set('sort_by', sortMap[mode] || mode);
    }
    window.location.href = url.toString();
  }

  function applySearch(query) {
    if (query.trim() === '') {
      window.location.href = window.location.pathname;
    } else {
      const url = new URL(window.location);
      url.searchParams.set('q', query);
      window.location.href = url.toString();
    }
  }

  function debounce(fn, delay) {
    let timer;
    return function(...args) {
      clearTimeout(timer);
      timer = setTimeout(() => fn.apply(this, args), delay);
    };
  }

  // Initialize grid on page load
  window.addEventListener('load', function() {
    updateGridDensity(parseInt(densitySlider.value));
  });
})();
</script>