hippofish/packages/frontend/src/scripts/hotkey.ts

96 lines
2.1 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
2020-12-27 09:04:41 +01:00
import keyCode from './keycode';
2022-07-04 16:46:48 +02:00
type Callback = (ev: KeyboardEvent) => void;
type Keymap = Record<string, Callback>;
2020-12-27 09:04:41 +01:00
type Pattern = {
which: string[];
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
};
type Action = {
patterns: Pattern[];
2022-07-04 16:46:48 +02:00
callback: Callback;
2020-12-27 09:04:41 +01:00
allowRepeat: boolean;
};
const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
const result = {
patterns: [],
2022-07-04 16:46:48 +02:00
callback,
allowRepeat: true,
2020-12-27 09:04:41 +01:00
} 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,
2020-12-27 09:04:41 +01:00
} 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'];
2020-12-27 09:04:41 +01:00
function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean {
const key = ev.key.toLowerCase();
2020-12-27 09:04:41 +01: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 09:04:41 +01:00
);
}
export const makeHotkey = (keymap: Keymap) => {
const actions = parseKeymap(keymap);
return (ev: KeyboardEvent) => {
2020-12-27 09:04:41 +01:00
if (document.activeElement) {
if (ignoreElements.some(el => document.activeElement!.matches(el))) return;
2020-12-27 09:04:41 +01:00
if (document.activeElement.attributes['contenteditable']) return;
}
for (const action of actions) {
const matched = match(ev, action.patterns);
2020-12-27 09:04:41 +01:00
if (matched) {
if (!action.allowRepeat && ev.repeat) return;
2020-12-27 09:04:41 +01:00
ev.preventDefault();
ev.stopPropagation();
action.callback(ev);
2020-12-27 09:04:41 +01:00
break;
}
}
};
};