Anki/ts/lib/tslib/shortcuts.ts
Damien Elmes 9f55cf26fc
Switch to SvelteKit (#3077)
* Update to latest Node LTS

* Add sveltekit

* Split tslib into separate @generated and @tslib components

SvelteKit's path aliases don't support multiple locations, so our old
approach of using @tslib to refer to both ts/lib and out/ts/lib will no
longer work. Instead, all generated sources and their includes are
placed in a separate out/ts/generated folder, and imported via @generated
instead. This also allows us to generate .ts files, instead of needing
to output separate .d.ts and .js files.

* Switch package.json to module type

* Avoid usage of baseUrl

Incompatible with SvelteKit

* Move sass into ts; use relative links

SvelteKit's default sass support doesn't allow overriding loadPaths

* jest->vitest, graphs example working with yarn dev

* most pages working in dev mode

* Some fixes after rebasing

* Fix/silence some svelte-check errors

* Get image-occlusion working with Fabric types

* Post-rebase lock changes

* Editor is now checked

* SvelteKit build integrated into ninja

* Use the new SvelteKit entrypoint for pages like congrats/deck options/etc

* Run eslint once for ts/**; fix some tests

* Fix a bunch of issues introduced when rebasing over latest main

* Run eslint fix

* Fix remaining eslint+pylint issues; tests now all pass

* Fix some issues with a clean build

* Latest bufbuild no longer requires @__PURE__ hack

* Add a few missed dependencies

* Add yarn.bat to fix Windows build

* Fix pages failing to show when ANKI_API_PORT not defined

* Fix svelte-check and vitest on Windows

* Set node path in ./yarn

* Move svelte-kit output to ts/.svelte-kit

Sadly, I couldn't figure out a way to store it in out/ if out/ is
a symlink, as it breaks module resolution when SvelteKit is run.

* Allow HMR inside Anki

* Skip SvelteKit build when HMR is defined

* Fix some post-rebase issues

I should have done a normal merge instead.
2024-03-31 09:16:31 +01:00

178 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,
});