mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Base shortcuts with letters no event.key, with symbols/numbers on event.code
This commit is contained in:
parent
5ef056a23e
commit
c89c42dc37
6 changed files with 79 additions and 38 deletions
|
@ -3,13 +3,11 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { Modifier } from "lib/shortcuts";
|
|
||||||
|
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
||||||
|
|
||||||
export let shortcut: string[][];
|
export let shortcut: string[][];
|
||||||
export let optionalModifiers: Modifier[] | undefined = [];
|
export let useCode = false;
|
||||||
|
|
||||||
const shortcutLabel = getPlatformString(shortcut);
|
const shortcutLabel = getPlatformString(shortcut);
|
||||||
|
|
||||||
|
@ -23,7 +21,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
shortcut,
|
shortcut,
|
||||||
optionalModifiers
|
useCode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
let registerCleanup: () => void;
|
let registerCleanup: () => void;
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
registerCleanup = registerShortcut(() => state.save(false), [
|
registerCleanup = registerShortcut(
|
||||||
["Control", "Enter"],
|
() => state.save(false),
|
||||||
]);
|
[["Control", "Enter"]],
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
onDestroy(() => registerCleanup?.());
|
onDestroy(() => registerCleanup?.());
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -42,8 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut={[['Control', 'Shift', 'C']]}
|
shortcut={[['Control', 'Alt?', 'Shift', 'C']]}
|
||||||
optionalModifiers={['Alt']}
|
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -99,7 +99,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut={[['Control', '+']]}
|
shortcut={[['Control', 'Equal']]}
|
||||||
|
useCode={true}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<WithState
|
<WithState
|
||||||
|
@ -123,7 +124,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<WithShortcut
|
<WithShortcut
|
||||||
shortcut={[['Control', '=']]}
|
shortcut={[['Control', 'Shift', 'Equal']]}
|
||||||
|
useCode={true}
|
||||||
let:createShortcut
|
let:createShortcut
|
||||||
let:shortcutLabel>
|
let:shortcutLabel>
|
||||||
<WithState
|
<WithState
|
||||||
|
|
|
@ -66,8 +66,8 @@ function updateFocus(evt: FocusEvent) {
|
||||||
|
|
||||||
registerShortcut(
|
registerShortcut(
|
||||||
() => document.addEventListener("focusin", updateFocus, { once: true }),
|
() => document.addEventListener("focusin", updateFocus, { once: true }),
|
||||||
[["Tab"]],
|
[["Shift?", "Tab"]],
|
||||||
["Shift"]
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
export function onKeyUp(evt: KeyboardEvent): void {
|
export function onKeyUp(evt: KeyboardEvent): void {
|
||||||
|
|
|
@ -4,8 +4,6 @@ import * as tr from "./i18n";
|
||||||
|
|
||||||
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
|
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
|
||||||
|
|
||||||
const modifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
|
|
||||||
|
|
||||||
function isApplePlatform(): boolean {
|
function isApplePlatform(): boolean {
|
||||||
return (
|
return (
|
||||||
window.navigator.platform.startsWith("Mac") ||
|
window.navigator.platform.startsWith("Mac") ||
|
||||||
|
@ -64,10 +62,15 @@ function capitalize(key: string): string {
|
||||||
return key[0].toLocaleUpperCase() + key.slice(1);
|
return key[0].toLocaleUpperCase() + key.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRequiredModifier(modifier: string): boolean {
|
||||||
|
return !modifier.endsWith("?");
|
||||||
|
}
|
||||||
|
|
||||||
function toPlatformString(modifiersAndKey: string[]): string {
|
function toPlatformString(modifiersAndKey: string[]): string {
|
||||||
return (
|
return (
|
||||||
modifiersToPlatformString(modifiersAndKey.slice(0, -1)) +
|
modifiersToPlatformString(
|
||||||
capitalize(keyToPlatformString(modifiersAndKey[modifiersAndKey.length - 1]))
|
modifiersAndKey.slice(0, -1).filter(isRequiredModifier)
|
||||||
|
) + capitalize(keyToPlatformString(modifiersAndKey[modifiersAndKey.length - 1]))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,33 +78,58 @@ export function getPlatformString(keyCombination: string[][]): string {
|
||||||
return keyCombination.map(toPlatformString).join(", ");
|
return keyCombination.map(toPlatformString).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkKey(event: KeyboardEvent, key: string): boolean {
|
function checkKey(
|
||||||
return event.key === key;
|
getProperty: (event: KeyboardEvent) => string,
|
||||||
|
event: KeyboardEvent,
|
||||||
|
key: string
|
||||||
|
): boolean {
|
||||||
|
return getProperty(event) === key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkModifiers(
|
const allModifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
|
||||||
event: KeyboardEvent,
|
|
||||||
optionalModifiers: Modifier[],
|
function partition<T>(predicate: (t: T) => boolean, items: T[]): [T[], T[]] {
|
||||||
activeModifiers: string[]
|
const trueItems: T[] = [];
|
||||||
): boolean {
|
const falseItems: T[] = [];
|
||||||
return modifiers.reduce(
|
|
||||||
(matches: boolean, modifier: string, currentIndex: number): boolean =>
|
items.forEach((t) => {
|
||||||
|
const target = predicate(t) ? trueItems : falseItems;
|
||||||
|
target.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [trueItems, falseItems];
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTrailing(modifier: string): string {
|
||||||
|
return modifier.substring(0, modifier.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkModifiers(event: KeyboardEvent, modifiers: string[]): boolean {
|
||||||
|
const [requiredModifiers, otherModifiers] = partition(
|
||||||
|
isRequiredModifier,
|
||||||
|
modifiers
|
||||||
|
);
|
||||||
|
|
||||||
|
const optionalModifiers = otherModifiers.map(removeTrailing);
|
||||||
|
|
||||||
|
return allModifiers.reduce(
|
||||||
|
(matches: boolean, currentModifier: string, currentIndex: number): boolean =>
|
||||||
matches &&
|
matches &&
|
||||||
(optionalModifiers.includes(modifier as Modifier) ||
|
(optionalModifiers.includes(currentModifier as Modifier) ||
|
||||||
event.getModifierState(platformModifiers[currentIndex]) ===
|
event.getModifierState(platformModifiers[currentIndex]) ===
|
||||||
activeModifiers.includes(modifier)),
|
requiredModifiers.includes(currentModifier)),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function check(
|
function check(
|
||||||
|
getProperty: (event: KeyboardEvent) => string,
|
||||||
event: KeyboardEvent,
|
event: KeyboardEvent,
|
||||||
optionalModifiers: Modifier[],
|
|
||||||
modifiersAndKey: string[]
|
modifiersAndKey: string[]
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
checkKey(event, modifiersAndKey[modifiersAndKey.length - 1]) &&
|
checkModifiers(event, modifiersAndKey.slice(0, -1)) &&
|
||||||
checkModifiers(event, optionalModifiers, modifiersAndKey.slice(0, -1))
|
checkKey(getProperty, event, modifiersAndKey[modifiersAndKey.length - 1])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +139,7 @@ const NUMPAD_KEY = 3;
|
||||||
function innerShortcut(
|
function innerShortcut(
|
||||||
lastEvent: KeyboardEvent,
|
lastEvent: KeyboardEvent,
|
||||||
callback: (event: KeyboardEvent) => void,
|
callback: (event: KeyboardEvent) => void,
|
||||||
optionalModifiers: Modifier[],
|
getProperty: (event: KeyboardEvent) => string,
|
||||||
...keyCombination: string[][]
|
...keyCombination: string[][]
|
||||||
): void {
|
): void {
|
||||||
let interval: number;
|
let interval: number;
|
||||||
|
@ -122,10 +150,13 @@ function innerShortcut(
|
||||||
const [nextKey, ...restKeys] = keyCombination;
|
const [nextKey, ...restKeys] = keyCombination;
|
||||||
|
|
||||||
const handler = (event: KeyboardEvent): void => {
|
const handler = (event: KeyboardEvent): void => {
|
||||||
if (check(event, optionalModifiers, nextKey)) {
|
if (check(getProperty, event, nextKey)) {
|
||||||
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
innerShortcut(event, callback, getProperty, ...restKeys);
|
||||||
clearTimeout(interval);
|
clearTimeout(interval);
|
||||||
} else if (event.location === GENERAL_KEY || event.location === NUMPAD_KEY) {
|
} else if (
|
||||||
|
event.location === GENERAL_KEY ||
|
||||||
|
event.location === NUMPAD_KEY
|
||||||
|
) {
|
||||||
// Any non-modifier key will cancel the shortcut sequence
|
// Any non-modifier key will cancel the shortcut sequence
|
||||||
document.removeEventListener("keydown", handler);
|
document.removeEventListener("keydown", handler);
|
||||||
}
|
}
|
||||||
|
@ -135,16 +166,25 @@ function innerShortcut(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function byKey(event: KeyboardEvent): string {
|
||||||
|
return event.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function byCode(event: KeyboardEvent): string {
|
||||||
|
return event.code;
|
||||||
|
}
|
||||||
|
|
||||||
export function registerShortcut(
|
export function registerShortcut(
|
||||||
callback: (event: KeyboardEvent) => void,
|
callback: (event: KeyboardEvent) => void,
|
||||||
keyCombination: string[][],
|
keyCombination: string[][],
|
||||||
optionalModifiers: Modifier[] = []
|
useCode = false
|
||||||
): () => void {
|
): () => void {
|
||||||
const [firstKey, ...restKeys] = keyCombination;
|
const [firstKey, ...restKeys] = keyCombination;
|
||||||
|
const getProperty = useCode ? byCode : byKey;
|
||||||
|
|
||||||
const handler = (event: KeyboardEvent): void => {
|
const handler = (event: KeyboardEvent): void => {
|
||||||
if (check(event, optionalModifiers, firstKey)) {
|
if (check(getProperty, event, firstKey)) {
|
||||||
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
innerShortcut(event, callback, getProperty, ...restKeys);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue