oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

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.
25export 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.
54export 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