anki/ts/lib/shortcuts.ts

192 lines
5.2 KiB
TypeScript
Raw Normal View History

2021-04-22 16:49:30 +02:00
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "./i18n";
import { isApplePlatform } from "./platform";
import { registerPackage } from "./register-package";
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
// how modifiers are mapped
const platformModifiers = isApplePlatform()
? ["Meta", "Alt", "Shift", "Control"]
: ["Control", "Alt", "Shift", "OS"];
function modifiersToPlatformString(modifiers: string[]): string {
const displayModifiers = isApplePlatform()
? ["^", "⌥", "⇧", "⌘"]
: [`${tr.keyboardCtrl()}+`, "Alt+", `${tr.keyboardShift()}+`, "Win+"];
let result = "";
for (const modifier of modifiers) {
result += displayModifiers[platformModifiers.indexOf(modifier)];
}
return result;
}
const keyCodeLookup = {
Backspace: 8,
Delete: 46,
Tab: 9,
Enter: 13,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
"=": 187,
"-": 189,
"[": 219,
"]": 221,
"\\": 220,
";": 186,
"'": 222,
",": 188,
".": 190,
"/": 191,
"`": 192,
};
function isRequiredModifier(modifier: string): boolean {
return !modifier.endsWith("?");
}
function splitKeyCombinationString(keyCombinationString: string): string[][] {
return keyCombinationString.split(", ").map((segment) => segment.split("+"));
}
function toPlatformString(keyCombination: string[]): string {
2021-05-20 18:46:22 +02:00
return (
modifiersToPlatformString(
keyCombination.slice(0, -1).filter(isRequiredModifier)
) + keyCombination[keyCombination.length - 1]
2021-05-20 18:46:22 +02:00
);
}
export function getPlatformString(keyCombinationString: string): string {
return splitKeyCombinationString(keyCombinationString)
.map(toPlatformString)
.join(", ");
}
2021-04-21 23:59:50 +02:00
function checkKey(event: KeyboardEvent, key: number): boolean {
return event.which === key;
}
const allModifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
function partition<T>(predicate: (t: T) => boolean, items: T[]): [T[], T[]] {
const trueItems: T[] = [];
const falseItems: T[] = [];
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 =>
2021-04-21 23:59:50 +02:00
matches &&
(optionalModifiers.includes(currentModifier as Modifier) ||
event.getModifierState(platformModifiers[currentIndex]) ===
requiredModifiers.includes(currentModifier)),
2021-04-21 23:59:50 +02:00
true
);
}
const check =
(keyCode: number, modifiers: string[]) =>
(event: KeyboardEvent): boolean => {
return checkKey(event, keyCode) && checkModifiers(event, modifiers);
};
function keyToCode(key: string): number {
return keyCodeLookup[key] || key.toUpperCase().charCodeAt(0);
}
function keyCombinationToCheck(
keyCombination: string[]
): (event: KeyboardEvent) => boolean {
const keyCode = keyToCode(keyCombination[keyCombination.length - 1]);
const modifiers = keyCombination.slice(0, -1);
return check(keyCode, modifiers);
2021-04-21 23:59:50 +02:00
}
const GENERAL_KEY = 0;
const NUMPAD_KEY = 3;
2021-04-21 23:59:50 +02:00
function innerShortcut(
lastEvent: KeyboardEvent,
callback: (event: KeyboardEvent) => void,
...checks: ((event: KeyboardEvent) => boolean)[]
2021-04-21 23:59:50 +02:00
): void {
2021-04-22 15:24:27 +02:00
let interval: number;
if (checks.length === 0) {
2021-04-21 23:59:50 +02:00
callback(lastEvent);
} else {
const [nextCheck, ...restChecks] = checks;
2021-04-21 23:59:50 +02:00
const handler = (event: KeyboardEvent): void => {
if (nextCheck(event)) {
innerShortcut(event, callback, ...restChecks);
2021-04-22 15:24:27 +02:00
clearTimeout(interval);
} else if (
event.location === GENERAL_KEY ||
event.location === NUMPAD_KEY
) {
// Any non-modifier key will cancel the shortcut sequence
document.removeEventListener("keydown", handler);
2021-04-21 23:59:50 +02:00
}
};
document.addEventListener("keydown", handler, { once: true });
}
}
export function registerShortcut(
2021-04-21 23:59:50 +02:00
callback: (event: KeyboardEvent) => void,
keyCombinationString: string
2021-04-21 23:59:50 +02:00
): () => void {
const [check, ...restChecks] =
splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
2021-04-21 23:59:50 +02:00
const handler = (event: KeyboardEvent): void => {
if (check(event)) {
innerShortcut(event, callback, ...restChecks);
2021-04-21 23:59:50 +02:00
}
};
document.addEventListener("keydown", handler);
return (): void => document.removeEventListener("keydown", handler);
}
registerPackage("anki/shortcuts", {
registerShortcut,
getPlatformString,
});