← back to Commercialrealestate
col-resize: add click-to-sort headers (asc/desc/reset, numeric-aware, blanks last)
d433ef8a235c86309e19fcf1e2acc718199c9741 · 2026-07-12 11:06:56 -0700 · Steve
Files touched
Diff
commit d433ef8a235c86309e19fcf1e2acc718199c9741
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Jul 12 11:06:56 2026 -0700
col-resize: add click-to-sort headers (asc/desc/reset, numeric-aware, blanks last)
---
public/col-resize.js | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 91 insertions(+), 1 deletion(-)
diff --git a/public/col-resize.js b/public/col-resize.js
index 4cd3e22..66ba0c1 100644
--- a/public/col-resize.js
+++ b/public/col-resize.js
@@ -20,6 +20,7 @@
var MIN = 40; // px, smallest a column may shrink to
var LS = 'cr:'; // localStorage key prefix
var HANDLE_W = 8; // px hit-area of the drag handle
+ var lastResizeUp = 0; // timestamp of the last resize drag end (sort ignores that click)
// ---- persistence ----------------------------------------------------------
function tableSig(headers) {
@@ -102,6 +103,91 @@
th.appendChild(handle);
wireHandle(handle, table, cols, headers, sig, idx);
});
+
+ addSorting(table, headers);
+ }
+
+ // ---- click-a-header-to-sort (asc → desc → original) -----------------------
+ function addSorting(table, headers) {
+ if (table.hasAttribute('data-no-sort')) return;
+ var headerRow = headers[0].parentNode;
+
+ headers.forEach(function (th, idx) {
+ if (th.querySelector('.cr-sort')) return; // already wired
+ th.classList.add('cr-sortable');
+ var ind = document.createElement('span');
+ ind.className = 'cr-sort';
+ th.appendChild(ind);
+
+ th.addEventListener('click', function (e) {
+ if (e.target.closest && e.target.closest('.cr-handle')) return; // it was a resize grab
+ if (Date.now() - lastResizeUp < 300) return; // a drag just ended
+ var dir = th.dataset.crSort === 'asc' ? 'desc' : th.dataset.crSort === 'desc' ? 'none' : 'asc';
+ sortBy(table, headers, headerRow, idx, dir);
+ headers.forEach(function (o) { o.dataset.crSort = ''; o.querySelector('.cr-sort').textContent = ''; });
+ th.dataset.crSort = dir;
+ th.querySelector('.cr-sort').textContent = dir === 'asc' ? ' ▲' : dir === 'desc' ? ' ▼' : '';
+ });
+ });
+ }
+
+ function bodyRows(table, headerRow) {
+ var rows = [];
+ for (var b = 0; b < table.tBodies.length; b++) {
+ var tb = table.tBodies[b];
+ for (var i = 0; i < tb.rows.length; i++) if (tb.rows[i] !== headerRow) rows.push(tb.rows[i]);
+ }
+ return rows;
+ }
+
+ function cellText(row, idx) {
+ var c = row.cells[idx];
+ return c ? (c.textContent || '').trim() : '';
+ }
+ // Parse "$1,234.56", "45%", "-12" → number; non-numeric → NaN.
+ function num(s) {
+ if (!s) return NaN;
+ var n = parseFloat(s.replace(/[^0-9.\-]/g, ''));
+ return isNaN(n) ? NaN : n;
+ }
+ function isNumericColumn(rows, idx) {
+ var seen = 0;
+ for (var i = 0; i < rows.length && seen < 30; i++) {
+ var t = cellText(rows[i], idx);
+ if (!t) continue;
+ seen++;
+ if (isNaN(num(t))) return false;
+ }
+ return seen > 0;
+ }
+
+ function sortBy(table, headers, headerRow, idx, dir) {
+ var rows = bodyRows(table, headerRow);
+ if (!rows.length) return;
+ if (!table.__crOrig) table.__crOrig = rows.slice(); // remember natural order once
+
+ if (dir === 'none') {
+ var order = table.__crOrig.filter(function (r) { return r.parentNode; });
+ var dest0 = rows[0].parentNode;
+ order.forEach(function (r) { dest0.appendChild(r); });
+ return;
+ }
+
+ var numeric = isNumericColumn(rows, idx);
+ var mul = dir === 'asc' ? 1 : -1;
+ // Decorate → sort → undecorate keeps blanks last and the sort stable.
+ rows.map(function (r, i) { return { r: r, i: i, t: cellText(r, idx) }; })
+ .sort(function (a, b) {
+ var ae = a.t === '', be = b.t === '';
+ if (ae && be) return a.i - b.i;
+ if (ae) return 1; // blanks always sink
+ if (be) return -1;
+ var cmp;
+ if (numeric) cmp = num(a.t) - num(b.t);
+ else cmp = a.t.localeCompare(b.t, undefined, { numeric: true, sensitivity: 'base' });
+ return cmp !== 0 ? cmp * mul : a.i - b.i; // stable tiebreak
+ })
+ .forEach(function (o) { o.r.parentNode.appendChild(o.r); });
}
function wireHandle(handle, table, cols, headers, sig, idx) {
@@ -134,6 +220,7 @@
window.removeEventListener('pointerup', up);
window.removeEventListener('touchmove', move);
window.removeEventListener('touchend', up);
+ lastResizeUp = Date.now();
save(sig, idx, cols[idx].getBoundingClientRect().width);
}
// Double-click → auto-fit this column to its content.
@@ -168,7 +255,10 @@
+ '.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;}'
+ 'body.cr-dragging{cursor:col-resize!important;user-select:none!important;}'
- + 'body.cr-dragging *{cursor:col-resize!important;}';
+ + 'body.cr-dragging *{cursor:col-resize!important;}'
+ + 'th.cr-sortable{cursor:pointer;}'
+ + 'th.cr-sortable:hover{background:rgba(127,127,127,.12);}'
+ + '.cr-sort{font-size:.85em;opacity:.8;white-space:nowrap;}';
var s = document.createElement('style');
s.id = 'cr-style';
s.textContent = css;
← dd4abf6 Add spreadsheet-style draggable column resizing (col-resize.
·
back to Commercialrealestate
·
CRCP email-alerts: fix George send (/api/send + body field) 035640a →