forked from misskey-dev/misskey
-
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
108 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* | ||
* SPDX-FileCopyrightText: syuilo and misskey-project | ||
* SPDX-License-Identifier: AGPL-3.0-only | ||
*/ | ||
|
||
import { getHTMLElementOrNull } from '@/scripts/tms/get-or-null.js'; | ||
|
||
//#region types | ||
type Callback = (ev: KeyboardEvent) => unknown; | ||
|
||
type Keymap = Record<string, Callback>; | ||
|
||
type Pattern = { | ||
which: string[]; | ||
ctrl: boolean; | ||
alt: boolean; | ||
shift: boolean; | ||
}; | ||
|
||
type Action = { | ||
patterns: Pattern[]; | ||
callback: Callback; | ||
}; | ||
//#endregion | ||
|
||
//#region consts | ||
const KEY_ALIASES = { | ||
'esc': 'Escape', | ||
'enter': 'Enter', | ||
'space': ' ', | ||
'up': 'ArrowUp', | ||
'down': 'ArrowDown', | ||
'left': 'ArrowLeft', | ||
'right': 'ArrowRight', | ||
'plus': ['+', ';'], | ||
}; | ||
|
||
const IGNORE_ELEMENTS = ['input', 'textarea']; | ||
|
||
const ALLOW_REPEAT_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; | ||
//#endregion | ||
|
||
//#region impl | ||
export const makeHotkey = (keymap: Keymap) => { | ||
const actions = parseKeymap(keymap); | ||
return (ev: KeyboardEvent) => { | ||
if (ev.repeat && !ALLOW_REPEAT_KEYS.includes(ev.key)) return; | ||
if ('pswp' in window && window.pswp != null) return; | ||
if (document.activeElement != null) { | ||
if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return; | ||
if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return; | ||
} | ||
for (const { patterns, callback } of actions) { | ||
if (matchPatterns(ev, patterns)) { | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
callback(ev); | ||
} | ||
} | ||
}; | ||
}; | ||
|
||
const parseKeymap = (keymap: Keymap) => { | ||
return Object.entries(keymap).map(([rawPatterns, callback]) => { | ||
const patterns = parsePatterns(rawPatterns); | ||
return { patterns, callback } as const satisfies Action; | ||
}); | ||
}; | ||
|
||
const parsePatterns = (rawPatterns: keyof Keymap) => { | ||
return rawPatterns.split('|').map(part => { | ||
const keys = part.split('+').map(x => x.trim().toLowerCase()); | ||
const which = parseKeyCode(keys.findLast(x => !['ctrl', 'alt', 'shift'].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; | ||
}); | ||
}; | ||
|
||
const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns']) => { | ||
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; | ||
}); | ||
}; | ||
|
||
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 [raw]; | ||
return [...raw]; | ||
}; | ||
|
||
const getValueByKey = < | ||
T extends Record<keyof any, unknown>, | ||
K extends keyof T | keyof any, | ||
>(obj: T, key: K) => { | ||
return obj[key] as K extends keyof T ? T[K] : T[keyof T] | undefined; | ||
}; | ||
//#endregion |