source.fact.ngo a coherence.ngo project

assets/emergence-mobile-shell.js

raw ↗ · AGPL-3.0

// Keep the working pane between two small edges; supporting chrome is on demand. export function enhanceMobileShell() { const root = document.documentElement; if (!root.classList.contains('has-shell')) return; const shell = document.querySelector('.em-shell'); const nav = shell.querySelector('.em-lens-nav'); const header = document.querySelector('header.chrome'); const mobile = matchMedia('(max-width:900px)'); const identity = shell.querySelector('.em-rail-id .em-status'); const fullIdentity = identity.textContent; const secondary = [...nav.querySelectorAll('a')].slice(4); let focusAfterRoute = false; const more = document.createElement('button'); more.type = 'button'; more.className = 'em-more-toggle'; more.textContent = 'More'; more.setAttribute('aria-haspopup', 'dialog'); more.setAttribute('aria-controls', 'em-more'); more.setAttribute('aria-expanded', 'false'); nav.append(more); const dialog = document.createElement('dialog'); dialog.id = 'em-more'; dialog.className = 'em-more'; dialog.setAttribute('aria-labelledby', 'em-more-title'); dialog.innerHTML = '<div class="em-more-heading"><h2 id="em-more-title">More</h2><button type="button" autofocus>Close</button></div>'; const destinations = document.createElement('nav'); destinations.className = 'em-more-links'; destinations.setAttribute('aria-label', 'More views'); secondary.forEach(link => destinations.append(link.cloneNode(true))); dialog.append(destinations, shell.querySelector('.em-rail-stats').cloneNode(true)); const site = document.createElement('nav'); site.className = 'em-more-links'; site.setAttribute('aria-label', 'Site'); header.querySelectorAll('nav a[href="/"], nav a[href="/xray/"]').forEach(link => site.append(link.cloneNode(true))); dialog.append(site); const settings = document.createElement('div'); settings.className = 'em-more-settings'; dialog.append(settings, header.querySelector('.badge').cloneNode(true)); document.body.append(dialog); // Move the real theme control, retaining the site's storage and accessible label. const theme = header.querySelector('.mode-toggle'); const themeHome = document.createComment('theme control'); theme.before(themeHome); const verified = document.createElement('a'); verified.href = '#verified'; verified.className = 'em-pocket-verified'; verified.textContent = '0 verified · inactive'; shell.querySelector('.em-rail-verified').after(verified); function markCurrent() { const current = shell.querySelector('[data-pane-on]')?.id; const isSecondary = secondary.some(link => link.hash === '#' + current); more.toggleAttribute('data-current', isSecondary); destinations.querySelectorAll('a').forEach(link => { if (link.hash === '#' + current) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current'); }); } function measure() { root.style.setProperty('--app-top', Math.round(header.getBoundingClientRect().height) + 'px'); root.style.setProperty('--em-dock-height', Math.ceil(nav.getBoundingClientRect().height) + 'px'); } function sync() { if (!mobile.matches && dialog.open) dialog.close(); root.classList.toggle('em-pocket', mobile.matches); identity.textContent = mobile.matches ? 'Not fact-checked · AI' : fullIdentity; if (mobile.matches) settings.append(theme); else themeHome.after(theme); measure(); markCurrent(); } more.addEventListener('click', () => { markCurrent(); dialog.showModal(); more.setAttribute('aria-expanded', 'true'); }); dialog.querySelector('button').addEventListener('click', () => dialog.close()); dialog.addEventListener('close', () => more.setAttribute('aria-expanded', 'false')); dialog.addEventListener('click', event => { if (event.target === dialog) { const rect = dialog.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close(); } const link = event.target.closest('a[href^="#"]'); if (link) { dialog.close(); if (link.hash === location.hash) requestAnimationFrame(focusPane); else focusAfterRoute = true; } }); function focusPane() { const heading = shell.querySelector('[data-pane-on] h2'); if (!heading) return; heading.tabIndex = -1; heading.focus({ preventScroll: true }); } window.addEventListener('hashchange', () => { if (dialog.open) dialog.close(); markCurrent(); if (focusAfterRoute) { focusAfterRoute = false; requestAnimationFrame(focusPane); } }); mobile.addEventListener('change', sync); new ResizeObserver(measure).observe(header); new ResizeObserver(measure).observe(nav); sync(); }