summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/scripts')
-rw-r--r--packages/frontend/src/scripts/hotkey.ts169
-rw-r--r--packages/frontend/src/scripts/keycode.ts24
2 files changed, 104 insertions, 89 deletions
diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts
index 0600bff893..fd79baa604 100644
--- a/packages/frontend/src/scripts/hotkey.ts
+++ b/packages/frontend/src/scripts/hotkey.ts
@@ -3,93 +3,132 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import keyCode from './keycode.js';
+//#region types
+export type Keymap = Record<string, CallbackFunction | CallbackObject>;
-type Callback = (ev: KeyboardEvent) => void;
+type CallbackFunction = (ev: KeyboardEvent) => unknown;
-type Keymap = Record<string, Callback>;
+type CallbackObject = {
+ callback: CallbackFunction;
+ allowRepeat?: boolean;
+};
type Pattern = {
which: string[];
- ctrl?: boolean;
- shift?: boolean;
- alt?: boolean;
+ ctrl: boolean;
+ alt: boolean;
+ shift: boolean;
};
type Action = {
patterns: Pattern[];
- callback: Callback;
- allowRepeat: boolean;
+ callback: CallbackFunction;
+ options: Required<Omit<CallbackObject, 'callback'>>;
};
+//#endregion
-const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
- const result = {
- patterns: [],
- callback,
- allowRepeat: true,
- } as Action;
+//#region consts
+const KEY_ALIASES = {
+ 'esc': 'Escape',
+ 'enter': ['Enter', 'NumpadEnter'],
+ 'space': [' ', 'Spacebar'],
+ 'up': 'ArrowUp',
+ 'down': 'ArrowDown',
+ 'left': 'ArrowLeft',
+ 'right': 'ArrowRight',
+ 'plus': ['+', ';'],
+};
- if (patterns.match(/^\(.*\)$/) !== null) {
- result.allowRepeat = false;
- patterns = patterns.slice(1, -1);
- }
+const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
- result.patterns = patterns.split('|').map(part => {
- const pattern = {
- which: [],
- ctrl: false,
- alt: false,
- shift: false,
- } as Pattern;
+const IGNORE_ELEMENTS = ['input', 'textarea'];
+//#endregion
- 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());
+//#region impl
+export const makeHotkey = (keymap: Keymap) => {
+ const actions = parseKeymap(keymap);
+ return (ev: KeyboardEvent) => {
+ if ('pswp' in window && window.pswp != null) return;
+ if (document.activeElement != null) {
+ if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
+ if ((document.activeElement as HTMLElement).isContentEditable) return;
+ }
+ for (const { patterns, callback, options } of actions) {
+ if (matchPatterns(ev, patterns, options)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ callback(ev);
}
}
+ };
+};
- return pattern;
+const parseKeymap = (keymap: Keymap) => {
+ return Object.entries(keymap).map(([rawPatterns, rawCallback]) => {
+ const patterns = parsePatterns(rawPatterns);
+ const callback = parseCallback(rawCallback);
+ const options = parseOptions(rawCallback);
+ return { patterns, callback, options } as const satisfies Action;
});
+};
- 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,
- );
-}
+const parsePatterns = (rawPatterns: keyof Keymap) => {
+ return rawPatterns.split('|').map(part => {
+ const keys = part.split('+').map(trimLower);
+ const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x)));
+ const ctrl = keys.includes('ctrl');
+ const alt = keys.includes('alt');
+ const shift = keys.includes('shift');
+ return { which, ctrl, alt, shift } as const satisfies Pattern;
+ });
+};
-export const makeHotkey = (keymap: Keymap) => {
- const actions = parseKeymap(keymap);
+const parseCallback = (rawCallback: Keymap[keyof Keymap]) => {
+ if (typeof rawCallback === 'object') {
+ return rawCallback.callback;
+ }
+ return rawCallback;
+};
- return (ev: KeyboardEvent) => {
- if (document.activeElement) {
- if (ignoreElements.some(el => document.activeElement!.matches(el))) return;
- if (document.activeElement.attributes['contenteditable']) return;
- }
+const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
+ const defaultOptions = {
+ allowRepeat: false,
+ } as const satisfies Action['options'];
+ if (typeof rawCallback === 'object') {
+ const { callback, ...rawOptions } = rawCallback;
+ const options = { ...defaultOptions, ...rawOptions };
+ return { ...options } as const satisfies Action['options'];
+ }
+ return { ...defaultOptions } as const satisfies Action['options'];
+};
- for (const action of actions) {
- const matched = match(ev, action.patterns);
+const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options: Action['options']) => {
+ if (ev.repeat && !options.allowRepeat) return false;
+ const key = ev.key.toLowerCase();
+ return patterns.some(({ which, ctrl, shift, alt }) => {
+ if (!which.includes(key)) return false;
+ if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
+ if (alt !== ev.altKey) return false;
+ if (shift !== ev.shiftKey) return false;
+ return true;
+ });
+};
- if (matched) {
- if (!action.allowRepeat && ev.repeat) return;
+const parseKeyCode = (input?: string | null) => {
+ if (input == null) return [];
+ const raw = getValueByKey(KEY_ALIASES, input);
+ if (raw == null) return [input];
+ if (typeof raw === 'string') return [trimLower(raw)];
+ return raw.map(trimLower);
+};
- ev.preventDefault();
- ev.stopPropagation();
- action.callback(ev);
- break;
- }
- }
- };
+const getValueByKey = <
+ T extends Record<keyof any, unknown>,
+ K extends keyof T | keyof any,
+ R extends K extends keyof T ? T[K] : T[keyof T] | undefined,
+>(obj: T, key: K) => {
+ return obj[key] as R;
};
+
+const trimLower = (str: string) => str.trim().toLowerCase();
+//#endregion
diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts
deleted file mode 100644
index 7ffceafada..0000000000
--- a/packages/frontend/src/scripts/keycode.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export default (input: string): string[] => {
- if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
- const codes = aliases[input];
- return Array.isArray(codes) ? codes : [codes];
- } else {
- return [input];
- }
-};
-
-export const aliases = {
- 'esc': 'Escape',
- 'enter': ['Enter', 'NumpadEnter'],
- 'space': [' ', 'Spacebar'],
- 'up': 'ArrowUp',
- 'down': 'ArrowDown',
- 'left': 'ArrowLeft',
- 'right': 'ArrowRight',
- 'plus': ['NumpadAdd', 'Semicolon'],
-};