← back to Commercialrealestate
column-manager: add external-state + footer/hint opts; adopt in mls.html (TK-10120)
3b9663c458e44d1a8ae134d5b0f343c24a85ee28 · 2026-08-01 22:04:50 -0700 · Steve Abrams
column-manager.js: new opts.state (visible/setVisible/order/setOrder/reset) makes
the panel a pure VIEW over a host page that already owns column state, plus
opts.footer/opts.hint toggles. Both internal + external modes verified (Playwright).
mls.html: reference adoption — the flat #fFields toggle list is replaced by the
shared column-manager panel (grouped + in-panel search + drag-reorder), wired as a
view over mls's existing VISCOL/COLORDER so render/thead-reorder/resize/sort/density
all keep working unchanged. Removed the now-duplicate #fFields change handler;
refresh the panel after a thead reorder; graceful fallback to the old flat list if
the module hasn't loaded.
Verified live (:9943, real-mouse): panel renders grouped+searchable (53 cols);
toggling re-renders the grid (8->9 th); sort works; resize works (267->363); Reset
restores default; 0 console errors. Follow-up: the other ~18 grid pages are the
same 4-edit swap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/column-manager.jsM public/mls.html
Diff
commit 3b9663c458e44d1a8ae134d5b0f343c24a85ee28
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 22:04:50 2026 -0700
column-manager: add external-state + footer/hint opts; adopt in mls.html (TK-10120)
column-manager.js: new opts.state (visible/setVisible/order/setOrder/reset) makes
the panel a pure VIEW over a host page that already owns column state, plus
opts.footer/opts.hint toggles. Both internal + external modes verified (Playwright).
mls.html: reference adoption — the flat #fFields toggle list is replaced by the
shared column-manager panel (grouped + in-panel search + drag-reorder), wired as a
view over mls's existing VISCOL/COLORDER so render/thead-reorder/resize/sort/density
all keep working unchanged. Removed the now-duplicate #fFields change handler;
refresh the panel after a thead reorder; graceful fallback to the old flat list if
the module hasn't loaded.
Verified live (:9943, real-mouse): panel renders grouped+searchable (53 cols);
toggling re-renders the grid (8->9 th); sort works; resize works (267->363); Reset
restores default; 0 console errors. Follow-up: the other ~18 grid pages are the
same 4-edit swap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/column-manager.js | 62 ++++++++++++++++++++++++++++--------------------
public/mls.html | 22 ++++++++++++++---
2 files changed, 55 insertions(+), 29 deletions(-)
diff --git a/public/column-manager.js b/public/column-manager.js
index bb7f6e6..b5606d7 100644
--- a/public/column-manager.js
+++ b/public/column-manager.js
@@ -51,19 +51,28 @@
var onDensity = opts.onDensity || null;
var dens = opts.density || null;
- var VIS = ls(NS + ':vis', {}); // {key: bool} overrides
- var ORDER = ls(NS + ':order', []); // ordered keys (persisted)
-
- function colVis(k) { return (k in VIS) ? !!VIS[k] : DEFAULT_ON.has(k); }
- function saveVis() { lsSet(NS + ':vis', VIS); }
- function saveOrder() { lsSet(NS + ':order', ORDER); }
-
- function syncOrder() {
+ // EXTERNAL-STATE MODE: pass opts.state to make this panel a pure VIEW over a host
+ // page that already owns column visibility/order (e.g. mls.html's VISCOL/COLORDER).
+ // Hooks: visible(k)->bool · setVisible(k,v) · order()->[keys] · setOrder([keys]) · reset().
+ // Omit opts.state and the panel owns its own localStorage-backed state.
+ var ext = opts.state || null;
+ var VIS = ext ? null : ls(NS + ':vis', {}); // internal-mode only
+ var ORDER = ext ? null : ls(NS + ':order', []);
+
+ function colVis(k) { return ext ? !!ext.visible(k) : ((k in VIS) ? !!VIS[k] : DEFAULT_ON.has(k)); }
+ function setColVis(k, v) { if (ext) { ext.setVisible(k, v); } else { VIS[k] = v; lsSet(NS + ':vis', VIS); } }
+ function doReset() { if (ext) { ext.reset(); } else { VIS = {}; ORDER = []; lsSet(NS + ':vis', VIS); lsSet(NS + ':order', ORDER); } }
+
+ function getOrder() {
+ if (ext) return ext.order().slice();
var keys = COLS.map(function (c) { return c.k; });
- ORDER = ORDER.filter(function (k) { return keys.indexOf(k) >= 0; }); // prune removed
- keys.forEach(function (k) { if (ORDER.indexOf(k) < 0) ORDER.push(k); }); // append new
+ ORDER = ORDER.filter(function (k) { return keys.indexOf(k) >= 0; }); // prune removed
+ keys.forEach(function (k) { if (ORDER.indexOf(k) < 0) ORDER.push(k); }); // append new
+ return ORDER.slice();
}
- function orderedCols() { syncOrder(); return ORDER.map(function (k) { return COLS.find(function (c) { return c.k === k; }); }).filter(Boolean); }
+ function setOrder(arr) { if (ext) { ext.setOrder(arr.slice()); } else { ORDER = arr.slice(); lsSet(NS + ':order', ORDER); } }
+
+ function orderedCols() { return getOrder().map(function (k) { return COLS.find(function (c) { return c.k === k; }); }).filter(Boolean); }
function visibleCols() { return orderedCols().filter(function (c) { return colVis(c.k); }); }
function visKeys() { return visibleCols().map(function (c) { return c.k; }); }
@@ -73,7 +82,6 @@
// ---- render the panel ----
function build() {
- syncOrder();
var groups = {};
orderedCols().forEach(function (c) { (groups[c.g || 'Other'] = groups[c.g || 'Other'] || []).push(c); });
@@ -94,11 +102,13 @@
});
});
html += '</div>';
- html += '<div class="cm-foot">' +
- '<button class="cm-btn" data-act="all">All</button>' +
- '<button class="cm-btn" data-act="none">None</button>' +
- '<button class="cm-btn" data-act="reset" title="Reset columns to the tight default">↺ Reset</button></div>';
- html += '<div class="cm-hint">Drag a column header to reorder · drag its right edge to resize · click to sort</div>';
+ if (opts.footer !== false) {
+ html += '<div class="cm-foot">' +
+ '<button class="cm-btn" data-act="all">All</button>' +
+ '<button class="cm-btn" data-act="none">None</button>' +
+ '<button class="cm-btn" data-act="reset" title="Reset columns to the tight default">↺ Reset</button></div>';
+ }
+ if (opts.hint !== false) html += '<div class="cm-hint">Drag a column header to reorder · drag its right edge to resize · click to sort</div>';
html += '</div>';
mount.innerHTML = html;
wire();
@@ -109,16 +119,16 @@
mount.addEventListener('change', function (e) {
var cb = e.target.closest && e.target.closest('input[data-ck]');
if (!cb) return;
- VIS[cb.dataset.ck] = cb.checked; saveVis(); onChange(visKeys(), ORDER.slice());
+ setColVis(cb.dataset.ck, cb.checked); onChange(visKeys(), getOrder());
});
// footer buttons
mount.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('.cm-btn'); if (!b) return;
var act = b.dataset.act;
- if (act === 'all') { COLS.forEach(function (c) { VIS[c.k] = true; }); saveVis(); build(); }
- else if (act === 'none') { COLS.forEach(function (c) { VIS[c.k] = false; }); saveVis(); build(); }
- else if (act === 'reset') { VIS = {}; ORDER = []; saveVis(); saveOrder(); build(); }
- onChange(visKeys(), ORDER.slice());
+ if (act === 'all') { COLS.forEach(function (c) { setColVis(c.k, true); }); build(); }
+ else if (act === 'none') { COLS.forEach(function (c) { setColVis(c.k, false); }); build(); }
+ else if (act === 'reset') { doReset(); build(); }
+ onChange(visKeys(), getOrder());
});
// search filter
var q = mount.querySelector('.cm-search');
@@ -166,11 +176,11 @@
if (!dragK) return; var row = e.target.closest('.cm-row'); if (!row) return; e.preventDefault();
var tgt = row.dataset.k;
if (tgt !== dragK) {
- syncOrder(); var from = ORDER.indexOf(dragK);
+ var ord = getOrder(); var from = ord.indexOf(dragK);
if (from >= 0) {
var r = row.getBoundingClientRect(), after = (e.clientY - r.top) > r.height / 2;
- ORDER.splice(from, 1); var to = ORDER.indexOf(tgt); if (after) to += 1; ORDER.splice(to, 0, dragK);
- saveOrder(); build(); onChange(visKeys(), ORDER.slice());
+ ord.splice(from, 1); var to = ord.indexOf(tgt); if (after) to += 1; ord.splice(to, 0, dragK);
+ setOrder(ord); build(); onChange(visKeys(), getOrder());
}
}
dragK = null;
@@ -186,7 +196,7 @@
return {
visibleCols: visibleCols,
- order: function () { syncOrder(); return ORDER.slice(); },
+ order: function () { return getOrder(); },
density: function () { return densVal; },
refresh: build, // call after COLS changes (auto-discovered columns)
setCols: function (next) { COLS = next || []; build(); }
diff --git a/public/mls.html b/public/mls.html
index 6764ea5..331057b 100644
--- a/public/mls.html
+++ b/public/mls.html
@@ -294,7 +294,22 @@ function buildRail(){
$('#fType').innerHTML=chip(tc,'ty'); $('#fStatus').innerHTML=chip(sc,'st'); $('#fCity').innerHTML=chip(cc,'cy',16); $('#fSource').innerHTML=chip(oc,'so'); $('#fFirm').innerHTML=chip(fc,'fm',24);
buildFieldToggles();
}
-function buildFieldToggles(){ $('#fFields').innerHTML=COLS.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join(''); }
+// Column show/hide + in-panel search & drag-reorder via the shared column-manager.js
+// drop-in (TK-10120), running as a VIEW over mls's own VISCOL/COLORDER state.
+let __cm=null;
+function buildFieldToggles(){
+ if(!window.ColumnManager){ // fallback if the module hasn't loaded (keeps the page usable)
+ $('#fFields').innerHTML=COLS.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join(''); return; }
+ const groupOf=k=>DEFAULT_ON.has(k)?'Shown by default':'More columns';
+ const cols=COLS.map(c=>({k:c.k,l:c.l,g:groupOf(c.k)}));
+ if(!__cm){
+ __cm=ColumnManager.init({ mount:'#fFields', footer:false, hint:false, cols,
+ state:{ visible:k=>colVis(k), setVisible:(k,v)=>{VISCOL[k]=v;saveCols();},
+ order:()=>{syncColOrder();return COLORDER.slice();}, setOrder:a=>{COLORDER=a;saveColOrder();},
+ reset:()=>{VISCOL={};saveCols();COLORDER=[];saveColOrder();} },
+ onChange:()=>render() });
+ } else { __cm.setCols(cols); }
+}
function isNonQM(r){ const t=(r.type||'').toLowerCase();
if(r.type==='Condo' && r.warrantable_status && r.warrantable_status!=='fha_approved') return true;
if(/multi/.test(t) && r.units>=1 && r.units<=9) return true;
@@ -416,7 +431,7 @@ $('#thead').addEventListener('drop',e=>{ if(!dragKey) return; const th=e.target.
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();
+ saveColOrder(); lastColSig=''; render(); if(__cm)__cm.refresh();
}
}
dragKey=null; });
@@ -484,7 +499,7 @@ $('#fSource').addEventListener('click',e=>{const c=e.target.closest('.chip');if(
$('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');render();});
$('#pMin').addEventListener('input',()=>{F.pmin=$('#pMin').value?+$('#pMin').value:null;render();});
$('#pMax').addEventListener('input',()=>{F.pmax=$('#pMax').value?+$('#pMax').value:null;render();});
-$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
+// (#fFields change is handled inside column-manager.js now — its onChange calls 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.
@@ -542,6 +557,7 @@ fetch('/data/ranked.json').then(r=>r.json()).then(async d=>{ DATA=(d.ranked||d)|
autoCols(); buildRail(); render(); })
.catch(()=>{ $('#count').textContent='failed to load /data/ranked.json'; });
</script>
+ <script src="/column-manager.js" defer></script>
<script src="/col-resize.js" defer></script>
</body>
</html>
← 6710021 Add reusable column-manager.js drop-in (DTD verdict A — dock
·
back to Commercialrealestate
·
Adopt column-manager.js in gov-agents + sales (TK-10120 swee a01bf5e →