mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
193 lines
5.4 KiB
TypeScript
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);
|
|
}
|