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-06-05 03:28:36 +02:00
|
|
|
|
2021-06-29 17:15:07 +02:00
|
|
|
import type { Modifier } from "./keys";
|
2021-04-22 13:04:24 +02:00
|
|
|
|
2021-06-29 17:15:07 +02:00
|
|
|
import { registerPackage } from "./register-package";
|
2021-07-06 15:52:47 +02:00
|
|
|
import { modifiersToPlatformString, keyToPlatformString, checkModifiers } from "./keys";
|
2021-04-22 13:04:24 +02:00
|
|
|
|
2021-05-22 17:50:23 +02:00
|
|
|
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,
|
2021-04-22 13:04:24 +02:00
|
|
|
};
|
|
|
|
|
2021-05-21 22:45:55 +02:00
|
|
|
function isRequiredModifier(modifier: string): boolean {
|
|
|
|
return !modifier.endsWith("?");
|
|
|
|
}
|
|
|
|
|
2021-05-22 17:50:23 +02:00
|
|
|
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 (
|
2021-05-21 22:45:55 +02:00
|
|
|
modifiersToPlatformString(
|
2021-10-19 01:06:00 +02:00
|
|
|
keyCombination.slice(0, -1).filter(isRequiredModifier),
|
2021-07-06 15:52:47 +02:00
|
|
|
) + keyToPlatformString(keyCombination[keyCombination.length - 1])
|
2021-05-20 18:46:22 +02:00
|
|
|
);
|
2021-04-22 13:04:24 +02:00
|
|
|
}
|
|
|
|
|
2021-05-22 17:50:23 +02:00
|
|
|
export function getPlatformString(keyCombinationString: string): string {
|
|
|
|
return splitKeyCombinationString(keyCombinationString)
|
|
|
|
.map(toPlatformString)
|
|
|
|
.join(", ");
|
2021-04-22 13:04:24 +02:00
|
|
|
}
|
2021-04-21 23:59:50 +02:00
|
|
|
|
2021-05-22 17:50:23 +02:00
|
|
|
function checkKey(event: KeyboardEvent, key: number): boolean {
|
|
|
|
return event.which === key;
|
2021-05-21 22:45:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-06-29 16:50:54 +02:00
|
|
|
// function checkModifiers(event: KeyboardEvent, modifiers: string[]): boolean {
|
|
|
|
function separateRequiredOptionalModifiers(
|
2021-10-19 01:06:00 +02:00
|
|
|
modifiers: string[],
|
2021-06-29 16:50:54 +02:00
|
|
|
): [Modifier[], Modifier[]] {
|
2021-05-21 22:45:55 +02:00
|
|
|
const [requiredModifiers, otherModifiers] = partition(
|
|
|
|
isRequiredModifier,
|
2021-10-19 01:06:00 +02:00
|
|
|
modifiers,
|
2021-05-21 22:45:55 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
const optionalModifiers = otherModifiers.map(removeTrailing);
|
2021-06-29 16:50:54 +02:00
|
|
|
return [requiredModifiers as Modifier[], optionalModifiers as Modifier[]];
|
2021-04-21 23:59:50 +02:00
|
|
|
}
|
|
|
|
|
2021-05-26 01:21:33 +02:00
|
|
|
const check =
|
|
|
|
(keyCode: number, modifiers: string[]) =>
|
|
|
|
(event: KeyboardEvent): boolean => {
|
2021-06-29 16:50:54 +02:00
|
|
|
const [required, optional] = separateRequiredOptionalModifiers(modifiers);
|
|
|
|
|
|
|
|
return checkKey(event, keyCode) && checkModifiers(required, optional)(event);
|
2021-05-26 01:21:33 +02:00
|
|
|
};
|
2021-05-22 17:50:23 +02:00
|
|
|
|
|
|
|
function keyToCode(key: string): number {
|
|
|
|
return keyCodeLookup[key] || key.toUpperCase().charCodeAt(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
function keyCombinationToCheck(
|
2021-10-19 01:06:00 +02:00
|
|
|
keyCombination: string[],
|
2021-05-22 17:50:23 +02:00
|
|
|
): (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
|
|
|
}
|
|
|
|
|
2021-05-21 19:03:06 +02:00
|
|
|
const GENERAL_KEY = 0;
|
|
|
|
const NUMPAD_KEY = 3;
|
|
|
|
|
2021-04-21 23:59:50 +02:00
|
|
|
function innerShortcut(
|
2021-10-18 14:01:15 +02:00
|
|
|
target: EventTarget | Document,
|
2021-04-21 23:59:50 +02:00
|
|
|
lastEvent: KeyboardEvent,
|
|
|
|
callback: (event: KeyboardEvent) => void,
|
2021-05-22 17:50:23 +02:00
|
|
|
...checks: ((event: KeyboardEvent) => boolean)[]
|
2021-04-21 23:59:50 +02:00
|
|
|
): void {
|
2021-04-22 15:24:27 +02:00
|
|
|
let interval: number;
|
|
|
|
|
2021-05-22 17:50:23 +02:00
|
|
|
if (checks.length === 0) {
|
2021-04-21 23:59:50 +02:00
|
|
|
callback(lastEvent);
|
|
|
|
} else {
|
2021-05-22 17:50:23 +02:00
|
|
|
const [nextCheck, ...restChecks] = checks;
|
2021-04-21 23:59:50 +02:00
|
|
|
const handler = (event: KeyboardEvent): void => {
|
2021-05-22 17:50:23 +02:00
|
|
|
if (nextCheck(event)) {
|
2021-10-18 14:01:15 +02:00
|
|
|
innerShortcut(target, event, callback, ...restChecks);
|
2021-04-22 15:24:27 +02:00
|
|
|
clearTimeout(interval);
|
2021-05-21 22:45:55 +02:00
|
|
|
} else if (
|
|
|
|
event.location === GENERAL_KEY ||
|
|
|
|
event.location === NUMPAD_KEY
|
|
|
|
) {
|
2021-05-20 18:32:53 +02:00
|
|
|
// 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-10-18 14:01:15 +02:00
|
|
|
keyCombinationString: string,
|
2021-10-19 01:06:00 +02:00
|
|
|
target: EventTarget | Document = document,
|
2021-04-21 23:59:50 +02:00
|
|
|
): () => void {
|
2021-05-26 01:21:33 +02:00
|
|
|
const [check, ...restChecks] =
|
|
|
|
splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
|
2021-04-21 23:59:50 +02:00
|
|
|
|
|
|
|
const handler = (event: KeyboardEvent): void => {
|
2021-05-22 17:50:23 +02:00
|
|
|
if (check(event)) {
|
2021-10-18 14:01:15 +02:00
|
|
|
innerShortcut(target, event, callback, ...restChecks);
|
2021-04-21 23:59:50 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-10-18 14:01:15 +02:00
|
|
|
target.addEventListener("keydown", handler as EventListener);
|
|
|
|
return (): void => target.removeEventListener("keydown", handler as EventListener);
|
2021-04-21 23:59:50 +02:00
|
|
|
}
|
2021-08-30 14:41:40 +02:00
|
|
|
|
|
|
|
registerPackage("anki/shortcuts", {
|
|
|
|
registerShortcut,
|
|
|
|
getPlatformString,
|
|
|
|
});
|