← 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, '&lt;') + '</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, '&lt;') + '</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);
})();