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
|
2021-04-22 13:04:24 +02:00
|
|
|
import * as tr from "./i18n";
|
|
|
|
|
2021-04-22 17:52:27 +02:00
|
|
|
export type Modifier = "Control" | "Alt" | "Shift" | "Meta";
|
2021-04-22 17:28:38 +02:00
|
|
|
|
|
|
|
const modifiers: Modifier[] = ["Control", "Alt", "Shift", "Meta"];
|
2021-04-21 23:59:50 +02:00
|
|
|
|
2021-04-23 03:22:30 +02:00
|
|
|
function isApplePlatform(): boolean {
|
|
|
|
return (
|
|
|
|
window.navigator.platform.startsWith("Mac") ||
|
|
|
|
window.navigator.platform.startsWith("iP")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-22 13:04:24 +02:00
|
|
|
// how modifiers are mapped
|
2021-04-23 03:22:30 +02:00
|
|
|
const platformModifiers = isApplePlatform()
|
|
|
|
? ["Meta", "Alt", "Shift", "Control"]
|
|
|
|
: ["Control", "Alt", "Shift", "OS"];
|
2021-04-22 13:04:24 +02:00
|
|
|
|
|
|
|
function modifiersToPlatformString(modifiers: string[]): string {
|
2021-04-23 03:22:30 +02:00
|
|
|
const displayModifiers = isApplePlatform()
|
|
|
|
? ["^", "⌥", "⇧", "⌘"]
|
|
|
|
: [`${tr.keyboardCtrl()}+`, "Alt+", `${tr.keyboardShift()}+`, "Win+"];
|
2021-04-22 13:04:24 +02:00
|
|
|
|
|
|
|
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 {
|
2021-04-23 05:00:18 +02:00
|
|
|
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;
|
|
|
|
}
|
2021-04-22 13:04:24 +02:00
|
|
|
}
|
|
|
|
|
2021-04-22 15:24:27 +02:00
|
|
|
function toPlatformString(modifiersAndKey: string[]): string {
|
2021-04-22 13:04:24 +02:00
|
|
|
return `${modifiersToPlatformString(
|
|
|
|
modifiersAndKey.slice(0, -1)
|
|
|
|
)}${keyToPlatformString(modifiersAndKey[modifiersAndKey.length - 1])}`;
|
|
|
|
}
|
|
|
|
|
2021-05-20 18:28:59 +02:00
|
|
|
export function getPlatformString(keyCombination: string[][]): string {
|
|
|
|
return keyCombination.map(toPlatformString).join(", ");
|
2021-04-22 13:04:24 +02:00
|
|
|
}
|
2021-04-21 23:59:50 +02:00
|
|
|
|
|
|
|
function checkKey(event: KeyboardEvent, key: string): boolean {
|
2021-05-20 18:28:59 +02:00
|
|
|
return event.key === key;
|
2021-04-21 23:59:50 +02:00
|
|
|
}
|
|
|
|
|
2021-04-22 17:28:38 +02:00
|
|
|
function checkModifiers(
|
|
|
|
event: KeyboardEvent,
|
|
|
|
optionalModifiers: Modifier[],
|
|
|
|
activeModifiers: string[]
|
|
|
|
): boolean {
|
2021-04-21 23:59:50 +02:00
|
|
|
return modifiers.reduce(
|
|
|
|
(matches: boolean, modifier: string, currentIndex: number): boolean =>
|
|
|
|
matches &&
|
2021-04-22 17:28:38 +02:00
|
|
|
(optionalModifiers.includes(modifier as Modifier) ||
|
|
|
|
event.getModifierState(platformModifiers[currentIndex]) ===
|
|
|
|
activeModifiers.includes(modifier)),
|
2021-04-21 23:59:50 +02:00
|
|
|
true
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-22 17:28:38 +02:00
|
|
|
function check(
|
|
|
|
event: KeyboardEvent,
|
|
|
|
optionalModifiers: Modifier[],
|
|
|
|
modifiersAndKey: string[]
|
|
|
|
): boolean {
|
2021-04-21 23:59:50 +02:00
|
|
|
return (
|
|
|
|
checkKey(event, modifiersAndKey[modifiersAndKey.length - 1]) &&
|
2021-04-22 17:28:38 +02:00
|
|
|
checkModifiers(event, optionalModifiers, modifiersAndKey.slice(0, -1))
|
2021-04-21 23:59:50 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function innerShortcut(
|
|
|
|
lastEvent: KeyboardEvent,
|
|
|
|
callback: (event: KeyboardEvent) => void,
|
2021-04-22 17:28:38 +02:00
|
|
|
optionalModifiers: Modifier[],
|
2021-04-22 13:04:24 +02:00
|
|
|
...keyCombination: string[][]
|
2021-04-21 23:59:50 +02:00
|
|
|
): void {
|
2021-04-22 15:24:27 +02:00
|
|
|
let interval: number;
|
|
|
|
|
2021-04-22 13:04:24 +02:00
|
|
|
if (keyCombination.length === 0) {
|
2021-04-21 23:59:50 +02:00
|
|
|
callback(lastEvent);
|
|
|
|
} else {
|
2021-04-22 13:04:24 +02:00
|
|
|
const [nextKey, ...restKeys] = keyCombination;
|
2021-04-21 23:59:50 +02:00
|
|
|
|
|
|
|
const handler = (event: KeyboardEvent): void => {
|
2021-04-22 17:28:38 +02:00
|
|
|
if (check(event, optionalModifiers, nextKey)) {
|
|
|
|
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
2021-04-22 15:24:27 +02:00
|
|
|
clearTimeout(interval);
|
2021-05-20 18:32:53 +02:00
|
|
|
} else if (event.location === 0) {
|
|
|
|
// 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 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-22 01:14:38 +02:00
|
|
|
export function registerShortcut(
|
2021-04-21 23:59:50 +02:00
|
|
|
callback: (event: KeyboardEvent) => void,
|
2021-05-20 18:28:59 +02:00
|
|
|
keyCombination: string[][],
|
2021-04-22 17:28:38 +02:00
|
|
|
optionalModifiers: Modifier[] = []
|
2021-04-21 23:59:50 +02:00
|
|
|
): () => void {
|
2021-04-22 13:04:24 +02:00
|
|
|
const [firstKey, ...restKeys] = keyCombination;
|
2021-04-21 23:59:50 +02:00
|
|
|
|
|
|
|
const handler = (event: KeyboardEvent): void => {
|
2021-04-22 17:28:38 +02:00
|
|
|
if (check(event, optionalModifiers, firstKey)) {
|
|
|
|
innerShortcut(event, callback, optionalModifiers, ...restKeys);
|
2021-04-21 23:59:50 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener("keydown", handler);
|
|
|
|
return (): void => document.removeEventListener("keydown", handler);
|
|
|
|
}
|