calckey/packages/client/src/scripts/hotkey.ts

105 lines
2.1 KiB
TypeScript
Raw Normal View History

2023-01-13 04:40:33 +00:00
import keyCode from "./keycode";
2020-12-27 08:04:41 +00:00
2022-07-04 14:46:48 +00:00
type Callback = (ev: KeyboardEvent) => void;
type Keymap = Record<string, Callback>;
2020-12-27 08:04:41 +00:00
type Pattern = {
which: string[];
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
};
type Action = {
patterns: Pattern[];
2022-07-04 14:46:48 +00:00
callback: Callback;
2020-12-27 08:04:41 +00:00
allowRepeat: boolean;
};
2023-01-13 04:40:33 +00:00
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);
2020-12-27 08:04:41 +00:00
}
2023-01-13 04:40:33 +00:00
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());
}
}
2020-12-27 08:04:41 +00:00
2023-01-13 04:40:33 +00:00
return pattern;
});
return result;
});
2020-12-27 08:04:41 +00:00
2023-01-13 04:40:33 +00:00
const ignoreElemens = ["input", "textarea"];
2020-12-27 08:04:41 +00:00
2023-01-13 04:40:33 +00:00
function match(ev: KeyboardEvent, patterns: Action["patterns"]): boolean {
const key = ev.code.toLowerCase();
2023-01-13 04:40:33 +00:00
return patterns.some(
(pattern) =>
pattern.which.includes(key) &&
pattern.ctrl === ev.ctrlKey &&
pattern.shift === ev.shiftKey &&
pattern.alt === ev.altKey &&
!ev.metaKey,
2020-12-27 08:04:41 +00:00
);
}
export const makeHotkey = (keymap: Keymap) => {
const actions = parseKeymap(keymap);
return (ev: KeyboardEvent) => {
2020-12-27 08:04:41 +00:00
if (document.activeElement) {
2023-01-13 04:40:33 +00:00
if (ignoreElemens.some((el) => document.activeElement!.matches(el)))
return;
if (document.activeElement.attributes["contenteditable"]) return;
2020-12-27 08:04:41 +00:00
}
for (const action of actions) {
const matched = match(ev, action.patterns);
2020-12-27 08:04:41 +00:00
if (matched) {
if (!action.allowRepeat && ev.repeat) return;
2020-12-27 08:04:41 +00:00
ev.preventDefault();
ev.stopPropagation();
action.callback(ev);
2020-12-27 08:04:41 +00:00
break;
}
}
};
};