-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
index.js
167 lines (147 loc) · 6.71 KB
/
index.js
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import _ from 'underscore';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import * as KeyCommand from 'react-native-key-command';
import bindHandlerToKeydownEvent from './bindHandlerToKeydownEvent';
import getOperatingSystem from '../getOperatingSystem';
import CONST from '../../CONST';
const operatingSystem = getOperatingSystem();
// Handlers for the various keyboard listeners we set up
const eventHandlers = {};
// Documentation information for keyboard shortcuts that are displayed in the keyboard shortcuts informational modal
const documentedShortcuts = {};
/**
* @returns {Array}
*/
function getDocumentedShortcuts() {
return _.sortBy(_.values(documentedShortcuts), 'displayName');
}
/**
* Generates the normalized display name for keyboard shortcuts.
*
* @param {String} key
* @param {String|Array<String>} modifiers
* @returns {String}
*/
function getDisplayName(key, modifiers) {
let displayName = (() => {
// Type of key is string and the type of KeyCommand.constants.* is number | string. Use _.isEqual to match different types.
if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputEnter', 'keyInputEnter').toString().toLowerCase())) {
return ['ENTER'];
}
if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputEscape', 'keyInputEscape').toString().toLowerCase())) {
return ['ESCAPE'];
}
if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputUpArrow', 'keyInputUpArrow').toString().toLowerCase())) {
return ['ARROWUP'];
}
if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputDownArrow', 'keyInputDownArrow').toString().toLowerCase())) {
return ['ARROWDOWN'];
}
if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputLeftArrow', 'keyInputLeftArrow').toString().toLowerCase())) {
return ['ARROWLEFT'];
}
if (_.isEqual(key.toLowerCase(), lodashGet(KeyCommand, 'constants.keyInputRightArrow', 'keyInputRightArrow').toString().toLowerCase())) {
return ['ARROWRIGHT'];
}
return [key.toUpperCase()];
})();
if (_.isString(modifiers)) {
displayName.unshift(modifiers);
} else if (_.isArray(modifiers)) {
displayName = [..._.sortBy(modifiers), ...displayName];
}
displayName = _.map(displayName, (modifier) => lodashGet(CONST.KEYBOARD_SHORTCUT_KEY_DISPLAY_NAME, modifier.toUpperCase(), modifier));
return displayName.join(' + ');
}
_.each(CONST.KEYBOARD_SHORTCUTS, (shortcut) => {
const shortcutTrigger = lodashGet(shortcut, ['trigger', operatingSystem], lodashGet(shortcut, 'trigger.DEFAULT'));
// If there is no trigger for the current OS nor a default trigger, then we don't need to do anything
if (!shortcutTrigger) {
return;
}
KeyCommand.addListener(shortcutTrigger, (keycommandEvent, event) => bindHandlerToKeydownEvent(getDisplayName, eventHandlers, keycommandEvent, event));
});
/**
* Unsubscribes a keyboard event handler.
*
* @param {String} displayName The display name for the key combo to stop watching
* @param {String} callbackID The specific ID given to the callback at the time it was added
* @private
*/
function unsubscribe(displayName, callbackID) {
eventHandlers[displayName] = _.reject(eventHandlers[displayName], (callback) => callback.id === callbackID);
}
/**
* Return platform specific modifiers for keys like Control (CMD on macOS)
*
* @param {Array<String>} keys
* @returns {Array}
*/
function getPlatformEquivalentForKeys(keys) {
return _.map(keys, (key) => {
if (!_.has(CONST.PLATFORM_SPECIFIC_KEYS, key)) {
return key;
}
const platformModifiers = CONST.PLATFORM_SPECIFIC_KEYS[key];
return lodashGet(platformModifiers, operatingSystem, platformModifiers.DEFAULT || key);
});
}
/**
* Subscribes to a keyboard event.
* @param {String} key The key to watch, i.e. 'K' or 'Escape'
* @param {Function} callback The callback to call
* @param {String} descriptionKey Translation key for shortcut description
* @param {Array<String>} [modifiers] Can either be shift or control
* @param {Boolean} [captureOnInputs] Should we capture the event on inputs too?
* @param {Boolean|Function} [shouldBubble] Should the event bubble?
* @param {Number} [priority] The position the callback should take in the stack. 0 means top priority, and 1 means less priority than the most recently added.
* @param {Boolean} [shouldPreventDefault] Should call event.preventDefault after callback?
* @param {Array<String>} [excludedNodes] Do not capture key events targeting excluded nodes (i.e. do not prevent default and let the event bubble)
* @returns {Function} clean up method
*/
function subscribe(key, callback, descriptionKey, modifiers = 'shift', captureOnInputs = false, shouldBubble = false, priority = 0, shouldPreventDefault = true, excludedNodes = []) {
const platformAdjustedModifiers = getPlatformEquivalentForKeys(modifiers);
const displayName = getDisplayName(key, platformAdjustedModifiers);
if (!_.has(eventHandlers, displayName)) {
eventHandlers[displayName] = [];
}
const callbackID = Str.guid();
eventHandlers[displayName].splice(priority, 0, {
id: callbackID,
callback,
captureOnInputs,
shouldPreventDefault,
shouldBubble,
excludedNodes,
});
if (descriptionKey) {
documentedShortcuts[displayName] = {
shortcutKey: key,
descriptionKey,
displayName,
modifiers,
};
}
return () => unsubscribe(displayName, callbackID);
}
/**
* This module configures a global keyboard event handler.
*
* It uses a stack to store event handlers for each key combination. Some additional details:
*
* - By default, new handlers are pushed to the top of the stack. If you pass a >0 priority when subscribing to the key event,
* then the handler will get pushed further down the stack. This means that priority of 0 is higher than priority 1.
*
* - When a key event occurs, we trigger callbacks for that key starting from the top of the stack.
* By default, events do not bubble, and only the handler at the top of the stack will be executed.
* Individual callbacks can be configured with the shouldBubble parameter, to allow the next event handler on the stack execute.
*
* - Each handler has a unique callbackID, so calling the `unsubscribe` function (returned from `subscribe`) will unsubscribe the expected handler,
* regardless of its position in the stack.
*/
const KeyboardShortcut = {
subscribe,
getDocumentedShortcuts,
};
export default KeyboardShortcut;