UNPKG

4.17 kBPlain TextView Raw
1const isEqual = (a, b) => {
2 const aKeys = Object.keys(a);
3
4 if (aKeys.length !== Object.keys(b).length) {
5 return false;
6 }
7
8 return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]);
9};
10
11const isArrayEqual = (a, b) => a.length === b.length && a.every((v, i) => isEqual(v, b[i]));
12
13export const matchHotkey = (buffer, hotkey) => {
14 if (buffer.length < hotkey.length) {
15 return false;
16 }
17
18 const indexDiff = buffer.length - hotkey.length;
19 for (let i = hotkey.length - 1; i >= 0; i -= 1) {
20 if (!isEqual(buffer[indexDiff + i], hotkey[i])) {
21 return false;
22 }
23 }
24
25 return true;
26};
27
28const arrayToObject = (arr) => arr.reduce((obj, key) => ({ ...obj, [key]: true }), {});
29
30const allModifiers = ['ctrl', 'shift', 'alt', 'meta'];
31const indexedModifiers = arrayToObject(allModifiers);
32
33const isHotkeyValid = (hotkey) => Object.keys(hotkey).filter((k) => !indexedModifiers[k]).length === 1;
34
35const validate = (value, message) => {
36 if (!value) {
37 throw new Error(message);
38 }
39};
40
41const validateType = (value, name, type) => {
42 validate(typeof value === type, `The ${name} must be a ${type}; given ${typeof value}`);
43};
44
45export const normalizeHotkey = (hotkey) =>
46 hotkey.split(/ +/g).map((part) => {
47 const arr = part.split('+').filter(Boolean);
48 const result = arrayToObject(arr);
49
50 validate(Object.keys(result).length >= arr.length, `Hotkey combination has duplicates "${hotkey}"`);
51
52 validate(isHotkeyValid(result), `Invalid hotkey combination: "${hotkey}"`);
53
54 return result;
55 });
56
57const validateListenerArgs = (hotkey, callback) => {
58 validateType(hotkey, 'hotkey', 'string');
59 validateType(callback, 'callback', 'function');
60};
61
62const createListenersFn = (listeners, fn) => (hotkey, callback) => {
63 validateListenerArgs(hotkey, callback);
64 fn(listeners, hotkey, callback);
65};
66
67const registerListener = (listeners, hotkey, callback) => {
68 listeners.push({ hotkey: normalizeHotkey(hotkey), callback });
69};
70
71const unregisterListener = (listeners, hotkey, callback) => {
72 const normalized = normalizeHotkey(hotkey);
73
74 const index = listeners.findIndex((l) => l.callback === callback && isArrayEqual(normalized, l.hotkey));
75
76 if (index !== -1) {
77 listeners.splice(index, 1);
78 }
79};
80
81const debounce = (fn, time) => {
82 let timeoutId: any = null;
83
84 return () => {
85 clearTimeout(timeoutId);
86 timeoutId = setTimeout(fn, time);
87 };
88};
89
90const getKey = (key) => {
91 switch (key) {
92 case '+':
93 return 'plus';
94 case ' ':
95 return 'space';
96 default:
97 return key;
98 }
99};
100
101const createKeyDownListener = (listeners, debounceTime) => {
102 let buffer: any[] = [];
103
104 const clearBufferDebounced = debounce(() => {
105 buffer = [];
106 }, debounceTime);
107
108 return (event) => {
109 if (event.repeat) {
110 return;
111 }
112
113 if (event.getModifierState(event.key)) {
114 return;
115 }
116
117 clearBufferDebounced();
118
119 const description = {
120 [getKey(event.key)]: true,
121 };
122
123 allModifiers.forEach((m) => {
124 if (event[`${m}Key`]) {
125 description[m] = true;
126 }
127 });
128
129 buffer.push(description);
130
131 listeners.forEach((listener) => {
132 if (matchHotkey(buffer, listener.hotkey)) {
133 listener.callback(event);
134 }
135 });
136 };
137};
138
139const validateContext = (options) => {
140 const { debounceTime = 500, autoEnable = true } = options || {};
141
142 validateType(debounceTime, 'debounceTime', 'number');
143 validate(debounceTime > 0, 'debounceTime must be > 0');
144 validateType(autoEnable, 'autoEnable', 'boolean');
145
146 return { debounceTime, autoEnable };
147};
148
149export const createContext = (options) => {
150 const { debounceTime, autoEnable } = validateContext(options);
151
152 const listeners = [];
153 const keyDownListener = createKeyDownListener(listeners, debounceTime);
154
155 const enable = () => document.addEventListener('keydown', keyDownListener);
156 const disable = () => document.removeEventListener('keydown', keyDownListener);
157
158 if (autoEnable) {
159 enable();
160 }
161
162 return {
163 register: createListenersFn(listeners, registerListener),
164 unregister: createListenersFn(listeners, unregisterListener),
165 enable,
166 disable,
167 };
168};