[object Object]

← back to Nationalrealestate

Fix column resize on markets/brokers grids (adjust column not working)

3dfd37e1c113a60adf37e62c7aef414a1c008950 · 2026-07-31 11:38:10 -0700 · Steve Abrams

Propagate the fixed col-resize.js from CRCP (was the old pre-fix drop-in with
none of the width-application / hidden-table / handle-position / draggable-guard
fixes) AND add the missing per-engine wiring in crcp-grid.js:

- col-resize.js: full parity with commercialrealestate's fixed copy
  (syncTableWidth pins table width so fixed-layout honors <col> widths, hidden-
  table bail, reattach, handle right:0, draggable guard, startW-from-style).
- crcp-grid.js render(): deterministic ColResize.refresh() after the innerHTML
  table rebuild instead of racing the async MutationObserver.
- crcp-grid.js sort handler: ignore clicks on .cr-handle / during cr-dragging,
  so grabbing the resize handle no longer fires a sort + full re-render mid-drag
  (that was collapsing resize to a single 14px step / 0).

Verified real-mouse (Playwright) on the running :9913 app: markets 59->179,
brokers 112->232 (was 59->59 / 112->112 broken); sort still works; 0 errors.
Local only — Kamatera deploy is gated. yoloforever cycle 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 3dfd37e1c113a60adf37e62c7aef414a1c008950
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 31 11:38:10 2026 -0700

    Fix column resize on markets/brokers grids (adjust column not working)
    
    Propagate the fixed col-resize.js from CRCP (was the old pre-fix drop-in with
    none of the width-application / hidden-table / handle-position / draggable-guard
    fixes) AND add the missing per-engine wiring in crcp-grid.js:
    
    - col-resize.js: full parity with commercialrealestate's fixed copy
      (syncTableWidth pins table width so fixed-layout honors <col> widths, hidden-
      table bail, reattach, handle right:0, draggable guard, startW-from-style).
    - crcp-grid.js render(): deterministic ColResize.refresh() after the innerHTML
      table rebuild instead of racing the async MutationObserver.
    - crcp-grid.js sort handler: ignore clicks on .cr-handle / during cr-dragging,
      so grabbing the resize handle no longer fires a sort + full re-render mid-drag
      (that was collapsing resize to a single 14px step / 0).
    
    Verified real-mouse (Playwright) on the running :9913 app: markets 59->179,
    brokers 112->232 (was 59->59 / 112->112 broken); sort still works; 0 errors.
    Local only — Kamatera deploy is gated. yoloforever cycle 2.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/col-resize.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++---
 public/crcp-grid.js  |  8 +++++
 2 files changed, 101 insertions(+), 5 deletions(-)

diff --git a/public/col-resize.js b/public/col-resize.js
index 66ba0c1..a322990 100644
--- a/public/col-resize.js
+++ b/public/col-resize.js
@@ -59,6 +59,19 @@
     return cg;
   }
 
+  // ---- keep the table's own width == sum of column widths -------------------
+  // CRITICAL for table-layout:fixed: with width:auto the browser shrink-to-fits
+  // the table to its container and REDISTRIBUTES column widths, silently ignoring
+  // any <col> width we set (so resizing appears to do nothing). Pinning the table
+  // to the exact sum of the column widths makes the <col> widths authoritative and
+  // lets the table grow past its container into the overflow-x:auto scroll wrapper.
+  function syncTableWidth(table, cols) {
+    if (table.getBoundingClientRect().width === 0) return;   // hidden — never pin garbage widths
+    var sum = 0;
+    for (var i = 0; i < cols.length; i++) sum += parseFloat(cols[i].style.width) || 0;
+    if (sum > 0) table.style.width = Math.round(sum) + 'px';
+  }
+
   // ---- wrap table in a horizontal-scroll container --------------------------
   function ensureScroll(table) {
     var p = table.parentElement;
@@ -75,6 +88,11 @@
     if (table.dataset.crDone === '1' || table.hasAttribute('data-no-resize')) return;
     var headers = headerCells(table);
     if (headers.length < 2) return;              // nothing meaningful to resize
+    // Don't enhance a HIDDEN table: its cells measure 0 → clamp to MIN, and
+    // syncTableWidth would then pin the table to MIN×ncols (e.g. 240px), so it
+    // renders as a broken sliver when later shown. Leave it untouched (renders
+    // fine at its natural CSS width); it enhances once a scan sees it visible.
+    if (table.getBoundingClientRect().width === 0) return;
     table.dataset.crDone = '1';
 
     var sig = tableSig(headers);
@@ -85,14 +103,25 @@
     // browser stops redistributing. Saved widths override the snapshot.
     var measured = headers.map(function (h) { return Math.max(MIN, Math.round(h.getBoundingClientRect().width)); });
     table.style.tableLayout = 'fixed';
-    table.style.width = 'auto';
     for (var i = 0; i < headers.length; i++) {
       var saved = load(sig, i);
       cols[i].style.width = (saved != null ? Math.max(MIN, saved) : measured[i]) + 'px';
     }
+    syncTableWidth(table, cols);   // definite width so fixed-layout honors the <col> widths
 
     // Add a drag handle to each header cell.
+    attachHandles(table, headers, cols, sig);
+
+    addSorting(table, headers);
+  }
+
+  // ---- (re)attach a drag handle to any header cell missing one ---------------
+  // Idempotent: an existing .cr-handle short-circuits, so this is safe to call
+  // repeatedly. Used both by enhance() and by reattach() after a host-app
+  // <thead> rebuild wipes the handle spans.
+  function attachHandles(table, headers, cols, sig) {
     headers.forEach(function (th, idx) {
+      if (th.querySelector('.cr-handle')) return;
       var cs = getComputedStyle(th);
       if (cs.position === 'static') th.style.position = 'relative';
       var handle = document.createElement('span');
@@ -101,10 +130,50 @@
       handle.setAttribute('aria-orientation', 'vertical');
       handle.title = 'Drag to resize • double-click to auto-fit';
       th.appendChild(handle);
+      guardDraggableAncestor(handle, th);
       wireHandle(handle, table, cols, headers, sig, idx);
     });
+  }
 
