Skip to main content

max / makenotwork

3.8 KB · 95 lines History Blame Raw
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">&times;</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 /**
42 * Whether this page declares its shortcuts rather than hard-coding them.
43 *
44 * `e0c0d991`. A described document carries `quasi-webview`'s chrome: one hidden
45 * button per binding, marked `data-chrome`, and `?` is bound to the described
46 * listing. This file loads on described and undescribed pages alike -- both are
47 * served from one shell -- so without the check both handlers fire on `?` and
48 * the reader gets two overlays.
49 *
50 * The described page wins, because its listing is derived from the bindings and
51 * this one is a hand-written copy. When the last template converts, this
52 * function and the overlay below it go with it.
53 */
54 function describedChrome(): boolean {
55 return document.querySelector('[data-chrome]') !== null;
56 }
57
58 /** Install global keyboard shortcuts + the nav toggle. Ported from mnw.js:504. */
59 export function initKeyboard(): void {
60 document.addEventListener('keydown', (e) => {
61 const tag = document.activeElement?.tagName;
62 const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
63
64 if (e.key === 'Escape') {
65 document.querySelector('.modal-overlay')?.remove();
66 }
67 if ((e.metaKey || e.ctrlKey) && e.key === 's') {
68 e.preventDefault();
69 const form = document.activeElement?.closest('form');
70 form?.querySelector<HTMLElement>('button[type="submit"]')?.click();
71 }
72 if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
73 e.preventDefault();
74 const searchInput = document.querySelector<HTMLInputElement>('.chrome-search input');
75 if (searchInput) {
76 searchInput.focus();
77 searchInput.select();
78 }
79 }
80 if (e.key === '?' && !inInput && !e.metaKey && !e.ctrlKey && !describedChrome()) {
81 e.preventDefault();
82 toggleShortcutsHelp();
83 }
84 });
85
86 document.addEventListener('click', (e) => {
87 // The ids and classes are `quasi-webview`'s: the header is a described
88 // `Chrome::band` and this script reads what that renderer emits.
89 const toggle = document.getElementById('quasi-disclose') as HTMLInputElement | null;
90 if (toggle && toggle.checked && (e.target as Element | null)?.closest('.chrome-nav a')) {
91 toggle.checked = false;
92 }
93 });
94 }
95