← back to Commercialrealestate
broker-grid: tight-default columns, drag-to-reorder, search all fields
982582ec5c21cf55793f34f62e54bde485bc7ff5 · 2026-07-31 08:40:20 -0700 · Steve Abrams
- START TIGHT: default to essentials (name/firm/type/title/state/listings); toggle rest from rail
- move columns around: HTML5 drag-to-reorder headers, order persisted (brokerColOrder)
- search ALL data: filter scans every field the API returns (incl. crexi_id + nested), space=AND
- data-no-sort on table so col-resize.js stops double-firing sort; page owns sort
- Reset button clears order+widths+visibility back to tight default
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
M public/broker-grid.html
Diff
commit 982582ec5c21cf55793f34f62e54bde485bc7ff5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 31 08:40:20 2026 -0700
broker-grid: tight-default columns, drag-to-reorder, search all fields
- START TIGHT: default to essentials (name/firm/type/title/state/listings); toggle rest from rail
- move columns around: HTML5 drag-to-reorder headers, order persisted (brokerColOrder)
- search ALL data: filter scans every field the API returns (incl. crexi_id + nested), space=AND
- data-no-sort on table so col-resize.js stops double-firing sort; page owns sort
- Reset button clears order+widths+visibility back to tight default
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
public/broker-grid.html | 78 ++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 70 insertions(+), 8 deletions(-)
diff --git a/public/broker-grid.html b/public/broker-grid.html
index cb093eb..335f3c3 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -24,6 +24,11 @@
table.g thead th{position:sticky;top:0;background:#1b2230;color:var(--mut);text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;padding:8px 10px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none;z-index:2;}
table.g thead th:hover{color:var(--ink);}
table.g thead th.asc::after{content:' ▲';color:var(--gold);} table.g thead th.desc::after{content:' ▼';color:var(--gold);}
+ /* draggable column reorder (Steve rule 2026-07-31: move columns around) */
+ table.g thead th.dragcol{cursor:grab;}
+ table.g thead th.dragging{opacity:.45;}
+ table.g thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+ table.g thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
table.g tbody td{padding:7px 10px;border-bottom:1px solid #20262f;}
table.g tbody tr:hover td{background:rgba(200,162,75,.07);}
table.g td.n{text-align:right;font-variant-numeric:tabular-nums;}
@@ -89,7 +94,7 @@
<h1><b>👥 Brokers</b> & Agents</h1>
<div class="nav"><a href="/crcp.html">← CRCP</a><a href="/">Explorer</a><a href="/mls.html">📋 Listings</a><a href="/gov-agents.html">🏛️ Gov-Licensed</a><a href="/linkedin.html">🔗 LinkedIn</a></div>
<span class="spacer"></span>
- <input class="search" id="q" type="text" placeholder="search name, firm, phone, email, specialty…">
+ <input class="search" id="q" type="text" placeholder="search all fields — name, firm, phone, email, license, source… (space = AND)">
<div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
<select id="sortsel" title="Sort brokers"></select>
<label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
@@ -102,12 +107,13 @@
<div class="rsec"><h4>Company / firm</h4><div class="chips" id="fFirm"></div></div>
<div class="rsec"><h4>Fields / columns — show / hide</h4>
<div class="fieldtoggles" id="fFields"></div>
- <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button></div>
+ <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset" title="Reset columns to the tight default — order, widths & visibility">↺ Reset</button></div>
+ <div class="mut" style="font-size:10.5px;margin-top:7px;line-height:1.35">Drag a column header to reorder · drag its right edge to resize · click to sort.</div>
</div>
</aside>
<div class="wrap">
<div class="statsbar" id="statsbar"></div>
- <div id="tableView" class="tblwrap"><table class="g"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+ <div id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
<div id="gridView" class="grid hide"></div>
</div>
</div>
@@ -175,12 +181,33 @@ const COLS=[
{k:'source',l:'Source',t:'s',g:'Details'},
{k:'created_at',l:'Added',t:'date',g:'Details'},
];
-let DATA=[], view='table', sortKey='listings', sortDir=-1, q='';
+let DATA=[], view='table', sortKey='listings', sortDir=-1, q='', lastColSig='';
const F={types:new Set(),firms:new Set()};
+// START TIGHT (Steve rule 2026-07-31): only the essentials show on first load; the
+// user toggles the rest on from the rail. A saved pref in VISCOL always wins.
+const DEFAULT_ON=new Set(['name','firm','agent_type','title','state','listings']);
let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('brokerCols')||'{}');}catch(e){}return s;})();
-function colVis(k){ if(k in VISCOL) return !!VISCOL[k]; const c=COLS.find(x=>x.k===k); return c?c.def!==0:true; }
-function visCols(){ return COLS.filter(c=>colVis(c.k)); }
+function colVis(k){ if(k in VISCOL) return !!VISCOL[k]; return DEFAULT_ON.has(k); }
+// Draggable column ORDER (Steve rule: move columns around). Keys in display order;
+// auto-discovered/new keys are appended, stale keys pruned. Persisted per browser.
+let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('brokerColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
+function syncColOrder(){
+ const keys=COLS.map(c=>c.k);
+ COLORDER=COLORDER.filter(k=>keys.includes(k)); // prune removed
+ keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); }); // append new
+}
+function orderedCols(){ syncColOrder(); return COLORDER.map(k=>COLS.find(c=>c.k===k)).filter(Boolean); }
+function visCols(){ return orderedCols().filter(c=>colVis(c.k)); }
function saveCols(){ try{localStorage.setItem('brokerCols',JSON.stringify(VISCOL));}catch(e){} }
+function saveColOrder(){ try{localStorage.setItem('brokerColOrder',JSON.stringify(COLORDER));}catch(e){} }
+// When the visible column SET or ORDER changes, hand the table back to col-resize.js
+// so it rebuilds its <colgroup> + drag handles for the current columns.
+function reinitResize(){
+ const tbl=document.querySelector('#tableView table.g'); if(!tbl) return;
+ const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
+ delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
+ if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
+}
function tog(set,v){ set.has(v)?set.delete(v):set.add(v); }
function autoCols(){
const known=new Set(COLS.map(c=>c.k)); const skip=new Set(['id']);
@@ -237,7 +264,15 @@ function cell(r,c){
}
function filtered(){
let rows=DATA.filter(passFacets);
- if(q){ const t=q.toLowerCase(); rows=rows.filter(r=>COLS.some(c=>{const v=r[c.k];return v!=null&&String(v).toLowerCase().includes(t);})); }
+ if(q){
+ // Search ALL data (Steve rule 2026-07-31): every field the API returned on a row —
+ // not just the defined/visible columns — incl. nested objects. Space = AND.
+ const terms=q.toLowerCase().split(/\s+/).filter(Boolean);
+ rows=rows.filter(r=>{
+ const hay=Object.keys(r).filter(k=>k!=='__hay').map(k=>{const v=r[k];return v==null?'':(typeof v==='object'?JSON.stringify(v):String(v));}).join(' ').toLowerCase();
+ return terms.every(t=>hay.includes(t));
+ });
+ }
const c=COLS.find(x=>x.k===sortKey)||COLS[0];
rows=rows.slice().sort((a,b)=>{
let x=a[c.k], y=b[c.k];
@@ -250,10 +285,12 @@ function filtered(){
function render(){
const rows=filtered(); const cols=visCols();
$('#count').textContent=`${rows.length} of ${DATA.length} brokers/agents`;
- $('#thead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
+ $('#thead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" draggable="true" title="Drag to reorder · click to sort" class="dragcol${sortKey===c.k?(sortDir>0?' asc':' desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
if(view==='table'){
$('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
$('#tbody').innerHTML=rows.map(r=>`<tr class="brow" data-id="${r.id}" data-name="${esc(r.name)}" style="cursor:pointer">`+cols.map(c=>{const cls=(c.t==='n')?' class="n"':'';return `<td${cls}>${cell(r,c)}</td>`;}).join('')+'</tr>').join('');
+ const csig=cols.map(c=>c.k).join(',');
+ if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
} else {
$('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
const skipGrid=new Set(['name','firm','agent_type','phone','email','website','linkedin','title']);
@@ -290,6 +327,27 @@ document.addEventListener('click',e=>{ const fm=e.target.closest('.findmail'); i
$('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='listings'||k==='total_assets'||k==='created_at')?-1:1;}
localStorage.setItem('brokSortKey',sortKey);localStorage.setItem('brokSortDir',sortDir);if(window.__syncSortSel)window.__syncSortSel();render();});
+// ── drag-to-reorder columns (Steve rule 2026-07-31: move columns around) ──
+let dragKey=null;
+$('#thead').addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) return;
+ if(e.target.closest('.cr-handle')){ e.preventDefault(); return; } // that grab is a resize, not a move
+ dragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',dragKey);}catch(_){}
+ th.classList.add('dragging'); });
+$('#thead').addEventListener('dragover',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th||th.dataset.k===dragKey) return;
+ e.preventDefault(); e.dataTransfer.dropEffect='move';
+ const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+ th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
+$('#thead').addEventListener('dragleave',e=>{ const th=e.target.closest('th'); if(th) th.classList.remove('dropL','dropR'); });
+$('#thead').addEventListener('drop',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th) return; e.preventDefault();
+ const tgt=th.dataset.k; if(tgt!==dragKey){
+ const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+ syncColOrder(); const from=COLORDER.indexOf(dragKey); if(from>=0){
+ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,dragKey);
+ saveColOrder(); lastColSig=''; render();
+ }
+ }
+ dragKey=null; });
+$('#thead').addEventListener('dragend',()=>{ document.querySelectorAll('#thead th.dragging,#thead th.dropL,#thead th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); dragKey=null; });
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c)=>{let v=r[c.k]; v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
const csv=cols.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>cols.map(c=>cv(r,c)).join(',')).join('\n');
@@ -332,6 +390,10 @@ $('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c
$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
$('#fAll').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=true);saveCols();buildFieldToggles();render();});
$('#fNone').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=false);saveCols();buildFieldToggles();render();});
+// Reset the whole table layout back to the tight default: visibility, order, widths.
+$('#fReset').addEventListener('click',()=>{ VISCOL={}; saveCols(); COLORDER=[]; saveColOrder();
+ try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e){}
+ lastColSig=''; buildFieldToggles(); render(); });
// collapsible rail tabs — all collapsed on load
const railEl=document.querySelector('.rail');
if(railEl){ railEl.addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('rsec')) h.parentElement.classList.toggle('collapsed'); });
← 73d873b map: CRCP data by B2B-legality — assessor/FHA/DRE/Census + a
·
back to Commercialrealestate
·
B2B: assumable-loan lead viewer (/fha-leads.html, 100% HUD p ea76efd →