Sticky status headers; fix floating details-view button on phones
- Status group headers ("Not Started (37)") stick under the top nav while
scrolling through their group, on every screen size
- On desktop the column header and filter rows stick beneath the group
header (table wrappers no longer clip them; these fixed-layout tables
never scroll sideways at desktop widths)
- layout.js measures the nav, group header and header row heights so the
sticky rows stack without gaps, re-measuring on re-render/resize/scroll
- Phones: the Advanced/Simple View button sits on its own line under the
device name instead of floating over the dialog content
- Rename mobile.css/js to layout.css/js now that they hold desktop rules
This commit is contained in:
1 parent
2cbb03894b
commit
a85c22b2b9
3 files changed
+77
-11
No files matched your search
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Layout helpers (see layout.css).
|
||||
*
|
||||
* Sticky headers: measures the top nav and each table's group header so the column
|
||||
* header rows can stick directly beneath them (--nav-h, --sticky-top, --thead-r1).
|
||||
*
|
||||
* Card view: on phones and tablets 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 both are reapplied (once per frame
|
||||
* at most) whenever the DOM changes.
|
||||
*/
|
||||
(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 ? ` <span class="count">${active}</span>` : ''}`;
|
||||
if (btn.innerHTML !== html) btn.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- Sticky offsets ---------------------------------------------------------------
|
||||
function setVar(el, name, value) {
|
||||
if (el.style.getPropertyValue(name) !== value) el.style.setProperty(name, value);
|
||||
}
|
||||
function groupHeaderFor(table) {
|
||||
// The group header is a sibling of the table or of its wrapper
|
||||
for (let el = table.parentElement, depth = 0; el && depth < 3; el = el.parentElement, depth++) {
|
||||
const gh = el.querySelector(':scope > .group-header');
|
||||
if (gh) return gh;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function updateSticky() {
|
||||
const nav = document.querySelector('.top-nav');
|
||||
const navH = nav ? nav.offsetHeight : 60;
|
||||
setVar(document.documentElement, '--nav-h', navH + 'px');
|
||||
document.querySelectorAll('table.master-table').forEach(table => {
|
||||
const gh = groupHeaderFor(table);
|
||||
const ghH = gh ? gh.offsetHeight : 0;
|
||||
const firstRow = table.tHead && table.tHead.rows[0];
|
||||
if (!firstRow || !firstRow.offsetHeight) return; // hidden page/group: keep old values
|
||||
setVar(table, '--sticky-top', (navH + ghH) + 'px');
|
||||
setVar(table, '--thead-r1', firstRow.offsetHeight + 'px');
|
||||
});
|
||||
}
|
||||
|
||||
let queued = false;
|
||||
function run() {
|
||||
queued = false;
|
||||
updateSticky();
|
||||
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);
|
||||
window.addEventListener('resize', schedule);
|
||||
// Groups and pages are shown by toggling display (not a DOM change), so also
|
||||
// re-measure as the user scrolls; writes only happen when a value changes.
|
||||
window.addEventListener('scroll', schedule, { passive: true });
|
||||
schedule();
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|
||||
else start();
|
||||
})();
|
||||
Reference in new issue
Block a user