← back to Animals

public/js/sort-table.js

68 lines

// Tiny dep-free sortable table. Add data-sortable to a <table>.
// Click a <th> to sort by that column. Click again to reverse.
// Numeric vs. string detected from the column's data; null/empty sort last.
(function() {
  'use strict';
  function detectKind(cells) {
    let n = 0, total = 0;
    for (const v of cells) {
      const t = (v ?? '').toString().trim();
      if (!t) continue;
      total++;
      if (/^-?\$?\d[\d,.]*\d*%?$/.test(t)) n++;
    }
    return total > 0 && n / total >= 0.7 ? 'num' : 'str';
  }
  function toNum(s) {
    const cleaned = (s ?? '').toString().replace(/[$,%\s]/g, '');
    const n = parseFloat(cleaned);
    return isNaN(n) ? null : n;
  }
  function init(table) {
    if (table.dataset.sortableInit) return;
    table.dataset.sortableInit = '1';
    const ths = table.querySelectorAll('thead th');
    const tbody = table.querySelector('tbody');
    if (!ths.length || !tbody) return;
    ths.forEach((th, idx) => {
      th.style.cursor = 'pointer';
      th.style.userSelect = 'none';
      const arrow = document.createElement('span');
      arrow.className = 'sort-arrow';
      arrow.style.cssText = 'opacity:.4;margin-left:.4em;font-size:.85em';
      arrow.textContent = '↕';
      th.appendChild(arrow);
      th.addEventListener('click', () => {
        const rows = Array.from(tbody.querySelectorAll('tr'));
        const cells = rows.map(r => (r.children[idx]?.textContent || '').trim());
        const kind = th.dataset.sort || detectKind(cells);
        const cur = th.dataset.dir === 'asc' ? 'desc' : 'asc';
        ths.forEach(o => { o.dataset.dir = ''; const a = o.querySelector('.sort-arrow'); if (a) { a.textContent = '↕'; a.style.opacity = .4; } });
        th.dataset.dir = cur;
        arrow.textContent = cur === 'asc' ? '▲' : '▼';
        arrow.style.opacity = 1;
        rows.sort((a, b) => {
          let av = (a.children[idx]?.textContent || '').trim();
          let bv = (b.children[idx]?.textContent || '').trim();
          if (kind === 'num') {
            const na = toNum(av), nb = toNum(bv);
            if (na === null && nb === null) return 0;
            if (na === null) return 1;          // empties sort last
            if (nb === null) return -1;
            return cur === 'asc' ? na - nb : nb - na;
          }
          av = av.toLowerCase(); bv = bv.toLowerCase();
          if (!av && !bv) return 0;
          if (!av) return 1;
          if (!bv) return -1;
          return cur === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
        });
        rows.forEach(r => tbody.appendChild(r));
      });
    });
  }
  function bootstrap() { document.querySelectorAll('table[data-sortable]').forEach(init); }
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap);
  else bootstrap();
})();