← 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();
})();