/* Global app navigation drawer for the Work Package Suite. The suite grew page by page and the only way between them was the browser's back button or the home page. This is the one place that lists everywhere you can go — a ☰ button in the app bar opening an off-canvas drawer. ROLE GATING: the drawer only offers what the signed-in account can actually reach. The Admin Console is admins-only, so it appears for admins only; the User Directory is readable by everyone (that's the point of a directory), so it always appears. Every destination re-checks server-side — this is navigation, not a permission. PROJECT CONTEXT: links that open a project-scoped page carry the active ?project= so the drawer doesn't silently drop the job you were looking at. Add it to a page with: after auth-guard.js. It mounts itself into whichever top bar the page has, and skips iframes (the embedded WP creator lives inside a page that already has one). */ (function () { 'use strict'; var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })(); if (inIframe) return; // ── the map ──────────────────────────────────────────────────────────────── // `match` is what marks a link current; `project` means "carry ?project=". // `show` is an optional gate, evaluated once the user is known. var LINKS = [ { section: 'Work' }, { href: 'index.html', match: /(^|\/)(index\.html)?$/, icon: '⌂', label: 'Home', sub: 'Projects & what\'s next' }, { href: 'work-package-suite.html?tab=sop', match: /work-package-suite\.html/, icon: '⚙', label: 'SOP Configuration', sub: 'The project baseline', project: true, tab: 'sop' }, { href: 'work-package-suite.html?tab=wp', match: null, icon: '▤', label: 'Work Package Creator', sub: 'Build and edit IWPs', project: true, tab: 'wp' }, { href: 'work-package-suite.html?tab=dashboard', match: null, icon: '▦', label: 'Dashboard', sub: 'Status & release gates', project: true, tab: 'dashboard' }, { href: 'field.html', match: /(^|\/)field\.html$/, icon: '⚒', label: 'Field View', sub: 'Update packages on site', project: true }, { section: 'People' }, { href: 'users.html', match: /(^|\/)users\.html$/, icon: '☺', label: 'User Directory', sub: 'Who\'s on the project' }, { href: 'admin.html', match: /(^|\/)admin\.html$/, icon: '⚡', label: 'Admin Console', sub: 'Settings & diagnostics', show: function () { return typeof window.wpIsAdmin === 'function' && window.wpIsAdmin(); } }, ]; function esc(v) { return String(v == null ? '' : v) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function isDark(node) { try { var m = (getComputedStyle(node).backgroundColor || '').match(/(\d+),\s*(\d+),\s*(\d+)/); if (!m) return true; return (0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3]) < 140; } catch (e) { return true; } } function activeProjectId() { try { var q = new URLSearchParams(location.search).get('project'); if (q) return q; return (window.ProjectData && ProjectData.getActiveId && ProjectData.getActiveId()) || ''; } catch (e) { return ''; } } // The suite page reads ?tab= and ?project=; keeping the current project on the link // is the difference between "open the dashboard" and "open the dashboard, then pick // the job again". function hrefFor(item) { if (!item.project) return item.href; var pid = activeProjectId(); if (!pid) return item.href; var sep = item.href.indexOf('?') >= 0 ? '&' : '?'; return item.href + sep + 'project=' + encodeURIComponent(pid); } // Current-page marking. The three suite tabs share one file, so they're told apart // by ?tab= (defaulting to sop, which is what work-package-suite.html itself does). function isCurrent(item) { var path = location.pathname; if (item.tab) { if (!/work-package-suite\.html$/.test(path)) return false; var tab = ''; try { tab = new URLSearchParams(location.search).get('tab') || 'sop'; } catch (e) { tab = 'sop'; } return tab === item.tab; } return !!(item.match && item.match.test(path)); } // ── build ────────────────────────────────────────────────────────────────── var drawer, scrim, btn, lastFocus = null; function buildDrawer(user) { scrim = document.createElement('div'); scrim.className = 'wp-navscrim'; scrim.hidden = true; scrim.addEventListener('click', close); drawer = document.createElement('nav'); drawer.className = 'wp-sidenav'; drawer.id = 'wp-sidenav'; drawer.setAttribute('aria-label', 'Suite navigation'); drawer.setAttribute('aria-hidden', 'true'); var rows = ''; LINKS.forEach(function (item) { if (item.section) { rows += '
' + esc(item.section) + '
'; return; } if (item.show && !item.show()) return; rows += '' + '' + '' + esc(item.label) + (item.sub ? '' + esc(item.sub) + '' : '') + ''; }); var who = user ? (user.full_name || user.username || '') : ''; drawer.innerHTML = '
' + 'Prime Controls' + 'Work Package SuitePrime Controls' + '' + '
' + '
' + rows + '
' + '
' + (who ? '
Signed in as' + esc(who) + '
' : '') + '' + '
'; drawer.querySelector('.wp-sidenav-close').addEventListener('click', close); drawer.querySelector('#wp-sidenav-signout').addEventListener('click', function () { if (typeof window.wpLogout === 'function') window.wpLogout(); }); document.body.appendChild(scrim); document.body.appendChild(drawer); } function focusables() { return drawer ? drawer.querySelectorAll('a[href], button:not([disabled])') : []; } function open() { if (!drawer) return; lastFocus = document.activeElement; scrim.hidden = false; // Two frames: the element has to be laid out un-transitioned before the class // that animates it lands, or it simply appears. requestAnimationFrame(function () { scrim.classList.add('is-open'); drawer.classList.add('is-open'); }); drawer.setAttribute('aria-hidden', 'false'); btn.setAttribute('aria-expanded', 'true'); var f = focusables(); if (f.length) f[0].focus(); } function close() { if (!drawer) return; drawer.classList.remove('is-open'); scrim.classList.remove('is-open'); drawer.setAttribute('aria-hidden', 'true'); btn.setAttribute('aria-expanded', 'false'); // Keep the scrim in the tree until the slide-out finishes, or the panel snaps. setTimeout(function () { if (!drawer.classList.contains('is-open')) scrim.hidden = true; }, 220); if (lastFocus && lastFocus.focus) lastFocus.focus(); } function isOpen() { return !!(drawer && drawer.classList.contains('is-open')); } // Escape closes; Tab cycles inside the drawer while it's open, so focus can't walk // off into the page behind the scrim. document.addEventListener('keydown', function (e) { if (!isOpen()) return; if (e.key === 'Escape') { e.preventDefault(); close(); return; } if (e.key !== 'Tab') return; var f = focusables(); if (!f.length) return; var first = f[0], last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }); // ── mount ────────────────────────────────────────────────────────────────── // The button goes at the START of the bar, before the brand: that is where a menu // affordance is looked for, and it keeps clear of the project switcher and search // that wp-chrome.js inserts into the middle of the same bar. function mount() { if (document.getElementById('wp-sidenav')) return; var host = document.querySelector('.wp-appbar') || document.querySelector('.header'); if (!host) return; btn = document.createElement('button'); btn.type = 'button'; btn.className = 'wp-navbtn'; btn.id = 'wp-navbtn'; btn.title = 'Menu'; btn.setAttribute('aria-label', 'Open navigation'); btn.setAttribute('aria-haspopup', 'true'); btn.setAttribute('aria-expanded', 'false'); btn.setAttribute('aria-controls', 'wp-sidenav'); if (!isDark(host)) btn.setAttribute('data-bar', 'light'); btn.innerHTML = ''; btn.addEventListener('click', function () { if (isOpen()) close(); else open(); }); host.insertBefore(btn, host.firstChild); buildDrawer(window.WP_USER); } // Wait for the auth guard: the gated links depend on the signed-in role, and an // unauthenticated page is about to redirect anyway. if (window.WP_USER) mount(); else document.addEventListener('wp-auth-ready', mount); })();