/* * 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); } // Top / Middle / Bottom (same bold arrows as Card fields) const ends = el('div', 'xf-actions jump-ends'); const svg = d => ''; const end = (icon, label, y) => { const b = el('button', 'xf-cancel jump-end'); b.type = 'button'; b.innerHTML = svg(icon); b.appendChild(el('span', null, label)); b.onclick = () => { close(); window.scrollTo({ top: y(), behavior: 'smooth' }); }; ends.appendChild(b); }; const maxY = () => document.documentElement.scrollHeight - window.innerHeight; end('', 'Top', () => 0); end('', 'Middle', () => Math.round(maxY() / 2)); end('', 'Bottom', () => maxY()); 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'); // up + down arrows, drawn like the Card fields arrows 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(); })();