Base shortcuts with letters no event.key, with symbols/numbers on event.code

This commit is contained in:
Henrik Giesel 2021-05-21 22:45:55 +02:00
parent 5ef056a23e
commit c89c42dc37
6 changed files with 79 additions and 38 deletions

View file

@ -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
); );
} }

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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);
} }
}; };