/* * Card layout support for small screens. * * On phones and tablets (see mobile.css) each table row is shown as a card, with every * cell rendered as "Column name: value". CSS can't look up a cell's column header, so * this copies each header's text onto its cells as data-label. The dashboard re-renders * its tables constantly, so labels are reapplied (once per frame at most) whenever the * DOM changes. On wide screens it does nothing. */ (function () { 'use strict'; const cardsQuery = window.matchMedia('(max-width: 1100px)'); const clean = text => text.replace(/[\u2195\u25B2\u25BC\u00D7\u2715]/g, '').replace(/\s+/g, ' ').trim(); function labelTable(table) { const thead = table.tHead; if (!thead) return; // The header row is the one with the most text (Custom View has a drag-handle row above it) let headerRow = null, best = -1; for (const row of thead.rows) { if (row.classList.contains('filter-row')) continue; const named = Array.from(row.cells).filter(c => clean(c.textContent)).length; if (named > best) { best = named; headerRow = row; } } if (!headerRow) return; for (const row of thead.rows) { if (row === headerRow) row.classList.add('card-label-row'); else if (!row.classList.contains('filter-row')) row.classList.add('card-aux-row'); } const labels = Array.from(headerRow.cells).map(c => clean(c.textContent)); const apply = row => { Array.from(row.cells).forEach((cell, i) => { const label = cell.colSpan > 1 ? '' : (labels[i] || ''); if (cell.dataset.label !== label) cell.dataset.label = label; }); }; thead.querySelectorAll('tr.filter-row').forEach(apply); for (const body of table.tBodies) for (const row of body.rows) apply(row); addFilterToggle(table, thead.querySelector('tr.filter-row')); } // Column filters take a lot of room above every card group, so they fold behind a // button that also shows how many filters are active. function addFilterToggle(table, filterRow) { if (!filterRow) return; let btn = table.previousElementSibling; if (!btn || !btn.classList.contains('card-filter-toggle')) { btn = document.createElement('button'); btn.type = 'button'; btn.className = 'card-filter-toggle'; btn.addEventListener('click', () => { table.classList.toggle('show-filters'); updateToggle(table, btn); }); table.parentNode.insertBefore(btn, table); filterRow.addEventListener('input', () => updateToggle(table, btn)); filterRow.addEventListener('change', () => updateToggle(table, btn)); } updateToggle(table, btn); } function updateToggle(table, btn) { const row = table.tHead && table.tHead.querySelector('tr.filter-row'); if (!row) return; const active = Array.from(row.querySelectorAll('input[type="text"]')).filter(i => i.value.trim()).length + row.querySelectorAll('input[type="checkbox"]:checked').length; const open = table.classList.contains('show-filters'); const html = `${open ? 'Hide' : 'Show'} filters${active ? ` ${active}` : ''}`; if (btn.innerHTML !== html) btn.innerHTML = html; } let queued = false; function run() { queued = false; if (!cardsQuery.matches) return; document.querySelectorAll('table.master-table').forEach(labelTable); } function schedule() { if (queued) return; queued = true; requestAnimationFrame(run); } function start() { new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true }); if (cardsQuery.addEventListener) cardsQuery.addEventListener('change', schedule); else cardsQuery.addListener(schedule); schedule(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start(); })();