← back to Commercialrealestate
public/sort-bar.js
82 lines
/* sort-bar.js — surface ALL sort options as a chip bar in the LEFT PANEL (Steve 2026-08-03:
* "provide all sort options in a bar … put on left panel"). Mirrors the page's existing sort
* <select> (#csort / #sortsel / #sort) into a .rail section of chips; clicking a chip drives the
* same select (so every page's existing sort logic is reused, including dynamic column lists).
* Falls back to an inline bar where a page has no left rail. Self-contained. */
(function () {
'use strict';
if (window.__sortBarInit) return; window.__sortBarInit = true;
var css = document.createElement('style');
css.textContent = [
'.sb-sec .sb-h{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut,#8A93A2);margin:0 0 9px;font-weight:600;}',
'.sb-chips{display:flex;flex-wrap:wrap;gap:5px;}',
'.sbchip{background:var(--card,#141925);color:var(--mut,#8b949e);border:1px solid var(--line,#232a38);border-radius:7px;padding:4px 9px;font-size:12px;cursor:pointer;line-height:1.2;}',
'.sbchip:hover{color:var(--ink,#e6edf3);border-color:var(--mut,#8A93A2);}',
'.sbchip.on{border-color:var(--blue,#58a6ff);color:var(--blue,#58a6ff);background:rgba(88,166,255,.14);font-weight:600;}',
'span.sb-sec{display:inline-flex;align-items:center;gap:5px;flex-wrap:wrap}',
'.sb-grp{flex-basis:100%;width:100%;font-size:9.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--mut,#8A93A2);opacity:.8;margin:7px 0 1px;font-weight:600}'
].join('');
document.head.appendChild(css);
function sel() { return document.querySelector('#csort, #sortsel, #sort'); }
function ensureChips(s) {
var host = document.getElementById('sb-host');
if (host) return host.querySelector('.sb-chips');
var rail = document.querySelector('.rail');
var chips = document.createElement('div'); chips.className = 'sb-chips';
if (rail) {
var sec = document.createElement('div'); sec.className = 'rsec sb-sec'; sec.id = 'sb-host';
sec.innerHTML = '<div class="sb-h">Sort</div>'; sec.appendChild(chips);
rail.insertBefore(sec, rail.firstChild); // top of the left panel
} else {
var span = document.createElement('span'); span.className = 'sb-sec'; span.id = 'sb-host';
var lbl = document.createElement('span'); lbl.className = 'sb-h'; lbl.textContent = 'Sort'; lbl.style.margin = '0 4px 0 0';
span.appendChild(lbl); span.appendChild(chips);
s.parentNode.insertBefore(span, s); // inline bar where there's no rail
}
s.style.display = 'none'; // the bar replaces the dropdown
return chips;
}
function build() {
var s = sel(); if (!s || s.tagName !== 'SELECT' || !s.options.length) return;
var chips = ensureChips(s);
// Preserve <optgroup> labels (dynamic column-driven selects have 20+ options across groups) so
// the rail shows "Financial / Location / …" headers instead of a wall of unlabeled chips (Cody gate).
var html = '', lastGrp = null;
Array.prototype.forEach.call(s.options, function (o, i) {
var grp = (o.parentElement && o.parentElement.tagName === 'OPTGROUP') ? (o.parentElement.label || '') : '';
if (grp !== lastGrp) { if (grp) html += '<span class="sb-grp">' + grp.replace(/</g, '<') + '</span>'; lastGrp = grp; }
var label = (o.textContent || '').replace(/^Sort:\s*/i, '').trim() || ('Option ' + (i + 1));
html += '<button class="sbchip' + (i === s.selectedIndex ? ' on' : '') + '" data-i="' + i + '">' + label.replace(/</g, '<') + '</button>';
});
chips.innerHTML = html;
}
document.addEventListener('click', function (e) {
var c = e.target.closest && e.target.closest('#sb-host .sbchip'); if (!c) return;
var s = sel(); if (!s) return;
s.selectedIndex = +c.dataset.i;
s.dispatchEvent(new Event('change', { bubbles: true }));
c.parentNode.querySelectorAll('.sbchip').forEach(function (x) { x.classList.toggle('on', x === c); });
});
function syncActive() {
var s = sel(), chips = document.querySelector('#sb-host .sb-chips'); if (!s || !chips) return;
Array.prototype.forEach.call(chips.children, function (x) { x.classList.toggle('on', +x.dataset.i === s.selectedIndex); });
}
function boot() {
build();
var s = sel();
if (s) {
new MutationObserver(build).observe(s, { childList: true }); // dynamic/column-driven option lists
s.addEventListener('change', syncActive); // keep active chip in sync (e.g. header-click sort)
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { setTimeout(boot, 400); });
else setTimeout(boot, 400);
})();