104 lines
2.1 KiB
TypeScript
104 lines
2.1 KiB
TypeScript
import keyCode from "./keycode";
|
|
|
|
type Callback = (ev: KeyboardEvent) => void;
|
|
|
|
type Keymap = Record<string, Callback>;
|
|
|
|
interface Pattern {
|
|
which: string[];
|
|
ctrl?: boolean;
|
|
shift?: boolean;
|
|
alt?: boolean;
|
|
}
|
|
|
|
interface 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;
|
|
}
|
|
}
|
|
};
|
|
};
|