| 1 | // fixtures/shortcuts.mjs — the keyboard shortcuts sheet. |
| 2 | // |
| 3 | // Built by READING THE MENU BAR'S OWN DOM rather than from a list. |
| 4 | // |
| 5 | // That is the whole design, and it is worth being explicit about why. A |
| 6 | // shortcuts sheet is the single most drift-prone screen in an |
| 7 | // application: every binding is stated twice, once where it works and |
| 8 | // once where it is documented, and the second copy is wrong within a |
| 9 | // month of anybody touching the first. The menu bar already renders |
| 10 | // every command's label and its shortcut, from the catalog, in the |
| 11 | // person's own locale. Reading that is one source of truth and no new |
| 12 | // strings beyond the dialog's title. |
| 13 | // |
| 14 | // It also means a shortcut that stops being offered stops being |
| 15 | // documented, automatically, which is the half nobody remembers to do |
| 16 | // by hand. |
| 17 | // |
| 18 | // What it cannot see is a binding with no menu item. Two of those exist |
| 19 | // and they are the ones a person most needs told, so they are passed in |
| 20 | // by the caller rather than invented here. |
| 21 | |
| 22 | // buildRows reads one menu bar and returns {group, label, keys} rows. |
| 23 | // Exported for the test, which can hand it a small fragment rather than |
| 24 | // the real page. |
| 25 | export function buildRows(menubar) { |
| 26 | const rows = [] |
| 27 | if (!menubar) return rows |
| 28 | |
| 29 | for (const menu of menubar.querySelectorAll('[estilo-menubar-menu]')) { |
| 30 | const title = menu.querySelector('[estilo-menubar-title]') |
| 31 | const group = title ? title.textContent.trim() : '' |
| 32 | for (const item of menu.querySelectorAll('[estilo-menu-item]')) { |
| 33 | // Already formatted for this platform by the server (estilo's |
| 34 | // menu.Shortcut), so the sheet and the menu cannot disagree. |
| 35 | const keys = item.getAttribute('data-docs-shortcut') |
| 36 | if (!keys) continue |
| 37 | // The label is its own element (estilo's [estilo-menu-label]), |
| 38 | // apart from the check and key columns. The sheet lists commands, |
| 39 | // not menu items, so an item's ellipsis ("Print…") is dropped. |
| 40 | const labelEl = item.querySelector('[estilo-menu-label]') |
| 41 | const label = (labelEl ? labelEl.textContent : item.textContent).trim().replace(/…$/, '') |
| 42 | rows.push({ group, label, keys }) |
| 43 | } |
| 44 | } |
| 45 | return rows |
| 46 | } |
| 47 | |
| 48 | // mountShortcuts fills the dialog and returns the command the menu |
| 49 | // runs, or null when the template renders no dialog. |
| 50 | // |
| 51 | // extraRows are the bindings no menu item carries. Passed in rather |
| 52 | // than hardcoded here so the strings stay in the catalog where every |
| 53 | // other user-visible string in this app lives. |
| 54 | export function mountShortcuts(dialog, { menubar, extraRows }) { |
| 55 | if (!dialog || typeof dialog.showModal !== 'function') return null |
| 56 | const body = dialog.querySelector('[docs-shortcuts-body]') |
| 57 | if (!body) return null |
| 58 | |
| 59 | return () => { |
| 60 | const rows = [...buildRows(menubar), ...(extraRows || [])] |
| 61 | body.textContent = '' |
| 62 | let group = null |
| 63 | for (const row of rows) { |
| 64 | if (row.group && row.group !== group) { |
| 65 | group = row.group |
| 66 | const h = document.createElement('h3') |
| 67 | h.setAttribute('docs-shortcuts-group', '') |
| 68 | h.textContent = group |
| 69 | body.appendChild(h) |
| 70 | } |
| 71 | const line = document.createElement('div') |
| 72 | line.setAttribute('docs-shortcuts-row', '') |
| 73 | const label = document.createElement('span') |
| 74 | label.textContent = row.label |
| 75 | const keys = document.createElement('kbd') |
| 76 | keys.textContent = row.keys |
| 77 | line.append(label, keys) |
| 78 | body.appendChild(line) |
| 79 | } |
| 80 | dialog.showModal() |
| 81 | return true |
| 82 | } |
| 83 | } |
| 84 | |