-    addSorting(table, headers);
+  // ---- coexist with native column-reorder (draggable <th>) ------------------
+  // If a header cell (or an ancestor) is draggable="true" for HTML5 drag-reorder,
+  // a real mousedown on the resize handle starts a NATIVE drag, which suppresses
+  // the pointermove stream the resizer needs — so the edge won't resize. The drag
+  // decision is made at mousedown, so we must clear draggable BEFORE the press:
+  // disable it whenever the pointer is over the handle, restore it on leave.
+  function guardDraggableAncestor(handle, th) {
+    var anc = (th.closest && th.closest('[draggable="true"]')) || null;
+    if (!anc) return;
+    var restore = function () { anc.draggable = true; };
+    handle.addEventListener('mouseenter', function () { anc.draggable = false; });
+    handle.addEventListener('mouseleave', restore);
+    // Belt-and-suspenders for pointer paths that skip mouseenter (pen/touch):
+    handle.addEventListener('pointerdown', function () { anc.draggable = false; });
+    window.addEventListener('pointerup', restore);
+  }
+
+  // ---- reattach after the host app rebuilds <thead> innerHTML ----------------
+  // A single-page app that re-renders its own <thead> (sort / search / filter)
+  // destroys the handle spans but leaves the <colgroup> — so widths survive but
+  // resizing dies. This re-wires the handles onto the fresh <th>s WITHOUT
+  // touching the colgroup, preserving custom widths. Falls back to a full
+  // enhance() when the table was never enhanced or its column COUNT changed.
+  function reattach(table) {
+    if (!table) return;
+    var cg = table.querySelector('colgroup[data-cr]');
+    if (!cg) { enhance(table); return; }                 // never enhanced → full path
+    var headers = headerCells(table);
+    if (!headers.length || headers.length !== cg.children.length) {
+      cg.remove();                                        // column set changed → rebuild
+      delete table.dataset.crDone;
+      table.style.tableLayout = '';
+      table.style.width = '';
+      enhance(table);
+      return;
+    }
+    table.dataset.crDone = '1';
+    attachHandles(table, headers, cg.children, tableSig(headers));
+    syncTableWidth(table, cg.children);   // re-assert width in case a re-render cleared it
   }
 
   // ---- click-a-header-to-sort (asc → desc → original) -----------------------
