summaryrefslogblamecommitdiffstatshomepage
path: root/frontend/src/lib/Shortcuts.ts
blob: 063bd409ca4e1af8596a532538c651519250eba4 (plain) (tree)
























































































































































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