← back to Dw Theme Frontpage Cta
sections/dw-boost-filter-topbar.liquid
515 lines
{%- comment -%}
DW Boost Filter Topbar — DTD verdict B (officer-yolo 2026-06-25)
A CUSTOM horizontal DROPDOWN filter toolbar that sits ABOVE Boost's grid and
drives Boost's pf_* URL params + sort_by. Ported UX from the left-hand filters
of new.designerwallcoverings.com (new-import-viewer): toggleable controls,
sort + density + search, all persisted to localStorage.
── HARD ARCHITECTURE RULES (do not violate) ──
1. This bar READS Boost's filter-tree DOM (option labels/counts) + the current
URL's pf_* params, and WRITES pf_* / sort_by params then NAVIGATES.
It NEVER mutates Boost's result grid DOM, never removes a Boost node, never
restyles Boost's React internals. Zero grid-break surface (see memory
reference_dw_theme_grid_init_abort: dropping a Boost DOM hook silently breaks
every grid — this bar only ADDS a sibling node, queries are read-only).
2. Boost is the filter ENGINE. The bar is UX-only. If Boost re-renders 1000x/day
the bar is independent — on each Boost mutation it re-reads option values
(debounced) but only to repopulate its OWN dropdowns.
3. Each filter field is individually toggleable ON/OFF by the user; the choice
persists in localStorage (dw_topbar_fields), same convention as sort/density.
4. NO private-label vendor names ever surface (Brewster/York/WallQuest/Command54
etc.) — the Brand/Vendor dropdown is built from Boost's emitted option list,
which already carries house-line names; a deny-list scrub is applied as a
belt-and-suspenders guard before any option is rendered.
── Boost param contract (verified on the live page 2026-06-25) ──
pf_t_<tag> tag-backed facets → product_type, color, style, durability,
historical_era (extensible: material,
process, pattern, contents = pf_t_*)
pf_v_brand vendor/brand facet
pf_p_price price range (pf_p_price=<min>:<max>)
sort_by Boost/Shopify sort (created-descending, price-ascending, …)
q search term
Each FIELD below maps to one Boost param. Fields whose Boost facet is not yet
configured in the Boost SD dashboard render their dropdown EMPTY with a
"configure in Boost" hint (see the accompanying runbook) — they do not break.
Revision: 2026-06-25 (officer-yolo / vp-dw-commerce)
{%- endcomment -%}
{%- liquid
assign accent = settings.color_accent | default: '#111111'
-%}
<div class="dw-tbf" data-section-id="{{ section.id }}" data-dw-topbar>
<style>
.dw-tbf{width:100%;padding:14px 0;margin-bottom:18px;border-bottom:1px solid var(--border-color,rgba(0,0,0,.1));
font-family:inherit;--dw-acc:{{ accent }}}
.dw-tbf__row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.dw-tbf__label{font-size:10px;letter-spacing:.3em;text-transform:uppercase;font-weight:700;
color:var(--text-muted,rgba(0,0,0,.55));margin-right:2px;white-space:nowrap}
/* one dropdown unit */
.dw-dd{position:relative}
.dw-dd__btn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 13px;
font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;
color:var(--text-color,inherit);background:transparent;border:1px solid var(--border-color,rgba(0,0,0,.14));
border-radius:20px;cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s;line-height:1}
.dw-dd__btn:hover{border-color:var(--dw-acc)}
.dw-dd__btn[aria-expanded="true"]{border-color:var(--dw-acc);background:var(--bg-hover,rgba(0,0,0,.03))}
.dw-dd__btn .dw-caret{font-size:8px;opacity:.6;transition:transform .15s}
.dw-dd__btn[aria-expanded="true"] .dw-caret{transform:rotate(180deg)}
.dw-dd__btn .dw-cnt{font-variant-numeric:tabular-nums;background:var(--dw-acc);color:#fff;
border-radius:999px;font-size:9px;min-width:15px;height:15px;display:inline-flex;align-items:center;
justify-content:center;padding:0 4px}
/* active (has a selection) */
.dw-dd.has-sel .dw-dd__btn{background:var(--dw-acc);color:#fff;border-color:var(--dw-acc)}
.dw-dd.has-sel .dw-dd__btn .dw-cnt{background:#fff;color:var(--dw-acc)}
/* panel */
.dw-dd__panel{position:absolute;top:calc(100% + 6px);left:0;z-index:40;min-width:220px;max-width:300px;
max-height:340px;overflow:auto;background:var(--bg-color,#fff);border:1px solid var(--border-color,rgba(0,0,0,.14));
border-radius:12px;box-shadow:0 12px 34px rgba(0,0,0,.16);padding:8px;display:none}
.dw-dd.open .dw-dd__panel{display:block}
.dw-opt{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;cursor:pointer;
font-size:12.5px;color:var(--text-color,inherit);user-select:none}
.dw-opt:hover{background:var(--bg-hover,rgba(0,0,0,.04))}
.dw-opt input{width:15px;height:15px;accent-color:var(--dw-acc);margin:0;cursor:pointer;flex:0 0 auto}
.dw-opt .dw-sw{width:15px;height:15px;border-radius:50%;border:1px solid rgba(0,0,0,.18);flex:0 0 auto}
.dw-opt .dw-on{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dw-opt .dw-oc{font-size:10.5px;color:var(--text-muted,rgba(0,0,0,.45));font-variant-numeric:tabular-nums}
.dw-dd__empty{padding:14px 10px;font-size:11.5px;color:var(--text-muted,rgba(0,0,0,.5));line-height:1.4}
.dw-dd__price{display:flex;gap:8px;align-items:center;padding:10px 9px}
.dw-dd__price input{width:74px;height:30px;border:1px solid var(--border-color,rgba(0,0,0,.16));
border-radius:8px;padding:0 8px;font:inherit;font-size:12px}
.dw-dd__apply{margin-top:6px;width:100%;height:32px;border:0;border-radius:8px;background:var(--dw-acc);
color:#fff;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;cursor:pointer}
/* sort + density + search reuse the same chip vocabulary */
.dw-tbf__sort{height:34px;padding:0 11px;border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:20px;
background:transparent;font:inherit;font-size:11px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;cursor:pointer;color:inherit}
.dw-tbf__sort:hover{border-color:var(--dw-acc)}
.dw-density{display:inline-flex;align-items:center;gap:9px;height:34px}
.dw-density input[type=range]{width:96px;accent-color:var(--dw-acc);cursor:pointer}
.dw-density .dw-dlabel{font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
color:var(--text-muted,rgba(0,0,0,.55));min-width:42px;text-align:right}
.dw-search{display:inline-flex;align-items:center;gap:7px;height:34px;border:1px solid var(--border-color,rgba(0,0,0,.14));
border-radius:20px;padding:0 12px;min-width:160px}
.dw-search input{flex:1;border:0;background:transparent;font:inherit;font-size:12px;outline:none;color:inherit}
.dw-search svg{width:13px;height:13px;stroke:var(--text-muted,rgba(0,0,0,.4));fill:none;stroke-width:1.6;flex:0 0 auto}
.dw-clear{margin-left:auto;font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
color:var(--dw-acc);background:transparent;border:0;cursor:pointer;white-space:nowrap}
.dw-clear:disabled{opacity:.35;cursor:default}
/* product-type segmented control (ports Boost's Wallcoverings/Fabrics tabs) */
.dw-seg{display:inline-flex;border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:20px;overflow:hidden;height:34px;margin-right:4px}
.dw-seg button{border:0;background:transparent;padding:0 14px;font:inherit;font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:600;cursor:pointer;color:inherit;white-space:nowrap;line-height:1}
.dw-seg button+button{border-left:1px solid var(--border-color,rgba(0,0,0,.12))}
.dw-seg button:hover{background:var(--bg-hover,rgba(0,0,0,.04))}
.dw-seg button[aria-pressed="true"]{background:var(--dw-acc);color:#fff}
/* live product count (ports Boost's "N products") */
.dw-count{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:var(--text-muted,rgba(0,0,0,.55));white-space:nowrap;margin-left:2px}
/* REPLACE Boost's native toolbar — this topbar is the single control surface.
Cosmetic hide ONLY: the node stays in the DOM so Boost's own JS and our
read-only count-reader keep working (never removes a Boost hook). */
.boost-sd__toolbar{display:none!important}
/* the "which filters are shown" gear menu (per-field ON/OFF) */
.dw-gear{position:relative}
.dw-gear__btn{height:34px;width:34px;display:inline-flex;align-items:center;justify-content:center;
border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:50%;background:transparent;cursor:pointer;font-size:14px}
.dw-gear__btn:hover{border-color:var(--dw-acc)}
.dw-gear__panel{position:absolute;top:calc(100% + 6px);right:0;z-index:41;min-width:200px;background:var(--bg-color,#fff);
border:1px solid var(--border-color,rgba(0,0,0,.14));border-radius:12px;box-shadow:0 12px 34px rgba(0,0,0,.16);padding:8px;display:none}
.dw-gear.open .dw-gear__panel{display:block}
.dw-gear__panel .dw-gtitle{font-size:9.5px;letter-spacing:.22em;text-transform:uppercase;font-weight:700;
color:var(--text-muted,rgba(0,0,0,.5));padding:5px 9px 7px}
@media (max-width:720px){
.dw-tbf__label{display:none}
.dw-density{display:none}
.dw-search{min-width:0;flex:1 1 100%;order:5}
.dw-dd__panel{max-width:84vw}
}
</style>
<div class="dw-tbf__row" data-tbf-row>
{%- comment -%} product-type segmented control (replaces Boost's native Wallcoverings/Fabrics tabs) {%- endcomment -%}
<span class="dw-seg" data-tbf-types role="group" aria-label="Product type">
<button type="button" data-ptype="" aria-pressed="false">All</button>
<button type="button" data-ptype="Wallcovering" aria-pressed="false">Wallcoverings</button>
<button type="button" data-ptype="Fabric" aria-pressed="false">Fabrics</button>
</span>
<span class="dw-tbf__label">Filter</span>
<span data-tbf-fields></span>
{%- comment -%} sort {%- endcomment -%}
<span class="dw-tbf__label" style="margin-left:6px">Sort</span>
<select class="dw-tbf__sort" data-tbf-sort aria-label="Sort products">
<option value="">Newest</option>
<option value="created-descending">Created · Newest</option>
<option value="created-ascending">Created · Oldest</option>
<option value="title-ascending">Title A→Z</option>
<option value="title-descending">Title Z→A</option>
<option value="price-ascending">Price ↑</option>
<option value="price-descending">Price ↓</option>
<option value="best-selling">Best selling</option>
</select>
{%- comment -%} density {%- endcomment -%}
<span class="dw-density" data-tbf-density-wrap>
<span class="dw-tbf__label">Grid</span>
<input type="range" min="2" max="8" value="4" data-tbf-density aria-label="Grid density">
<span class="dw-dlabel"><span data-tbf-dlabel>4</span> col</span>
</span>
{%- comment -%} search {%- endcomment -%}
<span class="dw-search" data-tbf-search-wrap>
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
<input type="text" data-tbf-search placeholder="Search pattern / sku / color…" aria-label="Search">
</span>
<button class="dw-clear" data-tbf-clear disabled>Clear all</button>
<span class="dw-count" data-tbf-count></span>
{%- comment -%} per-field ON/OFF gear {%- endcomment -%}
<span class="dw-gear" data-tbf-gear>
<button class="dw-gear__btn" data-tbf-gear-btn title="Choose which filters to show" aria-haspopup="true" aria-expanded="false">⚙</button>
<div class="dw-gear__panel" data-tbf-gear-panel>
<div class="dw-gtitle">Show filters</div>
<span data-tbf-gear-list></span>
</div>
</span>
</div>
</div>
<script>
(function(){
'use strict';
var root = document.currentScript.closest('[data-dw-topbar]') || document.querySelector('[data-dw-topbar]');
if(!root || root.__dwTbfInit) return; root.__dwTbfInit = true;
// ── FIELD MODEL ──────────────────────────────────────────────────────────
// key = stable id (localStorage)
// label= dropdown button text
// param= Boost URL param this field writes (the contract)
// kind = 'tag' (pf_t_*) | 'vendor' (pf_v_brand) | 'price' (pf_p_price)
// swatch=render a color dot per option (color/swatch field)
// The order here is the default left→right order in the bar.
var FIELDS = [
{ key:'color', label:'Color', param:'pf_t_color', kind:'tag', swatch:true },
{ key:'style', label:'Style', param:'pf_t_style', kind:'tag' },
{ key:'material', label:'Material', param:'pf_t_material', kind:'tag' },
{ key:'brand', label:'Brand', param:'pf_v_brand', kind:'vendor' },
{ key:'process', label:'Process', param:'pf_t_process', kind:'tag' },
{ key:'vendor', label:'Vendor', param:'pf_v_brand', kind:'vendor' },
{ key:'swatch', label:'Swatch', param:'pf_t_color', kind:'tag', swatch:true },
{ key:'contents', label:'Contents', param:'pf_t_contents', kind:'tag' },
{ key:'pattern', label:'Pattern', param:'pf_t_pattern', kind:'tag' },
{ key:'sku', label:'SKU', param:'pf_t_sku', kind:'tag' },
{ key:'durability',label:'Durability',param:'pf_t_durability', kind:'tag' },
{ key:'era', label:'Era', param:'pf_t_historical_era',kind:'tag' },
{ key:'price', label:'Price', param:'pf_p_price', kind:'price' }
];
// Which fields are ON by default. Data-backed only (verified 2026-06-25): color
// (color: tags), style/brand (global.Style/Brand facets), price (pf_p_price), and
// MATERIAL — backed by global.Contents (vinyl/grasscloth/raffia/paper, 78% fill);
// a Boost facet labeled "Material" from global.Contents emits pf_t_material to match.
// Process OFF (Construction/Finish empty — no data); Pattern OFF (near-unique → poor
// facet). All remain user-enableable via the ⚙ gear when their data lands.
var DEFAULT_ON = ['color','style','brand','material','price'];
// Private-label deny-list — these upstream names must NEVER reach a shopper.
var DENY = [/brewster/i,/\byork\b/i,/wallquest/i,/chesapeake/i,/nextwall/i,/seabrook/i,/command\s*54/i,/desima/i,/carlsten/i,/nicolette\s*mayer/i];
function denied(s){ s=String(s||''); return DENY.some(function(r){return r.test(s);}); }
var LS = {
fields:'dw_topbar_fields', density:'dw_collection_density',
sort:'dw_collection_sort', search:'dw_collection_search'
};
function lsGet(k,d){ try{var v=localStorage.getItem(k); return v==null?d:v;}catch(e){return d;} }
function lsSet(k,v){ try{localStorage.setItem(k,v);}catch(e){} }
function curParams(){ return new URLSearchParams(window.location.search); }
function esc(s){ return String(s==null?'':s).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c];}); }
// ── read which fields are enabled ────────────────────────────────────────
function enabledSet(){
var saved = lsGet(LS.fields,null);
if(saved){ try{ var a=JSON.parse(saved); if(Array.isArray(a)) return new Set(a); }catch(e){} }
return new Set(DEFAULT_ON);
}
// ── read Boost's emitted option values for a tag/vendor param ────────────
// STRICTLY READ-ONLY: we query Boost's filter-tree DOM for labels+counts.
// We never remove/restyle those nodes. If Boost hasn't rendered them yet
// (client API still loading) we fall back to the value already in the URL so
// the active selection is always reflected. Never throws → never breaks grid.
function boostOptions(field){
var out=[], seen={};
var param = field.param;
// 1) values already active in the URL (always show these so state is honest)
curParams().getAll(param).forEach(function(v){
v=decodeURIComponent(v); if(!seen[v]&&!denied(v)){ seen[v]=1; out.push({value:v,label:v,count:null,checked:true}); }
});
// 2) options Boost has rendered in its (possibly-hidden) native tree.
// Boost emits filter options as anchors/inputs carrying the pf_* param in
// href or data attributes. We scan defensively across known shapes.
try{
var tree = document.querySelector('.boost-sd__filter-tree') || document.querySelector('.boost-sd__sidebar');
if(tree){
tree.querySelectorAll('a[href],input[type="checkbox"]').forEach(function(el){
var href = el.getAttribute('href')||'';
var match=null;
if(href.indexOf(param+'=')>-1){
var u; try{ u=new URL(href, window.location.origin); }catch(e){ return; }
(u.searchParams.getAll(param)||[]).forEach(function(v){ match=v; });
}
if(!match && el.getAttribute('data-filter-value') && (el.getAttribute('data-filter-type')||'').toLowerCase().indexOf(field.key)>-1){
match = el.getAttribute('data-filter-value');
}
if(!match) return;
var lbl = (el.textContent||match).replace(/\s+\(\d[\d,]*\)\s*$/,'').trim() || match;
var cm = (el.textContent||'').match(/\((\d[\d,]*)\)\s*$/);
if(seen[match]||denied(match)||denied(lbl)) return;
seen[match]=1; out.push({value:match,label:lbl,count:cm?cm[1]:null,checked:false});
});
}
}catch(e){ /* read-only scan; ignore */ }
return out;
}
// ── swatch hex guess from a color name (cheap, named-color map) ──────────
var NAMED={black:'#1a1a1a',white:'#f4f4f4',grey:'#9aa0a6',gray:'#9aa0a6',silver:'#c8ccd1',red:'#c0392b',
pink:'#e84393',orange:'#e67e22',yellow:'#f1c40f',gold:'#caa53d',green:'#27ae60',olive:'#7d8c3e',
teal:'#16a085',blue:'#2980b9',navy:'#27408b',cyan:'#36c5d6',purple:'#8e44ad',violet:'#7c4dbb',
brown:'#8d6748',beige:'#d8c7a8',cream:'#efe6cf',taupe:'#b3a394',ivory:'#f1ebda',charcoal:'#36393f',
bronze:'#9c6b3c',copper:'#b87333',natural:'#d6c8af',neutral:'#cfc6b8'};
function swatchHex(name){
var n=String(name||'').toLowerCase();
for(var k in NAMED){ if(n.indexOf(k)>-1) return NAMED[k]; }
return '#cfc6b8';
}
// ── build a single dropdown for a field ──────────────────────────────────
function buildDropdown(field){
var opts = (field.kind==='price') ? [] : boostOptions(field);
var active = curParams().getAll(field.param).map(decodeURIComponent).filter(function(v){return !denied(v);});
var dd=document.createElement('span');
dd.className='dw-dd'+(active.length?' has-sel':'');
dd.setAttribute('data-dd', field.key);
var btn=document.createElement('button');
btn.type='button'; btn.className='dw-dd__btn'; btn.setAttribute('aria-expanded','false'); btn.setAttribute('aria-haspopup','true');
btn.innerHTML = esc(field.label) + (active.length?(' <span class="dw-cnt">'+active.length+'</span>'):'') + ' <span class="dw-caret">▾</span>';
dd.appendChild(btn);
var panel=document.createElement('div'); panel.className='dw-dd__panel';
if(field.kind==='price'){
var cur=curParams().get('pf_p_price')||''; var mn='',mx='';
if(cur.indexOf(':')>-1){ var pr=cur.split(':'); mn=pr[0]; mx=pr[1]; }
panel.innerHTML =
'<div class="dw-dd__price">'+
'<input type="number" min="0" step="1" placeholder="Min" data-pmin value="'+esc(mn)+'">'+
'<span style="opacity:.5">–</span>'+
'<input type="number" min="0" step="1" placeholder="Max" data-pmax value="'+esc(mx)+'">'+
'</div>'+
'<button class="dw-dd__apply" data-papply>Apply price</button>';
panel.querySelector('[data-papply]').addEventListener('click',function(){
var lo=panel.querySelector('[data-pmin]').value.trim(), hi=panel.querySelector('[data-pmax]').value.trim();
var p=curParams();
if(lo||hi){ p.set('pf_p_price',(lo||'0')+':'+(hi||'999999')); } else { p.delete('pf_p_price'); }
navTo(p);
});
} else if(!opts.length){
panel.innerHTML='<div class="dw-dd__empty">No <b>'+esc(field.label)+'</b> options are available for this collection yet.</div>';
} else {
var html=opts.map(function(o){
var sw = field.swatch ? '<span class="dw-sw" style="background:'+swatchHex(o.label)+'"></span>' : '';
return '<label class="dw-opt"><input type="checkbox" data-val="'+esc(o.value)+'" '+(o.checked?'checked':'')+'>'+
sw+'<span class="dw-on">'+esc(o.label)+'</span>'+(o.count?('<span class="dw-oc">'+esc(o.count)+'</span>'):'')+'</label>';
}).join('');
panel.innerHTML=html;
panel.querySelectorAll('input[type="checkbox"]').forEach(function(cb){
cb.addEventListener('change',function(){
var p=curParams(); var val=cb.getAttribute('data-val');
var have=p.getAll(field.param).map(decodeURIComponent);
p.delete(field.param);
if(cb.checked){ if(have.indexOf(val)<0) have.push(val); }
else { have=have.filter(function(v){return v!==val;}); }
have.forEach(function(v){ p.append(field.param,v); });
navTo(p);
});
});
}
dd.appendChild(panel);
btn.addEventListener('click',function(e){
e.stopPropagation();
var wasOpen=dd.classList.contains('open');
closeAll();
if(!wasOpen){ dd.classList.add('open'); btn.setAttribute('aria-expanded','true'); }
});
return dd;
}
// ── navigation: write params + go (resets paging so Boost re-queries) ─────
function navTo(params){
params.delete('page'); // jump back to page 1 on any filter change
var qs=params.toString();
window.location.href = window.location.pathname + (qs?('?'+qs):'');
}
function closeAll(){
root.querySelectorAll('.dw-dd.open').forEach(function(d){ d.classList.remove('open'); var b=d.querySelector('.dw-dd__btn'); if(b)b.setAttribute('aria-expanded','false'); });
var g=root.querySelector('[data-tbf-gear]'); if(g)g.classList.remove('open');
}
document.addEventListener('click',function(ev){ if(!root.contains(ev.target)) closeAll(); });
// ── render the field dropdowns according to enabled set ──────────────────
function renderFields(){
var host=root.querySelector('[data-tbf-fields]'); if(!host) return;
var en=enabledSet();
host.innerHTML='';
FIELDS.forEach(function(f){ if(en.has(f.key)) host.appendChild(buildDropdown(f)); });
// clear-all enabled when any pf_*/q is present
var anyActive=false; curParams().forEach(function(v,k){ if(k.indexOf('pf_')===0||k==='q') anyActive=true; });
var clr=root.querySelector('[data-tbf-clear]'); if(clr) clr.disabled=!anyActive;
}
// ── gear menu: per-field ON/OFF, persisted ───────────────────────────────
function renderGear(){
var list=root.querySelector('[data-tbf-gear-list]'); if(!list) return;
var en=enabledSet();
list.innerHTML=FIELDS.map(function(f){
return '<label class="dw-opt"><input type="checkbox" data-fkey="'+f.key+'" '+(en.has(f.key)?'checked':'')+'><span class="dw-on">'+esc(f.label)+'</span></label>';
}).join('');
list.querySelectorAll('input[data-fkey]').forEach(function(cb){
cb.addEventListener('change',function(){
var s=enabledSet();
if(cb.checked) s.add(cb.getAttribute('data-fkey')); else s.delete(cb.getAttribute('data-fkey'));
lsSet(LS.fields, JSON.stringify(Array.from(s)));
renderFields();
});
});
}
var gearBtn=root.querySelector('[data-tbf-gear-btn]'), gear=root.querySelector('[data-tbf-gear]');
if(gearBtn&&gear){ gearBtn.addEventListener('click',function(e){ e.stopPropagation(); var o=gear.classList.contains('open'); closeAll(); if(!o){gear.classList.add('open');gearBtn.setAttribute('aria-expanded','true');} else gearBtn.setAttribute('aria-expanded','false'); }); }
// ── sort ─────────────────────────────────────────────────────────────────
var sortSel=root.querySelector('[data-tbf-sort]');
if(sortSel){
var curSort=curParams().get('sort_by')||lsGet(LS.sort,'');
if(curSort){ var opt=sortSel.querySelector('option[value="'+curSort+'"]'); if(opt) sortSel.value=curSort; }
sortSel.addEventListener('change',function(){
lsSet(LS.sort,this.value); var p=curParams();
if(this.value) p.set('sort_by',this.value); else p.delete('sort_by');
navTo(p);
});
}
// ── density (drives Boost's grid var ONLY, never the grid nodes) ──────────
// We set the CSS custom property --dw-grid-cols that the live theme's Boost
// override already binds to .boost-sd__product-list. We do NOT touch list
// children, never set grid-template on the list directly here unless the var
// isn't wired — purely additive, desktop-only.
var dens=root.querySelector('[data-tbf-density]'), dlabel=root.querySelector('[data-tbf-dlabel]');
function applyDensity(n){
document.documentElement.style.setProperty('--dw-grid-cols', n);
var list=document.querySelector('.boost-sd__product-list');
if(list && !getComputedStyle(list).getPropertyValue('--dw-grid-cols')){
// fallback only if the theme var isn't bound — still never removes nodes
list.style.gridTemplateColumns='repeat('+n+',minmax(0,1fr))';
}
if(dlabel) dlabel.textContent=n;
}
if(dens){
var savedD=lsGet(LS.density,'4'); dens.value=savedD; applyDensity(parseInt(savedD,10)||4);
dens.addEventListener('input',function(){ var n=parseInt(this.value,10)||4; lsSet(LS.density,n); applyDensity(n); });
}
// ── search ────────────────────────────────────────────────────────────────
var srch=root.querySelector('[data-tbf-search]');
if(srch){
srch.value=curParams().get('q')||'';
var t; srch.addEventListener('input',function(){ clearTimeout(t); var self=this; t=setTimeout(function(){
lsSet(LS.search,self.value); var p=curParams();
if(self.value.trim()) p.set('q',self.value.trim()); else p.delete('q');
navTo(p);
},450); });
srch.addEventListener('keydown',function(e){ if(e.key==='Enter'){ clearTimeout(t); var p=curParams(); if(this.value.trim())p.set('q',this.value.trim());else p.delete('q'); navTo(p);} });
}
// ── clear all ──────────────────────────────────────────────────────────────
var clr=root.querySelector('[data-tbf-clear]');
if(clr) clr.addEventListener('click',function(){
var p=curParams(); var del=[];
p.forEach(function(v,k){ if(k.indexOf('pf_')===0||k==='q'||k==='page') del.push(k); });
del.forEach(function(k){ p.delete(k); });
navTo(p);
});
// ── re-read Boost options when Boost re-renders (debounced, read-only) ────
// Only repopulates OUR dropdowns; never writes to Boost's tree.
var ro; var target=document.querySelector('.boost-sd__filter-tree')||document.querySelector('.boost-sd__sidebar');
if(target){
var mo=new MutationObserver(function(){ clearTimeout(ro); ro=setTimeout(function(){ renderFields(); renderCount(); },300); });
mo.observe(target,{childList:true,subtree:true});
}
// ── product-type segmented control (ports Boost's Wallcoverings/Fabrics tabs) ─
var typeWrap=root.querySelector('[data-tbf-types]');
if(typeWrap){
var curType=curParams().get('pf_t_product_type')||'';
typeWrap.querySelectorAll('button[data-ptype]').forEach(function(b){
if(b.getAttribute('data-ptype')===curType) b.setAttribute('aria-pressed','true');
b.addEventListener('click',function(){
var p=curParams(); var t=b.getAttribute('data-ptype');
if(t) p.set('pf_t_product_type',t); else p.delete('pf_t_product_type');
navTo(p);
});
});
}
// ── live product count (ports Boost's "N products"); STRICTLY read-only ──────
function readCount(){
var sels=['.boost-sd__total-product','.boost-sd__filter-total-product','.boost-sd__total-product-count','.boost-sd__refine-total'];
for(var i=0;i<sels.length;i++){ var el=document.querySelector(sels[i]); if(el){ var m=(el.textContent||'').match(/[\d,]+/); if(m) return m[0]; } }
var tb=document.querySelector('.boost-sd__toolbar'); // hidden via CSS but present in DOM
if(tb){ var mm=(tb.textContent||'').match(/([\d,]+)\s*(products|results|items)/i); if(mm) return mm[1]; }
return null;
}
function renderCount(){ var c=root.querySelector('[data-tbf-count]'); if(!c) return; var n=readCount(); c.textContent = n ? (n+' products') : ''; }
// ── boot ──────────────────────────────────────────────────────────────────
renderFields(); renderGear(); renderCount();
// Boost loads async — retry the count a few times until it appears.
var _ct=0, _ci=setInterval(function(){ renderCount(); if(++_ct>8 || root.querySelector('[data-tbf-count]').textContent) clearInterval(_ci); },500);
})();
</script>
{% schema %}
{
"name": "DW Boost Filter Topbar",
"tag": "section",
"class": "dw-boost-filter-topbar",
"settings": [
{
"type": "header",
"content": "Filter Topbar"
},
{
"type": "paragraph",
"content": "Horizontal dropdown filter toolbar that drives Boost's pf_* params. Per-field show/hide is controlled by the shopper via the ⚙ menu (persisted in their browser); these defaults only seed first-visit."
},
{
"type": "color",
"id": "color_accent",
"label": "Accent color",
"default": "#111111"
}
],
"presets": [
{ "name": "DW Boost Filter Topbar" }
]
}
{% endschema %}