← back to Rentv
crm: in-facet type-ahead for large filter facets (City/List/Segment)
1ca6cbbc8ac06903d002a91fa9671ff9f9527048 · 2026-08-12 23:24:33 -0700 · Steve Abrams
Facets with >12 values render a filter input; live-matches any value
(incl. beyond the top-12), no re-render so focus is kept; 'more' hides
while searching, restores on clear. Small facets unchanged. Verified real
Chrome: 0 console errors, santa->4 matches incl. hidden values.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 1ca6cbbc8ac06903d002a91fa9671ff9f9527048
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 23:24:33 2026 -0700
crm: in-facet type-ahead for large filter facets (City/List/Segment)
Facets with >12 values render a filter input; live-matches any value
(incl. beyond the top-12), no re-render so focus is kept; 'more' hides
while searching, restores on clear. Small facets unchanged. Verified real
Chrome: 0 console errors, santa->4 matches incl. hidden values.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/crm.html | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/public/crm.html b/public/crm.html
index 054fe4c9..e5614212 100644
--- a/public/crm.html
+++ b/public/crm.html
@@ -35,6 +35,9 @@
.facet>.fg .fc{margin-left:auto;font-size:10px;color:#7a838c;font-weight:700}
.facet .fvals{display:none;flex-direction:column;gap:2px;padding:0 4px 8px}
.facet.open .fvals{display:flex}
+ .facet .ffilter{background:#0e141a;border:1px solid var(--line);border-radius:8px;color:#e7ecf1;font:600 11.5px 'Inter';padding:6px 9px;margin:0 6px 6px}
+ .facet .ffilter::placeholder{color:#6b747d}.facet .ffilter:focus{outline:0;border-color:var(--red)}
+ .fval.hid{display:none}
.fval{display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;color:#aeb7c0;font:600 12px 'Inter';padding:6px 10px;border-radius:8px;cursor:pointer;text-align:left}
.fval:hover{background:#12161c;color:#e7ecf1}
.fval.on{background:#12161c;color:#fff;box-shadow:inset 3px 0 0 var(--red)}
@@ -361,18 +364,29 @@ function renderFacets(){
act.forEach(v=>{ if(!tally.has(v)) tally.set(v,0); }); // keep an active value visible even at 0 in the current context
const vals=[...tally.entries()].sort((a,b)=> b[1]-a[1] || String(a[0]).localeCompare(String(b[0])));
if(!vals.length) return '';
- const showAll=S.facetMore[f.key], shown=showAll?vals:vals.slice(0,FACET_TOPN), fmt=f.fmt||(x=>x);
- const rowsHTML=shown.map(([v,n])=>`<button class="fval ${act.has(v)?'on':''}" data-fk="${esc(f.key)}" data-fv="${esc(v)}"><span class="ck">${act.has(v)?'✓':''}</span><span class="fl" title="${esc(fmt(v))}">${esc(fmt(v))}</span><span class="n">${n.toLocaleString()}</span></button>`).join('');
- const more=vals.length>FACET_TOPN ? `<button class="more" data-more="${esc(f.key)}">${showAll?'− Show less':'+ '+(vals.length-FACET_TOPN)+' more'}</button>` : '';
+ const showAll=S.facetMore[f.key], fmt=f.fmt||(x=>x), big=vals.length>FACET_TOPN;
+ // render ALL values (overflow hidden) so the type-ahead can reach any of them, not just the top-12
+ const rowsHTML=vals.map(([v,n],i)=>{ const lbl=fmt(v); const hid=(!showAll && i>=FACET_TOPN)?' hid':'';
+ return `<button class="fval ${act.has(v)?'on':''}${hid}" data-fk="${esc(f.key)}" data-fv="${esc(v)}" data-fl="${esc(String(lbl).toLowerCase())}"><span class="ck">${act.has(v)?'✓':''}</span><span class="fl" title="${esc(lbl)}">${esc(lbl)}</span><span class="n">${n.toLocaleString()}</span></button>`;
+ }).join('');
+ const search=big?`<input class="ffilter" data-ff="${esc(f.key)}" placeholder="filter ${esc(String(f.label).toLowerCase())}… (${vals.length})">`:'';
+ const more=big?`<button class="more" data-more="${esc(f.key)}">${showAll?'− Show less':'+ '+(vals.length-FACET_TOPN)+' more'}</button>`:'';
return `<div class="facet ${S.facetOpen[f.key]?'open':''}" data-facet="${esc(f.key)}">
<button class="fg" data-fg="${esc(f.key)}"><span class="caret">▶</span>${esc(f.label)}<span class="fc">${act.size?act.size+' sel':vals.length}</span></button>
- <div class="fvals">${rowsHTML}${more}</div>
+ <div class="fvals">${search}${rowsHTML}${more}</div>
</div>`;
}).join('');
$('facetClear').classList.toggle('show', activeFacetCount()>0);
box.querySelectorAll('.fg').forEach(b=>b.onclick=()=>{ const k=b.dataset.fg; S.facetOpen[k]=S.facetOpen[k]?0:1; setLS('facetOpen',JSON.stringify(S.facetOpen)); renderFacets(); });
box.querySelectorAll('.fval').forEach(b=>b.onclick=()=>{ const k=b.dataset.fk,v=b.dataset.fv,arr=S.facet[k]; const i=arr.indexOf(v); if(i>=0)arr.splice(i,1); else arr.push(v); setLS('facet',JSON.stringify(S.facet)); S.sel=null; render(); });
box.querySelectorAll('.more').forEach(b=>b.onclick=()=>{ const k=b.dataset.more; S.facetMore[k]=!S.facetMore[k]; renderFacets(); });
+ box.querySelectorAll('.ffilter').forEach(inp=>inp.oninput=()=>{ // live type-ahead: unhide any matching value (even beyond top-12), no re-render so focus is kept
+ const wrap=inp.closest('.facet'), q=inp.value.trim().toLowerCase(), more=wrap.querySelector('.more');
+ wrap.querySelectorAll('.fval').forEach((b,i)=>{
+ b.classList.toggle('hid', q ? !(b.dataset.fl||'').includes(q) : (i>=FACET_TOPN && !S.facetMore[inp.dataset.ff]));
+ });
+ if(more) more.style.display=q?'none':'';
+ });
}
// ── cross-reference the LABJ directory contacts against the OTHER RE-build DB already in this
// console: CRCP commercial brokers + LA-assessor owners (fetched by /api/crm). Match by
← 38f6ecc2 crm: add faceted filter panel to left rail (City/Source/List
·
back to Rentv
·
auto-data-snapshot: 2026-08-12T23:41:34 (7 data files) — dat 18321e52 →