anki/ts/lib/shortcuts.ts
Henrik Giesel a981e56008
Improved add-on extension API (#1626)
* Add componentHook functionality

* Register package NoteEditor

* Rename OldEditorAdapter to NoteEditor

* Expose instances in component-hook as well

* Rename NoteTypeButtons to NotetypeButtons

* Move PreviewButton initialization to BrowserEditor.svelte

* Remove focusInRichText

- Same thing can be done by inspecting activeInput

* Satisfy formatter

* Fix remaining rebase issues

* Add .bazel to .prettierignore

* Rename currentField and activeInput to focused{Field,Input}

* Move identifier to lib and registration to sveltelib

* Fix Dynamic component insertion

* Simplify editingInputIsRichText

* Give extra warning in svelte/svelte.ts

- This was caused by doing a rename of a files, that only differed in
  case: NoteTypeButtons.svelte to NotetypeButtons.svelte
- It was quite tough to figure out, and this console.log might make it
  easier if it ever happens again

* Change signature of contextProperty

* Add ts/typings for add-on definition files

* Add Anki types in typings/common/index.d.ts

* Export without .svelte suffix

It conflicts with how Svelte types its packages

* Fix left over .svelte import from editor.py

* Rename NoteTypeButtons to unrelated to ensure case-only rename

* Rename back to NotetypeButtons.svelte

* Remove unused component-hook.ts, Fix typing in lifecycle-hooks

* Merge runtime-require and register-package into one file

+ Give some preliminary types to require

* Rename uiDidLoad to loaded

* Fix eslint / svelte-check

* Rename context imports to noteEditorContext

* Fix import name mismatch

- I wonder why these issues are not caught by svelte-check?

* Rename two missed usages of uiDidLoad

* Fix ButtonDropdown from having wrong border-radius

* Uniformly rename libraries to packages

- I don't have a strong opinion on whether to name them libraries or
  packages, I just think we should have a uniform name.
- JS/TS only uses the terms "module" and "namespace", however `package`
  is a reserved keyword for future use, whereas `library` is not.

* Refactor registration.ts into dynamic-slotting

- This is part of an effort to refactor the dynamic slotting (extending
  buttons) functionality out of components like ButtonGroup.

* Remove dynamically-slottable logic from ButtonToolbar

* Use DynamicallySlottable in editor-toolbar

* Fix no border radius on indentation button dropdown

* Fix AddonButtons

* Remove Item/ButtonGroupItem in deck-options, where it's not necessary

* Remove unnecessary uses of Item and ButtonGroupItem

* Fix remaining tests

* Fix relative imports

* Revert change return value of remapBinToSrcDir to ./bazel/out...

* Remove typings directory

* Adjust comments for dynamic-slottings
2022-02-03 14:52:11 +10:00

167 lines
4.3 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { Modifier } from "./keys";
import { registerPackage } from "./runtime-require";
import {
modifiersToPlatformString,
keyToPlatformString,
checkModifiers,
checkIfInputKey,
} from "./keys";
import { on } from "./events";
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 {
return event.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 (checkIfInputKey(event)) {
// Any non-modifier key will cancel the shortcut sequence
remove();
}
}
}
export function registerShortcut(
callback: (event: KeyboardEvent) => void,
keyCombinationString: string,
target: EventTarget | Document = document,
): () => void {
const [check, ...restChecks] =
splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
function handler(event: KeyboardEvent): void {
if (check(event)) {
innerShortcut(target, event, callback, ...restChecks);
}
}
return on(target, "keydown", handler);
}
registerPackage("anki/shortcuts", {
registerShortcut,
getPlatformString,
});