| 1 |
// Keyboard shortcuts, the shortcuts-help modal, and the mobile nav toggle, |
| 2 |
// ported from mnw.js:504-572. |
| 3 |
|
| 4 |
/** Toggle the keyboard-shortcuts help overlay. Ported from mnw.js:531. */ |
| 5 |
export function toggleShortcutsHelp(): void { |
| 6 |
const existing = document.getElementById('shortcuts-help'); |
| 7 |
if (existing) { |
| 8 |
existing.remove(); |
| 9 |
return; |
| 10 |
} |
| 11 |
|
| 12 |
const overlay = document.createElement('div'); |
| 13 |
overlay.id = 'shortcuts-help'; |
| 14 |
overlay.className = 'modal-overlay'; |
| 15 |
overlay.style.display = 'flex'; |
| 16 |
overlay.onclick = (e) => { |
| 17 |
if (e.target === overlay) overlay.remove(); |
| 18 |
}; |
| 19 |
overlay.innerHTML = |
| 20 |
'<div class="modal-content" style="max-width: 420px; padding: 2rem;">' + |
| 21 |
'<div class="modal-header" style="margin-bottom: 1rem;">' + |
| 22 |
'<h2>Keyboard Shortcuts</h2>' + |
| 23 |
'<button type="button" class="modal-close" aria-label="Close">×</button>' + |
| 24 |
'</div>' + |
| 25 |
'<table style="width: 100%; font-size: 0.9rem;">' + |
| 26 |
'<tr><td style="padding: 0.3rem 0;"><kbd>Cmd+K</kbd></td><td>Search</td></tr>' + |
| 27 |
'<tr><td style="padding: 0.3rem 0;"><kbd>?</kbd></td><td>Show this help</td></tr>' + |
| 28 |
'<tr><td style="padding: 0.3rem 0;"><kbd>Esc</kbd></td><td>Close modal / overlay</td></tr>' + |
| 29 |
'<tr><td style="padding: 0.3rem 0;"><kbd>Cmd+S</kbd></td><td>Save current form</td></tr>' + |
| 30 |
'</table>' + |
| 31 |
'</div>'; |
| 32 |
|
| 33 |
// Wire the close button via JS, not inline onclick — the CSP has no |
| 34 |
// script-src 'unsafe-inline' so a generated inline handler is dead. |
| 35 |
const closeBtn = overlay.querySelector<HTMLElement>('.modal-close'); |
| 36 |
if (closeBtn) closeBtn.onclick = () => overlay.remove(); |
| 37 |
|
| 38 |
document.body.appendChild(overlay); |
| 39 |
} |
| 40 |
|
| 41 |
/** Install global keyboard shortcuts + the nav toggle. Ported from mnw.js:504. */ |
| 42 |
export function initKeyboard(): void { |
| 43 |
document.addEventListener('keydown', (e) => { |
| 44 |
const tag = document.activeElement?.tagName; |
| 45 |
const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'; |
| 46 |
|
| 47 |
if (e.key === 'Escape') { |
| 48 |
document.querySelector('.modal-overlay')?.remove(); |
| 49 |
} |
| 50 |
if ((e.metaKey || e.ctrlKey) && e.key === 's') { |
| 51 |
e.preventDefault(); |
| 52 |
const form = document.activeElement?.closest('form'); |
| 53 |
form?.querySelector<HTMLElement>('button[type="submit"]')?.click(); |
| 54 |
} |
| 55 |
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) { |
| 56 |
e.preventDefault(); |
| 57 |
const searchInput = document.getElementById('header-search-input') as HTMLInputElement | null; |
| 58 |
if (searchInput) { |
| 59 |
searchInput.focus(); |
| 60 |
searchInput.select(); |
| 61 |
} |
| 62 |
} |
| 63 |
if (e.key === '?' && !inInput && !e.metaKey && !e.ctrlKey) { |
| 64 |
e.preventDefault(); |
| 65 |
toggleShortcutsHelp(); |
| 66 |
} |
| 67 |
}); |
| 68 |
|
| 69 |
document.addEventListener('click', (e) => { |
| 70 |
const toggle = document.getElementById('nav-toggle') as HTMLInputElement | null; |
| 71 |
if (toggle && toggle.checked && (e.target as Element | null)?.closest('.nav-links a, .nav-links .btn--link')) { |
| 72 |
toggle.checked = false; |
| 73 |
} |
| 74 |
}); |
| 75 |
} |
| 76 |
|