@@ -196,7 +265,11 @@
     function down(e) {
       dragging = true;
       startX = (e.touches ? e.touches[0].clientX : e.clientX);
-      startW = cols[idx].getBoundingClientRect().width;
+      // Read the width we SET, not <col>.getBoundingClientRect() — that rect is
+      // unreliable for a <col> (Chromium returns stale/tiny values, same quirk as
+      // TK-10089). A bad startW + a zero-delta move (e.g. the mousedowns inside a
+      // double-click) would otherwise collapse the column to MIN (40px).
+      startW = parseFloat(cols[idx].style.width) || cols[idx].getBoundingClientRect().width;
       document.body.classList.add('cr-dragging');
       window.addEventListener('pointermove', move);
       window.addEventListener('pointerup', up);
@@ -210,6 +283,7 @@
       var x = (e.touches ? e.touches[0].clientX : e.clientX);
       var w = Math.max(MIN, startW + (x - startX));
       cols[idx].style.width = w + 'px';
+      syncTableWidth(table, cols);       // grow/shrink the table with the dragged column
       if (e.cancelable) e.preventDefault();
     }
     function up() {
@@ -221,7 +295,12 @@
       window.removeEventListener('touchmove', move);
       window.removeEventListener('touchend', up);
       lastResizeUp = Date.now();
-      save(sig, idx, cols[idx].getBoundingClientRect().width);
+      // Persist the width move() actually applied. A <col>'s getBoundingClientRect()
+      // does NOT reliably reflect a just-set style.width (Chromium returns a stale value),
+      // so saving that reverted resized columns to their old width on reload. Read the
+      // style we set; fall back to the header cell's rendered width (TK-10089).
+      var savedW = parseFloat(cols[idx].style.width) || headers[idx].getBoundingClientRect().width;
+      save(sig, idx, savedW);
     }
     // Double-click → auto-fit this column to its content.
     function autofit(e) {
@@ -230,12 +309,16 @@
       // Remember every column's current width, let this one go auto, measure, restore.
       var widths = [];
       for (var i = 0; i < cols.length; i++) widths[i] = cols[i].style.width;
+      var savedTableW = table.style.width;
       table.style.tableLayout = 'auto';
+      table.style.width = '';                 // release the pinned width so the column can size to content
       cols[idx].style.width = '';
       var natural = Math.max(MIN, Math.round(headers[idx].getBoundingClientRect().width));
       for (var j = 0; j < cols.length; j++) cols[j].style.width = widths[j];
       cols[idx].style.width = natural + 'px';
       table.style.tableLayout = 'fixed';
+      table.style.width = savedTableW;
+      syncTableWidth(table, cols);
       save(sig, idx, natural);
     }
 
@@ -250,7 +333,11 @@
     if (document.getElementById('cr-style')) return;
     var css = ''
       + '.cr-scroll{overflow-x:auto;max-width:100%;}'
-      + '.cr-handle{position:absolute;top:0;right:-' + (HANDLE_W / 2) + 'px;width:' + HANDLE_W + 'px;height:100%;'
+      // Handle sits INSIDE the column's right edge (right:0), not straddling it.
+      // Straddling (right:-N) pushes half the handle over the NEXT sticky <th>,
+      // which then paints on top (sibling stacking context) and steals the click —
+      // breaking resize entirely on draggable-column tables.
+      + '.cr-handle{position:absolute;top:0;right:0;width:' + HANDLE_W + 'px;height:100%;'
       +   'cursor:col-resize;user-select:none;touch-action:none;z-index:5;}'
       + '.cr-handle:hover::after,.cr-dragging .cr-handle::after{content:"";position:absolute;top:0;left:50%;'
       +   'width:2px;height:100%;transform:translateX(-50%);background:currentColor;opacity:.55;}'
@@ -294,6 +381,7 @@
 
   window.ColResize = {
     refresh: function () { scan(document); },
+    reattach: reattach,             // re-wire handles after a host-app thead rebuild
     reset: function () {                     // wipe all saved widths for this page
       try {
         Object.keys(localStorage).forEach(function (k) {
diff --git a/public/crcp-grid.js b/public/crcp-grid.js
index 71afff4..206b394 100644
--- a/public/crcp-grid.js
+++ b/public/crcp-grid.js
@@ -275,6 +275,10 @@
       if (sel) sel.value = SORT && SORT.k ? SORT.k + ':' + (SORT.dir < 0 ? 'desc' : 'asc') : '';
       const ab = $(O.mount + ' .cg-active');
       if (ab) ab.innerHTML = activeFilterHTML();
+      // render() rebuilds the whole table via innerHTML, so the previous
+      // col-resize enhancement is gone. Re-enhance deterministically instead of
+      // waiting on the async MutationObserver (which races the first interaction).
+      if (window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
       syncURL();
     }
 
@@ -328,6 +332,10 @@
         }
         const af = e.target.closest('.cg-af, .cg-af-clear');
         if (af) { clearFilter(af); return; }
+        // A click on the resize handle (or the tail of a resize drag) must NOT
+        // sort — otherwise grabbing the handle re-renders the whole table
+        // mid-gesture and the column never resizes. col-resize owns the handle.
+        if (e.target.closest('.cr-handle') || document.body.classList.contains('cr-dragging')) return;
         const th = e.target.closest('.cg-sortth');
         if (th && th.dataset.k) {
           const k = th.dataset.k;

← 06c2d3a Add recent_commercial_deals view — closed commercial deals f  ·  back to Nationalrealestate  ·  listings.html: load col-resize.js for parity with markets/br 498df4a →