Utilities

KeyboardShortcuts

Binds key combinations such as ⌘S or ? to functions on the page, leaving text boxes alone, and lists them.

#utility#keyboard#accessibility#interactive

Demo

Preview
Keyboard shortcuts
SaveCtrl+SorCtrl+Enter
New itemAlt+N
Show help?
CloseEsc

Last done: nothing yet; press one of the keys above.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/keyboard_shortcuts.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/keyboard_shortcuts.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/keyboard_shortcuts.qwe
component KeyboardShortcutsDemoLive() {    state log = []    let did = (what: str) => { log = take(push(log, what) | reverse, 4) | reverse }    <div.demo-stack>        <KeyboardShortcuts show={true} bindings={[            { keys: "mod+s, mod+Enter", label: "Save", run: () => { did("saved") } },            { keys: "alt+n", label: "New item", run: () => { did("new item") } },            { keys: "?", label: "Show help", run: () => { did("help") } },            { keys: "Escape", label: "Close", run: () => { did("closed") }, in_fields: true },            { keys: "g", run: () => { did("g") } },        ]}/>        <TextField name="kbsc_try" label="A text box: shortcuts other than Escape are left alone in it" placeholder="Type here"/>        <p.demo-note>Last done: {len(log) == 0 ? "nothing yet; press one of the keys above" : join(log, ", ")}.</p>    </div>}view KeyboardShortcutsDemo() =    <div.demo-stack>        <KeyboardShortcutsDemoLive/>    </div>

Source

ui/keyboard_shortcuts.qwe
// KeyboardShortcuts: binds key combinations to functions while it is on the// page, and can list them. "mod" is ⌘ on a Mac and Ctrl elsewhere.////   <KeyboardShortcuts bindings={[//       { keys: "mod+s", label: "Save", run: () => { save() } },//       { keys: "mod+k", label: "Search", run: () => { searching = true } },//       { keys: "?", label: "Show shortcuts", run: () => { help = true } },//       { keys: "Escape", label: "Close", run: () => { help = false }, in_fields: true },//   ]}/>//// bindings: maps of { keys, label?, run, in_fields? }. keys: modifiers (mod,// ctrl, alt, shift, meta) and a key joined by "+": a letter, a digit, a// character as typed ("?", "/"), or a key's name ("Escape", "Enter",// "ArrowUp", "F2"). Several combos for one action: "mod+s, mod+enter". run():// called with nothing. in_fields: also while typing in a text box (false: keys// typed into fields are left alone). The browser's own action for a key bound// here is prevented. show: lists the bindings that have a label, as a table of// keys (false). label: the list's caption ("Keyboard shortcuts").// keyboard_shortcuts_text(keys, mac) gives how a combo is written ("⌘S", "Ctrl+S").// Inside one of your components, since it takes functions.import ui/theme.qwe use ui_clslet keyboard_shortcuts_css = css`.ui-kbsc { width: 100%; border-collapse: collapse; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-kbsc caption { padding: 0 0 8px; text-align: left; font-weight: 600; }.ui-kbsc td { padding: 7px 0; border-top: 1px solid var(--ui-border); }.ui-kbsc td:last-child { text-align: right; }.ui-kbsc kbd { display: inline-block; min-width: 22px; padding: 2px 6px; border: 1px solid var(--ui-border); border-bottom-width: 2px;  border-radius: 5px; background: var(--ui-subtle); color: var(--ui-fg); font: 500 12px/1.4 var(--ui-mono); text-align: center; }.ui-kbsc-or { margin: 0 6px; color: var(--ui-muted); font-size: 12px; }`// A combo as { ctrl, alt, shift, meta, key }, "mod" made meta on a Mac and ctrl elsewhere.fn keyboard_shortcuts_parse(combo: str, mac: bool) -> map {    let parts = split(trim(combo), "+") | map((p) => trim(p))    // "mod++" and "+" name the plus key itself.    let key = ends_with(trim(combo), "++") || trim(combo) == "+" ? "+" : last(parts)    let mods = take(parts, len(parts) - 1) | map((m) => lower(m))    let has = (m) => contains(mods, m)    { ctrl: has("ctrl") || (has("mod") && !mac), meta: has("meta") || has("cmd") || (has("mod") && mac),      alt: has("alt") || has("option"), shift: has("shift"), key: lower(key) }}// How a combo is written: "⌘⇧S" on a Mac, "Ctrl+Shift+S" elsewhere.fn keyboard_shortcuts_text(combo: str, mac: bool = false) -> str {    let c = keyboard_shortcuts_parse(combo, mac)    let names = { escape: "Esc", arrowup: "↑", arrowdown: "↓", arrowleft: "←", arrowright: "→", enter: "Enter", " ": "Space", space: "Space" }    let k = names[c.key] ?? (len(c.key) == 1 ? upper(c.key) : upper(slice(c.key, 0, 1)) + slice(c.key, 1))    mac        ? join([c.ctrl && "⌃", c.alt && "⌥", c.shift && "⇧", c.meta && "⌘", k] | filter((p) => p != false), "")        : join([c.ctrl && "Ctrl", c.alt && "Alt", c.shift && "Shift", c.meta && "Meta", k] | filter((p) => p != false), "+")}// Whether a key event matches a combo. Shift is checked for letters and named// keys; for a character as typed ("?") it is part of how it was typed.fn keyboard_shortcuts_match(c: map, e) -> bool {    let key = lower(e.key ?? "")    let named_key = key == "space" ? " " : key    let want = c.key == "space" ? " " : c.key    let shift_counts = len(want) > 1 || re.test(want, "^[a-z0-9]$")    named_key == want && e.ctrlKey == c.ctrl && e.metaKey == c.meta && e.altKey == c.alt && (!shift_counts || e.shiftKey == c.shift)}component KeyboardShortcuts(bindings: list = [], show: bool = false, label: str = "Keyboard shortcuts", class: str? = null) {    style global keyboard_shortcuts_css    state mac = false    <div class={ui_cls(["ui-kbsc-host", class])} @mount={(_el) => {        mac = contains(str(js("navigator.platform") ?? ""), "Mac")        let onkey = (e) => {            let t = e.target            let typing = t != null && (t.isContentEditable == true || contains(["INPUT", "TEXTAREA", "SELECT"], str(t.tagName ?? "")))            let hit = find(bindings, (b) => (!typing || b.in_fields == true)                && any(split(b.keys, ","), (k) => trim(k) != "" && keyboard_shortcuts_match(keyboard_shortcuts_parse(k, mac), e)))            if hit != null && !e.repeat {                e.preventDefault()                hit.run()            }        }        js("document.addEventListener", "keydown", onkey)        () => js("document.removeEventListener", "keydown", onkey)    }}>        {show && <table.ui-kbsc>            <caption>{label}</caption>            <tbody>                {for b in bindings | filter((b) => b.label != null) => <tr key={b.keys}>                    <td>{b.label}</td>                    <td>{for [i, k] in enumerate(split(b.keys, ",") | map((k) => trim(k)) | filter((k) => k != "")) =>                        [i > 0 && <span.ui-kbsc-or>or</span>,                         <kbd aria-label={keyboard_shortcuts_text(k, mac)}>{keyboard_shortcuts_text(k, mac)}</kbd>]}</td>                </tr>}            </tbody>        </table>}    </div>}
Thread
68Useful? Like it, or add to its thread below.

Thread 0 posts

On KeyboardShortcuts: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.