b2049209ff
There are some style differences compared to prettier, and not all are necessarily an improvement, but it's much faster now.
179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import { on } from "./events";
|
|
import type { Modifier } from "./keys";
|
|
import { checkIfModifierKey, checkModifiers, keyToPlatformString, modifiersToPlatformString } from "./keys";
|
|
import { registerPackage } from "./runtime-require";
|
|
|
|
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 {
|
|
return (
|
|
modifiersToPlatformString(
|
|
keyCombination.slice(0, -1).filter(isRequiredModifier),
|
|
) + keyToPlatformString(keyCombination[keyCombination.length - 1])
|
|
);
|
|
}
|
|
|
|
export function getPlatformString(keyCombinationString: string): string {
|
|
return splitKeyCombinationString(keyCombinationString)
|
|
.map(toPlatformString)
|
|
.join(", ");
|
|
}
|
|
|
|
function checkKey(event: KeyboardEvent, key: number): boolean {
|
|
// avoid deprecation warning
|
|
const which = event["which" + ""];
|
|
return which === key;
|
|
}
|
|
|
|
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 separateRequiredOptionalModifiers(
|
|
modifiers: string[],
|
|
): [Modifier[], Modifier[]] {
|
|
const [requiredModifiers, otherModifiers] = partition(
|
|
isRequiredModifier,
|
|
modifiers,
|
|
);
|
|
|
|
const optionalModifiers = otherModifiers.map(removeTrailing);
|
|
return [requiredModifiers as Modifier[], optionalModifiers as Modifier[]];
|
|
}
|
|
|
|
const check =
|
|
(keyCode: number, requiredModifiers: Modifier[], optionalModifiers: Modifier[]) =>
|
|
(event: KeyboardEvent): boolean => {
|
|
return (
|
|
checkKey(event, keyCode)
|
|
&& checkModifiers(requiredModifiers, optionalModifiers)(event)
|
|
);
|
|
};
|
|
|
|
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 [required, optional] = separateRequiredOptionalModifiers(
|
|
keyCombination.slice(0, -1),
|
|
);
|
|
|
|
return check(keyCode, required, optional);
|
|
}
|
|
|
|
function innerShortcut(
|
|
target: EventTarget | Document,
|
|
lastEvent: KeyboardEvent,
|
|
callback: (event: KeyboardEvent) => void,
|
|
...checks: ((event: KeyboardEvent) => boolean)[]
|
|
): void {
|
|
if (checks.length === 0) {
|
|
return callback(lastEvent);
|
|
}
|
|
|
|
const [nextCheck, ...restChecks] = checks;
|
|
const remove = on(document, "keydown", handler, { once: true });
|
|
|
|
function handler(event: KeyboardEvent): void {
|
|
if (nextCheck(event)) {
|
|
innerShortcut(target, event, callback, ...restChecks);
|
|
} else if (!checkIfModifierKey(event)) {
|
|
// Any non-modifier key will cancel the shortcut sequence
|
|
remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface RegisterShortcutRestParams {
|
|
target: EventTarget;
|
|
/// There might be no good reason to use `keyup` other
|
|
/// than to circumvent Qt bugs
|
|
event: "keydown" | "keyup";
|
|
}
|
|
|
|
const defaultRegisterShortcutRestParams = {
|
|
target: document,
|
|
event: "keydown" as const,
|
|
};
|
|
|
|
export function registerShortcut(
|
|
callback: (event: KeyboardEvent) => void,
|
|
keyCombinationString: string,
|
|
restParams: Partial<RegisterShortcutRestParams> = defaultRegisterShortcutRestParams,
|
|
): () => void {
|
|
const {
|
|
target = defaultRegisterShortcutRestParams.target,
|
|
event = defaultRegisterShortcutRestParams.event,
|
|
} = restParams;
|
|
|
|
const [check, ...restChecks] = splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
|
|
|
|
function handler(event: KeyboardEvent): void {
|
|
if (check(event)) {
|
|
innerShortcut(target, event, callback, ...restChecks);
|
|
}
|
|
}
|
|
|
|
return on(target, event, handler);
|
|
}
|
|
|
|
registerPackage("anki/shortcuts", {
|
|
registerShortcut,
|
|
getPlatformString,
|
|
});
|