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 ignoreElements = ['input', 'textarea']; function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { const key = ev.key.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 (ignoreElements.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; } } }; };