import keyCode from "./keycode"; type Callback = (ev: KeyboardEvent) => void; type Keymap = Record; type Pattern = { which: string[]; ctrl?: boolean; shift?: boolean; alt?: boolean; }; type Action = { patterns: Pattern[]; callback: Callback; allowRepeat: boolean; }; const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { const result = { patterns: [], callback, allowRepeat: true, } as Action; if (patterns.match(/^\(.*\)$/) !== null) { result.allowRepeat = false; patterns = patterns.slice(1, -1); } result.patterns = patterns.split("|").map((part) => { const pattern = { which: [], ctrl: false, alt: false, shift: false, } as Pattern; const keys = part .trim() .split("+") .map((x) => x.trim().toLowerCase()); for (const key of keys) { switch (key) { case "ctrl": pattern.ctrl = true; break; case "alt": pattern.alt = true; break; case "shift": pattern.shift = true; break; default: pattern.which = keyCode(key).map((k) => k.toLowerCase()); } } return pattern; }); return result; }); const ignoreElemens = ["input", "textarea"]; function match(ev: KeyboardEvent, patterns: Action["patterns"]): boolean { const key = ev.code.toLowerCase(); return patterns.some( (pattern) => pattern.which.includes(key) && pattern.ctrl === ev.ctrlKey && pattern.shift === ev.shiftKey && pattern.alt === ev.altKey && !ev.metaKey, ); } export const makeHotkey = (keymap: Keymap) => { const actions = parseKeymap(keymap); return (ev: KeyboardEvent) => { if (document.activeElement) { if (ignoreElemens.some((el) => document.activeElement!.matches(el))) return; if (document.activeElement.attributes["contenteditable"]) return; } for (const action of actions) { const matched = match(ev, action.patterns); if (matched) { if (!action.allowRepeat && ev.repeat) return; ev.preventDefault(); ev.stopPropagation(); action.callback(ev); break; } } }; };