import { useEffect, useRef } from "react" /** * Window-level keyboard shortcuts. * * A chord reads as `mod+shift+z`, where `mod` is ⌘ on a Mac and Ctrl * everywhere else. Bindings are plain data, so a view declares the whole set * it answers to in one place. */ export type Shortcuts = Record void> /** Text fields and the code editor keep their own bindings and their own undo. */ export function isTextEntry(target: EventTarget | null): boolean { return Boolean( (target as Element | null)?.closest?.( "input, textarea, [contenteditable='true'], .monaco-editor", ), ) } /** Focus is inside the embedded code editor. */ export function inCodeEditor(target: EventTarget | null): boolean { return Boolean((target as Element | null)?.closest?.(".monaco-editor")) } function chordOf(event: KeyboardEvent): string { const parts: string[] = [] if (event.metaKey || event.ctrlKey) parts.push("mod") if (event.altKey) parts.push("alt") if (event.shiftKey) parts.push("shift") // Shift makes it a capital Z, so compare on the letter alone. parts.push(event.key.toLowerCase()) return parts.join("+") } /** * Bind chords for as long as the component is mounted. * * A chord listed in `inTextEntry` fires even while a field or the code editor * has focus; every other one steps aside, because typing there means typing. */ export function useShortcuts(bindings: Shortcuts, inTextEntry: string[] = []) { // Read through a ref, so a fresh handler on every render never re-binds. const latest = useRef({ bindings, inTextEntry }) latest.current = { bindings, inTextEntry } useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { const chord = chordOf(event) const run = latest.current.bindings[chord] if (!run) return if ( isTextEntry(event.target) && !latest.current.inTextEntry.includes(chord) ) { return } event.preventDefault() run(event) } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, []) }