Anki/ts/lib/shortcuts.ts

193 lines
5.4 KiB
TypeScript

// 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";
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
function isApplePlatform(): boolean {
return (
window.navigator.platform.startsWith("Mac") ||
window.navigator.platform.startsWith("iP")
);
}
// 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 alphabeticPrefix = "Key";
const numericPrefix = "Digit";
const keyToCharacterMap = {
Backslash: "\\",
Backquote: "`",
BracketLeft: "[",
BrackerRight: "]",
Quote: "'",
Semicolon: ";",
Minus: "-",
Equal: "=",
Comma: ",",
Period: ".",
Slash: "/",
};
function keyToPlatformString(key: string): string {
if (key.startsWith(alphabeticPrefix)) {
return key.slice(alphabeticPrefix.length);
} else if (key.startsWith(numericPrefix)) {
return key.slice(numericPrefix.length);
} else if (Object.prototype.hasOwnProperty.call(keyToCharacterMap, key)) {
return keyToCharacterMap[key];
} else {
return key;
}
}
function capitalize(key: string): string {
return key[0].toLocaleUpperCase() + key.slice(1);
}
function isRequiredModifier(modifier: string): boolean {
return !modifier.endsWith("?");
}
function toPlatformString(modifiersAndKey: string[]): string {
return (
modifiersToPlatformString(
modifiersAndKey.slice(0, -1).filter(isRequiredModifier)
) + capitalize(keyToPlatformString(modifiersAndKey[modifiersAndKey.length - 1]))
);
}
export function getPlatformString(keyCombination: string[][]): string {
return keyCombination.map(toPlatformString).join(", ");
}
function checkKey(
getProperty: (event: KeyboardEvent) => string,
event: KeyboardEvent,
key: string
): boolean {
return getProperty(event) === 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 =>
matches &&
(optionalModifiers.includes(currentModifier as Modifier) ||
event.getModifierState(platformModifiers[currentIndex]) ===
requiredModifiers.includes(currentModifier)),
true
);
}
function check(
getProperty: (event: KeyboardEvent) => string,
event: KeyboardEvent,
modifiersAndKey: string[]
): boolean {
return (
checkModifiers(event, modifiersAndKey.slice(0, -1)) &&
checkKey(getProperty, event, modifiersAndKey[modifiersAndKey.length - 1])
);
}
const GENERAL_KEY = 0;
const NUMPAD_KEY = 3;
function innerShortcut(
lastEvent: KeyboardEvent,
callback: (event: KeyboardEvent) => void,
getProperty: (event: KeyboardEvent) => string,
...keyCombination: string[][]
): void {
let interval: number;
if (keyCombination.length === 0) {
callback(lastEvent);
} else {
const [nextKey, ...restKeys] = keyCombination;
const handler = (event: KeyboardEvent): void => {
if (check(getProperty, event, nextKey)) {
innerShortcut(event, callback, getProperty, ...restKeys);
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);
}
};
document.addEventListener("keydown", handler, { once: true });
}
}
function byKey(event: KeyboardEvent): string {
return event.key;
}
function byCode(event: KeyboardEvent): string {
return event.code;
}
export function registerShortcut(
callback: (event: KeyboardEvent) => void,
keyCombination: string[][],
useCode = false
): () => void {
const [firstKey, ...restKeys] = keyCombination;
const getProperty = useCode ? byCode : byKey;
const handler = (event: KeyboardEvent): void => {
if (check(getProperty, event, firstKey)) {
innerShortcut(event, callback, getProperty, ...restKeys);
}
};
document.addEventListener("keydown", handler);
return (): void => document.removeEventListener("keydown", handler);
}