diff options
Diffstat (limited to 'frontend/src/lib/Shortcuts.ts')
-rw-r--r-- | frontend/src/lib/Shortcuts.ts | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/frontend/src/lib/Shortcuts.ts b/frontend/src/lib/Shortcuts.ts new file mode 100644 index 0000000..063bd40 --- /dev/null +++ b/frontend/src/lib/Shortcuts.ts @@ -0,0 +1,153 @@ +import { closeModal, modals } from 'svelte-modals'; +import { get } from 'svelte/store'; + +type LowercaseLetter = + | 'a' + | 'b' + | 'c' + | 'd' + | 'e' + | 'f' + | 'g' + | 'h' + | 'i' + | 'j' + | 'l' + | 'm' + | 'n' + | 'o' + | 'p' + | 'q' + | 'r' + | 's' + | 't' + | 'u' + | 'v' + | 'w' + | 'x' + | 'y' + | 'z'; + +type UppercaseLetter = Uppercase<LowercaseLetter>; +type Letter = LowercaseLetter | UppercaseLetter; +type Special = '?' | 'Enter' | 'Escape' | 'Delete'; + +const modeSwitches = ['n', 'g', 'i'] as const; +type ModeSwitch = (typeof modeSwitches)[number]; + +function isModeSwitch(s: string): s is ModeSwitch { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + return modeSwitches.indexOf(s as any) !== -1; +} + +type Key = Letter | Special; +type KeyCombo = `${ModeSwitch}${Letter}`; +export type Shortcut = Key | KeyCombo; + +type EventAction = (event: KeyboardEvent) => void; +type FocusAction = HTMLInputElement; +type ClickAction = HTMLElement; + +type Action = EventAction | FocusAction | ClickAction; + +const handlers = new Map<string, Action>(); +let mode: ModeSwitch | undefined; + +export function handleShortcuts(event: KeyboardEvent) { + if (isInputElement(event.target)) { + if (event.key === 'Escape') { + event.target.blur(); + event.preventDefault(); + event.stopImmediatePropagation(); + } + return; + } + + if (event.ctrlKey) { + return; + } + + if (event.key === 'Escape') { + if (get(modals).length > 0) { + closeModal(); + event.preventDefault(); + event.stopImmediatePropagation(); + return; + } + } + + if (isModeSwitch(event.key) && mode === undefined) { + mode = event.key; + event.preventDefault(); + return; + } + + const handler = handlers.get(mode === undefined ? event.key : `${mode}${event.key}`); + + if (!handler || get(modals).length > 0) { + mode = undefined; + return; + } + + if (handler instanceof HTMLInputElement) { + handler.focus(); + } else if (handler instanceof HTMLElement) { + handler.click(); + } else { + handler(event); + } + + mode = undefined; + event.preventDefault(); +} + +export function accelerator(node: HTMLElement | HTMLInputElement, sc: Shortcut) { + handlers.set(sc, node); + + return { + destroy() { + handlers.delete(sc); + } + }; +} + +export function binds(node: Document, scs: [string, EventAction][]) { + const handlers = new Map<string, EventAction>(); + + for (const [k, a] of scs) { + handlers.set(k, a); + } + + function keydown(event: KeyboardEvent) { + if (isInputElement(event.target)) return; + + const handler = handlers.get(event.key); + + if (!handler) return; + + handler(event); + event.preventDefault(); + } + + node.addEventListener('keydown', keydown); + + return { + destroy() { + node.removeEventListener('keydown', keydown); + } + }; +} + +export function addShortcut(sc: Shortcut, action: EventAction) { + handlers.set(sc, action); +} + +function isInputElement(target: EventTarget | null): target is HTMLElement { + return ( + target instanceof HTMLElement && + (target instanceof HTMLInputElement || + target instanceof HTMLSelectElement || + target instanceof HTMLTextAreaElement || + target.isContentEditable) + ); +} |