diff options
Diffstat (limited to 'packages/frontend/src/scripts')
| -rw-r--r-- | packages/frontend/src/scripts/hotkey.ts | 169 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/keycode.ts | 24 |
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'], -}; |