summaryrefslogtreecommitdiff
path: root/src/client/directives/hotkey.ts
blob: a1c49f0074062f55a7e4574bf58985f3ebd06ddd (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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { Directive } from 'vue';
import keyCode from '../scripts/keycode';
import { concat } from '../../prelude/array';

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

type action = {
	patterns: pattern[];

	callback: Function;

	allowRepeat: boolean;
};

const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
	const result = {
		patterns: [],
		callback: 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 ignoreElemens = ['input', 'textarea'];

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

export default {
	mounted(el, binding) {
		el._hotkey_global = binding.modifiers.global === true;

		const actions = getKeyMap(binding.value);

		// flatten
		const reservedKeys = concat(actions.map(a => a.patterns));

		el._misskey_reservedKeys = reservedKeys;

		el._keyHandler = (e: KeyboardEvent) => {
			const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : [];
			if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
			if (document.activeElement && document.activeElement.attributes['contenteditable']) return;

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

				if (matched) {
					if (!action.allowRepeat && e.repeat) return;
					if (el._hotkey_global && match(e, targetReservedKeys)) return;

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

		if (el._hotkey_global) {
			document.addEventListener('keydown', el._keyHandler);
		} else {
			el.addEventListener('keydown', el._keyHandler);
		}
	},

	unmounted(el) {
		if (el._hotkey_global) {
			document.removeEventListener('keydown', el._keyHandler);
		} else {
			el.removeEventListener('keydown', el._keyHandler);
		}
	}
} as Directive;