[object Object]

← 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

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 →