summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/Shortcuts.ts
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--frontend/src/lib/Shortcuts.ts153
1 files changed, 153 insertions, 0 deletions
diff --git a/frontend/src/lib/Shortcuts.ts b/frontend/src/lib/Shortcuts.ts
new file mode 100644
index 0000000..063bd40
--- /dev/null
+++ b/frontend/src/lib/Shortcuts.ts
@@ -0,0 +1,153 @@
+import { closeModal, modals } from 'svelte-modals';
+import { get } from 'svelte/store';
+
+type LowercaseLetter =
+ | 'a'
+ | 'b'
+ | 'c'
+ | 'd'
+ | 'e'
+ | 'f'
+ | 'g'
+ | 'h'
+ | 'i'
+ | 'j'
+ | 'l'
+ | 'm'
+ | 'n'
+ | 'o'
+ | 'p'
+ | 'q'
+ | 'r'
+ | 's'
+ | 't'
+ | 'u'
+ | 'v'
+ | 'w'
+ | 'x'
+ | 'y'
+ | 'z';
+
+type UppercaseLetter = Uppercase<LowercaseLetter>;
+type Letter = LowercaseLetter | UppercaseLetter;
+type Special = '?' | 'Enter' | 'Escape' | 'Delete';
+
+const modeSwitches = ['n', 'g', 'i'] as const;
+type ModeSwitch = (typeof modeSwitches)[number];
+
+function isModeSwitch(s: string): s is ModeSwitch {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
+ return modeSwitches.indexOf(s as any) !== -1;
+}
+
+type Key = Letter | Special;
+type KeyCombo = `${ModeSwitch}${Letter}`;
+export type Shortcut = Key | KeyCombo;
+
+type EventAction = (event: KeyboardEvent) => void;
+type FocusAction = HTMLInputElement;
+type ClickAction = HTMLElement;
+
+type Action = EventAction | FocusAction | ClickAction;
+
+const handlers = new Map<string, Action>();
+let mode: ModeSwitch | undefined;
+
+export function handleShortcuts(event: KeyboardEvent) {
+ if (isInputElement(event.target)) {
+ if (event.key === 'Escape') {
+ event.target.blur();
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ return;
+ }
+
+ if (event.ctrlKey) {
+ return;
+ }
+
+ if (event.key === 'Escape') {
+ if (get(modals).length > 0) {
+ closeModal();
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ return;
+ }
+ }
+
+ if (isModeSwitch(event.key) && mode === undefined) {
+ mode = event.key;
+ event.preventDefault();
+ return;
+ }
+
+ const handler = handlers.get(mode === undefined ? event.key : `${mode}${event.key}`);
+
+ if (!handler || get(modals).length > 0) {
+ mode = undefined;
+ return;
+ }
+
+ if (handler instanceof HTMLInputElement) {
+ handler.focus();
+ } else if (handler instanceof HTMLElement) {
+ handler.click();
+ } else {
+ handler(event);
+ }
+
+ mode = undefined;
+ event.preventDefault();
+}
+
+export function accelerator(node: HTMLElement | HTMLInputElement, sc: Shortcut) {
+ handlers.set(sc, node);
+
+ return {
+ destroy() {
+ handlers.delete(sc);
+ }
+ };
+}
+
+export function binds(node: Document, scs: [string, EventAction][]) {
+ const handlers = new Map<string, EventAction>();
+
+ for (const [k, a] of scs) {
+ handlers.set(k, a);
+ }
+
+ function keydown(event: KeyboardEvent) {
+ if (isInputElement(event.target)) return;
+
+ const handler = handlers.get(event.key);
+
+ if (!handler) return;
+
+ handler(event);
+ event.preventDefault();
+ }
+
+ node.addEventListener('keydown', keydown);
+
+ return {
+ destroy() {
+ node.removeEventListener('keydown', keydown);
+ }
+ };
+}
+
+export function addShortcut(sc: Shortcut, action: EventAction) {
+ handlers.set(sc, action);
+}
+
+function isInputElement(target: EventTarget | null): target is HTMLElement {
+ return (
+ target instanceof HTMLElement &&
+ (target instanceof HTMLInputElement ||
+ target instanceof HTMLSelectElement ||
+ target instanceof HTMLTextAreaElement ||
+ target.isContentEditable)
+ );
+}