/* Tabellensortierung per Spaltenkopf (Ausbaustufe 11). * * Jede Tabelle mit bekommt je Spaltenkopf mit Text einen Button * (⇅ unsortiert, ▲ aufsteigend, ▼ absteigend). Ausnahmen: th[data-nosort], * table[data-sort-mode="server"] (z.B. /buchungen, sortiert serverseitig). * Sortierwert: td[data-sort] (Zahl Punkt-dezimal / ISO-Datum) oder der * angezeigte Text (Datum TT.MM.JJJJ, deutscher Betrag 1.234,56, sonst Text). * Leere Werte und „–" stehen in beiden Richtungen am Ende. Zeilen mit * data-sort-follow (Bearbeiten-/Tilgungsplan-Zeilen) wandern mit ihrer * Vorzeile. Nur wird sortiert, bleibt unberührt. * Initialisierung über htmx.onLoad - greift auch bei nachgeladenen Fragmenten. */ (function () { 'use strict'; var DE_DATE = /^(\d{2})\.(\d{2})\.(\d{4})$/; var DE_NUMBER = /^-?(\d{1,3}(\.\d{3})+|\d+)(,\d+)?$/; var PLAIN_NUMBER = /^-?\d+(\.\d+)?$/; var ICON = { none: '⇅', asc: '▲', desc: '▼' }; var collator = new Intl.Collator('de', { sensitivity: 'base', numeric: true }); function sortKey(raw, fromDataSort) { var text = String(raw == null ? '' : raw).trim(); if (text === '' || text === '–' || text === '-') { return null; } if (fromDataSort && PLAIN_NUMBER.test(text)) { return { kind: 'number', value: parseFloat(text), text: text }; } var m = DE_DATE.exec(text); if (m) { return { kind: 'date', value: parseInt(m[3] + m[2] + m[1], 10), text: text }; } var num = text.replace(/\s*[€%]$/, '').replace(/\s/g, ''); if (DE_NUMBER.test(num)) { return { kind: 'number', value: parseFloat(num.replace(/\./g, '').replace(',', '.')), text: text }; } return { kind: 'text', value: text, text: text }; } function columnKind(keys) { var kind = null; for (var i = 0; i < keys.length; i++) { if (!keys[i]) { continue; } if (kind === null) { kind = keys[i].kind; } else if (kind !== keys[i].kind) { return 'text'; } } return kind || 'text'; } function sortItems(items, dir) { var kind = columnKind(items.map(function (it) { return it.key; })); var sign = dir === 'desc' ? -1 : 1; return items.slice().sort(function (a, b) { if (!a.key || !b.key) { if (a.key) { return -1; } if (b.key) { return 1; } return a.index - b.index; } var c = kind === 'text' ? collator.compare(a.key.text, b.key.text) : a.key.value - b.key.value; return c !== 0 ? sign * c : a.index - b.index; }); } function groupRows(rows, isFollow) { var groups = []; rows.forEach(function (row) { if (isFollow(row) && groups.length) { groups[groups.length - 1].push(row); } else { groups.push([row]); } }); return groups; } function sortTable(table, col, dir) { var tbody = table.tBodies[0]; if (!tbody) { return; } var groups = groupRows(Array.prototype.slice.call(tbody.rows), function (r) { return r.hasAttribute('data-sort-follow'); }); var items = groups.map(function (g, i) { var cell = g[0].cells[col]; var key = null; if (cell && cell.colSpan === 1) { key = cell.hasAttribute('data-sort') ? sortKey(cell.getAttribute('data-sort'), true) : sortKey(cell.textContent, false); } return { key: key, index: i, rows: g }; }); sortItems(items, dir).forEach(function (it) { it.rows.forEach(function (r) { tbody.appendChild(r); }); }); } function initTable(table) { if (table.hasAttribute('data-sort-init') || !table.tHead || !table.tHead.rows.length || table.getAttribute('data-sort-mode') === 'server') { return; } table.setAttribute('data-sort-init', ''); var doc = table.ownerDocument; Array.prototype.forEach.call(table.tHead.rows[0].cells, function (th) { var label = th.textContent.trim(); if (!label || th.hasAttribute('data-nosort')) { return; } var btn = doc.createElement('button'); btn.type = 'button'; btn.className = 'sort-btn'; btn.textContent = ICON.none; btn.setAttribute('aria-label', 'Nach ' + label + ' sortieren'); btn.addEventListener('click', function () { var dir = btn.getAttribute('data-dir') === 'asc' ? 'desc' : 'asc'; Array.prototype.forEach.call(table.tHead.querySelectorAll('.sort-btn'), function (b) { b.removeAttribute('data-dir'); b.classList.remove('active'); b.textContent = ICON.none; }); btn.setAttribute('data-dir', dir); btn.classList.add('active'); btn.textContent = ICON[dir]; sortTable(table, th.cellIndex, dir); }); th.appendChild(doc.createTextNode(' ')); th.appendChild(btn); }); } function init(root) { if (!root || !root.querySelectorAll) { return; } if (root.tagName === 'TABLE') { initTable(root); } Array.prototype.forEach.call(root.querySelectorAll('table'), initTable); } var api = { sortKey: sortKey, columnKind: columnKind, sortItems: sortItems, groupRows: groupRows, sortTable: sortTable, initTable: initTable, init: init }; if (typeof module !== 'undefined' && module.exports) { module.exports = api; } if (typeof window !== 'undefined') { window.FBSort = api; if (window.htmx) { window.htmx.onLoad(init); } } })();