/*
* USMT Dash - "Jump" for phones and tablets
*
* Scrolling through thousands of device cards is slow, so on narrow screens a round Jump
* button floats above the terminal bar on every page. It opens a sheet with:
* - Find a device: type part of a name and pick it; the page scrolls to that card and
* flashes it (nothing is filtered or hidden, unlike the search boxes).
* - Sections: every group on the page (USMT Status groups, Workbench stacks, the Overview
* list, the article list / article) with its count, to jump straight there.
* - Top / Bottom.
*/
(function () {
'use strict';
const narrow = window.matchMedia('(max-width: 1100px)');
let btn = null, sheet = null, backdrop = null;
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; };
const visible = e => !!(e && e.offsetParent !== null && e.getClientRects().length);
const currentPage = () => Array.from(document.querySelectorAll('.page')).find(p => p.style.display !== 'none');
const navH = () => parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nav-h')) || 60;
// Scroll so the element sits just below everything that sticks to the top
function scrollToEl(target, extra) {
const top = target.getBoundingClientRect().top + window.scrollY - navH() - (extra || 0) - 8;
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
}
// Sticky group header + filter bar above a card (the header is a sibling of the table or
// of one of its wrappers)
function stickyAbove(row) {
let h = 0;
const table = row.closest('table');
for (let e = table && table.parentElement, depth = 0; e && depth < 4; e = e.parentElement, depth++) {
const gh = e.querySelector(':scope > .group-header');
if (gh) { if (getComputedStyle(gh).position === 'sticky') h += gh.offsetHeight; break; }
}
const bar = table && table.previousElementSibling;
if (bar && bar.classList.contains('xf-bar')) h += bar.offsetHeight;
return h;
}
function flash(node) {
node.classList.remove('jump-flash');
void node.offsetWidth;
node.classList.add('jump-flash');
setTimeout(() => node.classList.remove('jump-flash'), 2200);
}
// A group header's title: a stack's name box, else its text without the buttons
function headerLabel(gh) {
const input = gh.querySelector('input');
if (input) return input.value.trim();
const c = gh.cloneNode(true);
c.querySelectorAll('button, div, .no-copy').forEach(n => n.remove());
return c.textContent.replace(/\s+/g, ' ').trim();
}
// ---------------------------------------------------------------------------------
// What's on the page
// ---------------------------------------------------------------------------------
function sections(page) {
const out = [];
page.querySelectorAll('.group-header').forEach(gh => {
if (!visible(gh)) return;
const label = headerLabel(gh);
if (label) out.push({ label, node: gh.closest('.table-wrapper, .stack-box, [id^="group-container-"]') || gh });
});
if (page.id === 'docs') {
const list = document.getElementById('docList'), art = document.getElementById('docToolbar');
if (visible(list)) out.push({ label: 'Article list', node: list.parentElement });
if (visible(art)) out.push({ label: 'Article', node: art });
}
if (page.id === 'overview') {
[['ov-issues', 'Issues to clear'], ['ov-grid', 'Status & locations'], ['ov-months', 'Deployment calendar']].forEach(([cls, label]) => {
const n = page.querySelector('.' + cls);
if (visible(n)) out.push({ label, node: n.closest('.ov-card') || n });
});
}
return out;
}
// Rows / cards whose device names match
function findDevices(page, q) {
q = q.trim().toUpperCase();
if (q.length < 2) return [];
const hits = [];
const seen = new Set();
page.querySelectorAll('table.master-table tbody tr').forEach(tr => {
if (hits.length >= 25 || !visible(tr)) return;
// device names are the bold cell text in every table (any naming scheme)
const names = Array.from(tr.querySelectorAll('td b')).map(b => b.textContent.trim().toUpperCase())
.concat(tr.cells[0] ? [tr.cells[0].textContent.trim().toUpperCase()] : []);
const name = names.find(n => n && n.length < 64 && n.includes(q));
if (!name) return;
const group = tr.closest('[id^="group-container-"], .stack-box, .table-wrapper');
const gh = group && group.querySelector('.group-header');
const where = gh ? headerLabel(gh).replace(/\s*\(\d+\)\s*$/, '') : '';
const key = name + '|' + where;
if (seen.has(key)) return;
seen.add(key);
hits.push({ name, where, tr });
});
return hits;
}
// ---------------------------------------------------------------------------------
// Sheet
// ---------------------------------------------------------------------------------
function close() {
if (sheet) sheet.remove();
if (backdrop) backdrop.remove();
sheet = backdrop = null;
}
function open() {
close();
const page = currentPage();
if (!page) return;
backdrop = el('div', 'xf-backdrop'); backdrop.onclick = close;
sheet = el('div', 'xf-menu xf-sheet jump-sheet');
sheet.setAttribute('role', 'dialog'); sheet.setAttribute('aria-label', 'Jump to');
const head = el('div', 'xf-head');
head.appendChild(el('span', 'xf-title', 'Jump to'));
const x = el('button', 'xf-x', '×'); x.type = 'button'; x.title = 'Close'; x.onclick = close;
head.appendChild(x);
sheet.appendChild(head);
const hasRows = !!page.querySelector('table.master-table tbody tr');
const results = el('div', 'jump-results');
if (hasRows) {
const search = el('input', 'xf-search jump-search');
search.type = 'search'; search.placeholder = 'Find a device (part of the name)...';
search.setAttribute('autocomplete', 'off'); search.setAttribute('autocapitalize', 'characters');
sheet.appendChild(search);
sheet.appendChild(results);
const show = () => {
results.textContent = '';
const q = search.value;
if (q.trim().length < 2) return;
const hits = findDevices(page, q);
if (!hits.length) { results.appendChild(el('div', 'jump-empty', 'No device on this page matches. (Filters may be hiding it.)')); return; }
hits.forEach(h => {
const b = el('button', 'jump-item'); b.type = 'button';
b.appendChild(el('b', null, h.name));
if (h.where) b.appendChild(el('span', null, h.where));
b.onclick = () => { close(); scrollToEl(h.tr, stickyAbove(h.tr)); setTimeout(() => flash(h.tr), 350); };
results.appendChild(b);
});
};
search.oninput = show;
search.onkeydown = e => { if (e.key === 'Enter') { const f = results.querySelector('.jump-item'); if (f) f.click(); } };
}
const secs = sections(page);
if (secs.length) {
sheet.appendChild(el('div', 'xf-group', 'Sections'));
const list = el('div', 'jump-sections');
secs.forEach(s => {
const b = el('button', 'jump-item jump-sec'); b.type = 'button';
b.textContent = s.label;
b.onclick = () => { close(); scrollToEl(s.node); };
list.appendChild(b);
});
sheet.appendChild(list);
}
const ends = el('div', 'xf-actions');
const top = el('button', 'xf-cancel', '↑ Top'); top.type = 'button';
top.onclick = () => { close(); window.scrollTo({ top: 0, behavior: 'smooth' }); };
const bottom = el('button', 'xf-cancel', '↓ Bottom'); bottom.type = 'button';
bottom.onclick = () => { close(); window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }); };
ends.appendChild(top); ends.appendChild(bottom);
sheet.appendChild(ends);
document.body.appendChild(backdrop);
document.body.appendChild(sheet);
const s = sheet.querySelector('.jump-search');
if (s) setTimeout(() => s.focus(), 50);
}
// ---------------------------------------------------------------------------------
// Button: phones / tablets only, on pages taller than a couple of screens
// ---------------------------------------------------------------------------------
function update() {
if (!btn) return;
const page = currentPage();
const long = document.documentElement.scrollHeight > window.innerHeight * 1.8;
btn.hidden = !narrow.matches || !page || !long;
}
function init() {
btn = el('button', 'jump-btn');
btn.type = 'button'; btn.title = 'Jump to a device or section'; btn.setAttribute('aria-label', 'Jump to a device or section');
btn.innerHTML = 'Jump';
btn.onclick = open;
btn.hidden = true;
document.body.appendChild(btn);
let t = null;
const later = () => { clearTimeout(t); t = setTimeout(update, 250); };
new MutationObserver(later).observe(document.body, { childList: true, subtree: true });
window.addEventListener('resize', later);
if (narrow.addEventListener) narrow.addEventListener('change', () => { close(); update(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && sheet) close(); });
update();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
})();