Filter by Variant Options

Description

07.26c10v4 Filter by Variant Options

#1. Install Code

#1.1. Click Gear icon on Store Page

07.26c05v1 Variant Description

Next, click Advanced > Page Header Code Injection

07.26c05v1 Variant Description

#1.2. Add this code to right box

<!-- 07.26c10v4 Filter by Variant Options -->
<script>
window.StoreVariantFilterConfig = {
  sectionSelector: '.product-list-container',
  spinnerTime: 500,
  format: 'dropdown', 
  logic: 'AND',
  multiSelect: false,
  showCount: true,
  hideEmpty: true,
  allLabel: 'All',
  search: {
    enable: false,
    placeholder: 'Search products...',
    width: '240px',
    background: '#ffffff',
    border: '1px solid #000000',
    borderRadius: '4px',
    textSize: '14px',
    textColor: '#000000',
    placeholderColor: '#888888'
  },
  style: {
    gap: '15px',
    rowGap: '15px',
    marginBottom: '30px',
    fontSize: '14px',
    activeColor: 'transparent',
    activeTextColor: '#000000',
    inactiveColor: 'transparent',
    inactiveTextColor: '#000000',
    dropdown: {
      minWidth: '150px',
      background: '#ffffff',
      border: '1px solid #e5e5e5',
      borderRadius: '3px',
      textSize: '14px',
      textColor: '#333333',
      panelBackground: '#ffffff',
      hoverBackground: '#f5f5f5'
    }
  }
};
</script>
<script>
(function() {
  const cfg = window.StoreVariantFilterConfig;
  if (!cfg) return;
  const st = cfg.style;
  const dd = st.dropdown;
  let allItems = [];
  let filterGroups = [];
  let searchTerm = '';
  let spinnerTimer;

  function injectCSS() {
    if (document.getElementById('svf-styles')) return;
    const style = document.createElement('style');
    style.id = 'svf-styles';
    style.innerHTML = `
      .svf-bar { display: flex; flex-wrap: wrap; align-items: center; gap: ${st.gap}; row-gap: ${st.rowGap}; margin-bottom: ${st.marginBottom}; }
      .svf-groups { display: flex; flex-wrap: wrap; align-items: center; gap: ${st.gap}; row-gap: ${st.rowGap}; }
      .svf-dd { position: relative; display: inline-block; }
      .svf-dd-head { display: inline-flex; align-items: center; justify-content: space-between; min-width: ${dd.minWidth}; padding: 10px 15px; background: ${dd.background}; border: ${dd.border}; border-radius: ${dd.borderRadius}; color: ${dd.textColor}; font-size: ${dd.textSize}; font-family: inherit; cursor: pointer; }
      .svf-dd-list { list-style: none; margin: 5px 0 0; padding: 5px 0; position: absolute; top: 100%; left: 0; min-width: 100%; z-index: 50; background: ${dd.panelBackground}; border: ${dd.border}; border-radius: ${dd.borderRadius}; display: none; max-height: 250px; overflow: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
      .svf-dd-list li { padding: 4px 15px; cursor: pointer; font-size: ${dd.textSize}; color: ${dd.textColor}; white-space: nowrap; font-weight: 400; line-height: 1.2; }
      .svf-dd-list li:hover { background: ${dd.hoverBackground}; }
      .svf-search { margin-left: auto; box-sizing: border-box; width: ${cfg.search.width}; background: ${cfg.search.background}; border: ${cfg.search.border}; border-radius: ${cfg.search.borderRadius}; color: ${cfg.search.textColor}; font-size: ${cfg.search.textSize}; padding: 10px 15px; outline: none; font-family: inherit;}
      .svf-search::placeholder { color: ${cfg.search.placeholderColor}; }
      .svf-spinner { display: none; width: 16px; height: 16px; border: 2px solid rgba(0,0,0,0.1); border-top: 2px solid ${dd.textColor}; border-radius: 50%; animation: svf-spin 0.8s linear infinite; margin-left: 5px; flex-shrink: 0; align-self: center; }
      @keyframes svf-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    `;
    document.head.appendChild(style);
  }

  function fetchStoreData(url) {
    return fetch(url + '?format=json').then(res => res.json());
  }

  function processData(data, domItems) {
    const attributeMap = {};
    const itemsMap = new Map();
    data.items.forEach(item => {
      const attributes = {};
      if(item.variants && item.variants.length > 0) {
          item.variants.forEach(variant => {
             if(variant.attributes) {
                 Object.keys(variant.attributes).forEach(key => {
                     if(!attributeMap[key]) attributeMap[key] = new Set();
                     attributeMap[key].add(variant.attributes[key]);
                     if(!attributes[key]) attributes[key] = new Set();
                     attributes[key].add(variant.attributes[key]);
                 });
             }
          });
      }
      itemsMap.set(item.id, attributes);
    });
    Object.keys(attributeMap).forEach(key => {
      filterGroups.push({
        label: key,
        options: Array.from(attributeMap[key]).sort(),
        selected: new Set()
      });
    });
    domItems.forEach(el => {
      const pid = el.getAttribute('data-product-id');
      if (pid && itemsMap.has(pid)) {
        allItems.push({ el: el, data: itemsMap.get(pid), title: el.querySelector('.product-list-item-title').textContent.toLowerCase() });
      }
    });
  }

  function getCount(groupLabel, option) {
    let count = 0;
    allItems.forEach(item => {
      if (item.data[groupLabel] && item.data[groupLabel].has(option)) count++;
    });
    return count;
  }

  function applyFilters() {
    const spinner = document.querySelector('.svf-spinner');
    if (spinner) spinner.style.display = 'block';
    clearTimeout(spinnerTimer);

    allItems.forEach(item => {
      let matchSearch = true;
      if (searchTerm) matchSearch = item.title.includes(searchTerm);
      let matchFilter = true;
      const activeGroups = filterGroups.filter(g => g.selected.size > 0);
      
      if (activeGroups.length > 0) {
        if (cfg.logic === 'AND') {
          matchFilter = activeGroups.every(g => {
            let hasAttr = false;
            g.selected.forEach(val => {
               if (item.data[g.label] && item.data[g.label].has(val)) hasAttr = true;
            });
            return hasAttr;
          });
        } else {
          matchFilter = activeGroups.some(g => {
            let hasAttr = false;
            g.selected.forEach(val => {
               if (item.data[g.label] && item.data[g.label].has(val)) hasAttr = true;
            });
            return hasAttr;
          });
        }
      }
      item.el.style.display = (matchSearch && matchFilter) ? '' : 'none';
    });

    document.querySelectorAll('.svf-dd-head').forEach(head => {
        const index = head.getAttribute('data-index');
        const g = filterGroups[index];
        const labelEl = head.querySelector('.svf-dd-label');
        if (g.selected.size === 0) labelEl.textContent = g.label;
        else labelEl.textContent = Array.from(g.selected).join(', ');
    });
    
    document.querySelectorAll('.svf-dd-list li').forEach(li => {
       const groupIdx = li.getAttribute('data-group');
       const opt = li.getAttribute('data-opt');
       const g = filterGroups[groupIdx];
       if(opt === '__all__') li.style.fontWeight = g.selected.size === 0 ? '700' : '400';
       else li.style.fontWeight = g.selected.has(opt) ? '700' : '400';
    });

    // Cập nhật lấy thời gian từ file cấu hình
    spinnerTimer = setTimeout(() => {
      if (spinner) spinner.style.display = 'none';
    }, cfg.spinnerTime || 1000); 
  }

  function buildUI(container) {
    if(filterGroups.length === 0 && !cfg.search.enable) return;

    const bar = document.createElement('div');
    bar.className = 'svf-bar';

    const groupsWrap = document.createElement('div');
    groupsWrap.className = 'svf-groups';

    filterGroups.forEach((g, idx) => {
      const wrap = document.createElement('div');
      wrap.className = 'svf-dd';

      const head = document.createElement('button');
      head.className = 'svf-dd-head';
      head.setAttribute('data-index', idx);
      head.innerHTML = `<span class="svf-dd-label">${g.label}</span><span style="margin-left:8px;">▾</span>`;

      const ul = document.createElement('ul');
      ul.className = 'svf-dd-list';

      if (cfg.allLabel) {
        const liAll = document.createElement('li');
        liAll.textContent = cfg.allLabel;
        liAll.setAttribute('data-group', idx);
        liAll.setAttribute('data-opt', '__all__');
        liAll.onclick = (e) => {
          e.stopPropagation();
          g.selected.clear();
          ul.style.display = 'none';
          applyFilters();
        };
        ul.appendChild(liAll);
      }

      g.options.forEach(opt => {
        const count = getCount(g.label, opt);
        if (cfg.hideEmpty && count === 0) return;
        
        const li = document.createElement('li');
        li.textContent = cfg.showCount ? `${opt} (${count})` : opt;
        li.setAttribute('data-group', idx);
        li.setAttribute('data-opt', opt);
        li.onclick = (e) => {
          e.stopPropagation();
          if (cfg.multiSelect) {
             if(g.selected.has(opt)) g.selected.delete(opt); else g.selected.add(opt);
          } else {
             g.selected.clear(); g.selected.add(opt);
             ul.style.display = 'none';
          }
          applyFilters();
        };
        ul.appendChild(li);
      });

      head.onclick = (e) => {
        e.preventDefault();
        e.stopPropagation();
        const isOpen = ul.style.display === 'block';
        document.querySelectorAll('.svf-dd-list').forEach(l => l.style.display = 'none');
        ul.style.display = isOpen ? 'none' : 'block';
      };

      wrap.appendChild(head);
      wrap.appendChild(ul);
      groupsWrap.appendChild(wrap);
    });

    const spinner = document.createElement('div');
    spinner.className = 'svf-spinner';
    groupsWrap.appendChild(spinner);

    bar.appendChild(groupsWrap);

    if (cfg.search.enable) {
      const input = document.createElement('input');
      input.className = 'svf-search';
      input.placeholder = cfg.search.placeholder;
      input.oninput = (e) => {
        searchTerm = e.target.value.toLowerCase();
        applyFilters();
      };
      bar.appendChild(input);
    }

    container.insertBefore(bar, container.firstChild);
    document.addEventListener('click', () => {
        document.querySelectorAll('.svf-dd-list').forEach(l => l.style.display = 'none');
    });
  }

  function init() {
    const section = document.querySelector(cfg.sectionSelector);
    if (!section || section.dataset.svfInit) return;
    
    const domItems = section.querySelectorAll('.product-list-item');
    if (domItems.length === 0) return;
    
    section.dataset.svfInit = 'true';
    injectCSS();

    fetchStoreData(window.location.pathname)
      .then(data => {
        processData(data, domItems);
        buildUI(section);
        const firstSpinner = document.querySelector('.svf-spinner');
        if(firstSpinner) firstSpinner.style.display = 'none';
      });
  }

  setTimeout(init, 500);
})();
</script>

07.26c10v4 Filter by Variant Options

#2. Customize

#2.1. You can change variant dropdown style at Line 32 – Line 41

dropdown: {
  minWidth: '150px',
  background: '#ffffff',
  border: '1px solid #e5e5e5',
  borderRadius: '3px',
  textSize: '14px',
  textColor: '#333333',
  panelBackground: '#ffffff',
  hoverBackground: '#f5f5f5'
}

 

 

Buy me a coffee