summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts/hotkey.ts
blob: 48c80c066b93c7f7e81c1d29b2b381ba7200d6c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import keyCode from './keycode.js';

type Callback = (ev: KeyboardEvent) => void;

type Keymap = Record<string, Callback>;

type Pattern = {
	which: string[];
	ctrl?: boolean;
	shift?: boolean;
	alt?: boolean;
};

type Action = {
	patterns: Pattern[];
	callback: Callback;
	allowRepeat: boolean;
};

const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
	const result = {
		patterns: [],
		callback,
		allowRepeat: true,
	} as Action;

	if (patterns.match(/^\(.*\)$/) !== null) {
		result.allowRepeat = false;
		patterns = patterns.slice(1, -1);
	}

	result.patterns = patterns.split('|').map(part => {
		const pattern = {
			which: [],
			ctrl: false,
			alt: false,
			shift: false,
		} as Pattern;

		const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
		for (const key of keys) {
			switch (key) {
				case 'ctrl': pattern.ctrl = true; break;
				case 'alt': pattern.alt = true; break;
				case 'shift': pattern.shift = true; break;
				default: pattern.which = keyCode(key).map(k => k.toLowerCase());
			}
		}

		return pattern;
	});

	return result;
});

const ignoreElements = ['input', 'textarea'];

function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean {
	const key = ev.key.toLowerCase();
	return patterns.some(pattern => pattern.which.includes(key) &&
		pattern.ctrl === ev.ctrlKey &&
		pattern.shift === ev.shiftKey &&
		pattern.alt === ev.altKey &&
		!ev.metaKey,
	);
}

export const makeHotkey = (keymap: Keymap) => {
	const actions = parseKeymap(keymap);

	return (ev: KeyboardEvent) => {
		if (document.activeElement) {
			if (ignoreElements.some(el => document.activeElement!.matches(el))) return;
			if (document.activeElement.attributes['contenteditable']) return;
		}

		for (const action of actions) {
			const matched = match(ev, action.patterns);

			if (matched) {
				if (!action.allowRepeat && ev.repeat) return;

				ev.preventDefault();
				ev.stopPropagation();
				action.callback(ev);
				break;
			}
		}
	};
};