summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-07-09 17:59:15 +0900
committerGitHub <noreply@github.com>2024-07-09 17:59:15 +0900
commita5407131d4d15edca924e2718902cefd81e49ee2 (patch)
treebad6d1e08071e9e14624a4d64fb851f137bb4fee /packages
parentBump release actions to v2 (develop-stable(master) branches system) (#13941) (diff)
downloadsharkey-a5407131d4d15edca924e2718902cefd81e49ee2.tar.gz
sharkey-a5407131d4d15edca924e2718902cefd81e49ee2.tar.bz2
sharkey-a5407131d4d15edca924e2718902cefd81e49ee2.zip
fix/refactor(frontend): hotkeyの改修 (#14157)
* improve(frontend): hotkeyの改修 (#234) (cherry picked from commit 678be147f4db709dadf25d007cc2e679e98a370e) * Change path, add missing script Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> * fix * fix * add missing keycodes * fix * update changelog --------- Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/src/boot/main-boot.ts27
-rw-r--r--packages/frontend/src/components/MkMediaAudio.vue47
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue47
-rw-r--r--packages/frontend/src/components/MkMenu.vue20
-rw-r--r--packages/frontend/src/components/MkModal.vue8
-rw-r--r--packages/frontend/src/components/MkNote.vue57
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue26
-rw-r--r--packages/frontend/src/directives/hotkey.ts2
-rw-r--r--packages/frontend/src/pages/antenna-timeline.vue5
-rw-r--r--packages/frontend/src/pages/timeline.vue5
-rw-r--r--packages/frontend/src/scripts/hotkey.ts169
-rw-r--r--packages/frontend/src/scripts/keycode.ts24
12 files changed, 269 insertions, 168 deletions
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index faf230a1a2..d327016317 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -13,7 +13,6 @@ import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
@@ -21,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mainRouter } from '@/router/main.js';
+import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@@ -69,14 +69,6 @@ export async function mainBoot() {
});
}
- const hotkeys = {
- 'd': (): void => {
- defaultStore.set('darkMode', !defaultStore.state.darkMode);
- },
- 's': (): void => {
- mainRouter.push('/search');
- },
- };
try {
if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1;
@@ -105,9 +97,6 @@ export async function mainBoot() {
}
if ($i) {
- // only add post shortcuts if logged in
- hotkeys['p|n'] = post;
-
defaultStore.loaded.then(() => {
if (defaultStore.state.accountSetupWizard !== -1) {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
@@ -334,7 +323,19 @@ export async function mainBoot() {
}
// shortcut
- document.addEventListener('keydown', makeHotkey(hotkeys));
+ const keymap = {
+ 'p|n': () => {
+ if ($i == null) return;
+ post();
+ },
+ 'd': () => {
+ defaultStore.set('darkMode', !defaultStore.state.darkMode);
+ },
+ 's': () => {
+ mainRouter.push('/search');
+ },
+ } as const satisfies Keymap;
+ document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
initializeSw();
}
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index ebd4fc9ca4..e8dfcc7768 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -80,6 +80,7 @@ import type { MenuItem } from '@/types/menu.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { type Keymap } from '@/scripts/hotkey.js';
import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue';
@@ -90,32 +91,44 @@ const props = defineProps<{
}>();
const keymap = {
- 'up': () => {
- if (hasFocus() && audioEl.value) {
- volume.value = Math.min(volume.value + 0.1, 1);
- }
+ 'up': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && audioEl.value) {
+ volume.value = Math.min(volume.value + 0.1, 1);
+ }
+ },
},
- 'down': () => {
- if (hasFocus() && audioEl.value) {
- volume.value = Math.max(volume.value - 0.1, 0);
- }
+ 'down': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && audioEl.value) {
+ volume.value = Math.max(volume.value - 0.1, 0);
+ }
+ },
},
- 'left': () => {
- if (hasFocus() && audioEl.value) {
- audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
- }
+ 'left': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && audioEl.value) {
+ audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
+ }
+ },
},
- 'right': () => {
- if (hasFocus() && audioEl.value) {
- audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
- }
+ 'right': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && audioEl.value) {
+ audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
+ }
+ },
},
'space': () => {
if (hasFocus()) {
togglePlayPause();
}
},
-};
+} as const satisfies Keymap;
// PlayerElもしくはその子要素にフォーカスがあるかどうか
function hasFocus() {
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 707d7c1501..7c46084c63 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -112,6 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import type { MenuItem } from '@/types/menu.js';
+import { type Keymap } from '@/scripts/hotkey.js';
import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js';
@@ -127,32 +128,44 @@ const props = defineProps<{
}>();
const keymap = {
- 'up': () => {
- if (hasFocus() && videoEl.value) {
- volume.value = Math.min(volume.value + 0.1, 1);
- }
+ 'up': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && videoEl.value) {
+ volume.value = Math.min(volume.value + 0.1, 1);
+ }
+ },
},
- 'down': () => {
- if (hasFocus() && videoEl.value) {
- volume.value = Math.max(volume.value - 0.1, 0);
- }
+ 'down': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && videoEl.value) {
+ volume.value = Math.max(volume.value - 0.1, 0);
+ }
+ },
},
- 'left': () => {
- if (hasFocus() && videoEl.value) {
- videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
- }
+ 'left': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && videoEl.value) {
+ videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
+ }
+ },
},
- 'right': () => {
- if (hasFocus() && videoEl.value) {
- videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
- }
+ 'right': {
+ allowRepeat: true,
+ callback: () => {
+ if (hasFocus() && videoEl.value) {
+ videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
+ }
+ },
},
'space': () => {
if (hasFocus()) {
togglePlayPause();
}
},
-};
+} as const satisfies Keymap;
// PlayerElもしくはその子要素にフォーカスがあるかどうか
function hasFocus() {
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index d91239b9e2..119504f744 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -98,6 +98,7 @@ import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { isTouchUsing } from '@/scripts/touch.js';
+import { type Keymap } from '@/scripts/hotkey.js';
const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
</script>
@@ -125,11 +126,20 @@ const items2 = ref<InnerMenuItem[]>();
const child = shallowRef<InstanceType<typeof XChild>>();
-const keymap = computed(() => ({
- 'up|k|shift+tab': focusUp,
- 'down|j|tab': focusDown,
- 'esc': close,
-}));
+const keymap = {
+ 'up|k|shift+tab': {
+ allowRepeat: true,
+ callback: () => focusUp(),
+ },
+ 'down|j|tab': {
+ allowRepeat: true,
+ callback: () => focusDown(),
+ },
+ 'esc': {
+ allowRepeat: true,
+ callback: () => close(false),
+ },
+} as const satisfies Keymap;
const childShowingItem = ref<MenuItem | null>();
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 9e69ab2207..264d8b6c9c 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -47,6 +47,7 @@ import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
import { defaultStore } from '@/store.js';
import { deviceKind } from '@/scripts/device-kind.js';
+import { type Keymap } from '@/scripts/hotkey.js';
function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === 'BODY') return null;
@@ -154,8 +155,11 @@ if (type.value === 'drawer') {
}
const keymap = {
- 'esc': () => emit('esc'),
-};
+ 'esc': {
+ allowRepeat: true,
+ callback: () => emit('esc'),
+ },
+} as const satisfies Keymap;
const MARGIN = 16;
const SCROLLBAR_THICKNESS = 16;
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 1313e4c58e..5f1820a379 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -198,6 +198,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js';
import { isEnabledUrlPreview } from '@/instance.js';
+import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -294,15 +295,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
}
const keymap = {
- 'r': () => reply(true),
- 'e|a|plus': () => react(true),
- 'q': () => renote(true),
- 'up|k|shift+tab': focusBefore,
- 'down|j|tab': focusAfter,
- 'esc': blur,
- 'm|o': () => showMenu(true),
- 's': () => showContent.value !== showContent.value,
-};
+ 'r': () => {
+ if (renoteCollapsed.value) return;
+ reply();
+ },
+ 'e|a|plus': () => {
+ if (renoteCollapsed.value) return;
+ react();
+ },
+ 'q': () => {
+ if (renoteCollapsed.value) return;
+ renote();
+ },
+ 'm': () => {
+ if (renoteCollapsed.value) return;
+ showMenu();
+ },
+ 'c': () => {
+ if (renoteCollapsed.value) return;
+ if (!defaultStore.state.showClipButtonInNoteFooter) return;
+ clip();
+ },
+ 'o': () => {
+ if (renoteCollapsed.value) return;
+ galleryEl.value?.openGallery();
+ },
+ 'v|enter': () => {
+ if (renoteCollapsed.value) {
+ renoteCollapsed.value = false;
+ } else if (appearNote.value.cw != null) {
+ showContent.value = !showContent.value;
+ } else if (isLong) {
+ collapsed.value = !collapsed.value;
+ }
+ },
+ 'esc': {
+ allowRepeat: true,
+ callback: () => blur(),
+ },
+ 'up|k|shift+tab': {
+ allowRepeat: true,
+ callback: () => focusBefore(),
+ },
+ 'down|j|tab': {
+ allowRepeat: true,
+ callback: () => focusAfter(),
+ },
+} as const satisfies Keymap;
provide('react', (reaction: string) => {
misskeyApi('notes/reactions/create', {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index bc1f416373..8f65e3b60a 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -233,6 +233,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { isEnabledUrlPreview } from '@/instance.js';
+import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -294,13 +295,24 @@ const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const keymap = {
- 'r': () => reply(true),
- 'e|a|plus': () => react(true),
- 'q': () => renote(true),
- 'esc': blur,
- 'm|o': () => showMenu(true),
- 's': () => showContent.value !== showContent.value,
-};
+ 'r': () => reply(),
+ 'e|a|plus': () => react(),
+ 'q': () => renote(),
+ 'm': () => showMenu(),
+ 'c': () => {
+ if (!defaultStore.state.showClipButtonInNoteFooter) return;
+ clip();
+ },
+ 'v|enter': () => {
+ if (appearNote.value.cw != null) {
+ showContent.value = !showContent.value;
+ }
+ },
+ 'esc': {
+ allowRepeat: true,
+ callback: () => blur(),
+ },
+} as const satisfies Keymap;
provide('react', (reaction: string) => {
misskeyApi('notes/reactions/create', {
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
index b082b6edf2..0a7d136f18 100644
--- a/packages/frontend/src/directives/hotkey.ts
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -4,7 +4,7 @@
*/
import { Directive } from 'vue';
-import { makeHotkey } from '../scripts/hotkey.js';
+import { makeHotkey } from '@/scripts/hotkey.js';
export default {
mounted(el, binding) {
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index 273250d1d0..ea64e457e3 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
- <div ref="rootEl" v-hotkey.global="keymap">
+ <div ref="rootEl">
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<MkTimeline
@@ -44,9 +44,6 @@ const antenna = ref<Misskey.entities.Antenna | null>(null);
const queue = ref(0);
const rootEl = shallowRef<HTMLElement>();
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
-const keymap = computed(() => ({
- 't': focus,
-}));
function queueUpdated(q) {
queue.value = q;
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 98744c6318..813cc326d0 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
- <div :key="src" ref="rootEl" v-hotkey.global="keymap">
+ <div :key="src" ref="rootEl">
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
@@ -58,9 +58,6 @@ provide('shouldOmitHeaderTitle', true);
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
-const keymap = {
- 't': focus,
-};
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();
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'],
-};