-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathKeyCombo.ts
More file actions
206 lines (186 loc) · 5.99 KB
/
KeyCombo.ts
File metadata and controls
206 lines (186 loc) · 5.99 KB
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { Keypress } from 'https://deno.land/x/keypress@0.0.7/mod.ts'
const keyToEventKeyMap = [
['Escape', 'Esc'],
[' ', 'Space'],
['ArrowLeft', 'Left'],
['ArrowRight', 'Right'],
['ArrowUp', 'Up'],
['ArrowDown', 'Down'],
['Enter', 'Return'],
['Add', 'Plus', '+'],
['Subtract', 'Minus', '-'],
['Multiply', 'Times', '*'],
['Divide', 'Div', '/'],
['Decimal', '.'],
['Separator', ','],
].reduce((map, combinations) => {
for (const item of combinations) {
;(map[item.toLowerCase()] = map[item.toLowerCase()] || []).push(...combinations.map(it => it.toLowerCase()))
}
return map
}, {} as Record<string, string[]>)
interface KeyComboParams {
/** If the ctrl key must be present or absent. */
ctrl?: boolean
/** If the shift key must be present or absent. */
shift?: boolean
/** If the alt/option key must be present or absent. */
alt?: boolean
/** If the command/super/win key must be present or absent. */
meta?: boolean
/** The key name that must be percent or absent. */
key?: string
}
/**
* A class that represents a key combination of modifier keys
* and maybe a normal key.
*
* @see KeyCombo.parse
*/
export default class KeyCombo {
readonly ctrl: boolean;
readonly shift: boolean;
readonly alt: boolean;
readonly meta: boolean;
readonly key: string;
/**
* Creates a new KeyCombo instance.
*/
constructor({ ctrl, shift, alt, meta, key }: KeyComboParams = {}) {
this.ctrl = ctrl ?? false
this.shift = shift ?? false
this.alt = alt ?? false
this.meta = meta ?? false
this.key = key ?? ''
// if (this.alt) throw new Error('The Alt/Option key is not supported!')
}
get option() {
return this.alt
}
get command() {
return this.meta
}
get win() {
return this.meta
}
get super() {
return this.meta
}
/**
* Parses a key combination from a string. The string should be in a format of
* modifier keys and then an actual key. To separate different keys use a plus
* (`+`). Modifier keys are `Ctrl`, `Shift`, `Alt` (`Option`), and the meta key.
* The meta modifier key is represented as windows logo, or on mac the text
* command, can be represented as `Command`, `Super`, or `Win`.
*
* @example
* ```javascript
* const combinations = [
* KeyCombo.parse('Ctrl+s'),
* KeyCombo.parse('Ctrl'),
* KeyCombo.parse('Ctrl+Shift+Esc'),
* KeyCombo.parse('Ctrl+Alt+Delete'),
* ]
* ```
*
* @param str The key combination string.
* @returns The key combination instance parsed from the string.
*/
static parse(str: string): KeyCombo {
let ctrl = false
let shift = false
let alt = false
let meta = false
const key = str
.replace(/\s*ctrl\s*(?:\+\s*|$)/i, () => (ctrl = true,''))
.replace(/\s*shift\s*(?:\+\s*|$)/i, () => (shift = true,''))
.replace(/\s*(?:alt|option)\s*(?:\+\s*|$)/i, () => (alt = true,''))
.replace(/\s*(?:meta|super|win|command|cmd)\s*(?:\+\s*|$)/i, () => (meta = true,''))
.trim()
return new KeyCombo({ ctrl, shift, alt, meta, key })
}
/**
* Test this key combination against a keyboard event. These events might come
* from events such as `keydown` and `keyup`.
* @example
* ```javascript
* const saveKeyCombo = KeyCombo.parse('Ctrl+S')
* if (saveKeyCombo.test(keypress)) {
* event.preventDefault()
* console.log('Saving...')
* }
* ```
* @param event The event to test this key combo against.
* @returns True if the keys in the event matches the key combo.
*/
test(event: Keypress): boolean {
return (
this.ctrl === event.ctrlKey &&
this.shift === event.shiftKey &&
this.meta === event.metaKey &&
(this.key
? this.key === event.key || (keyToEventKeyMap[this.key.toLowerCase()] || []).includes(event.key?.toLowerCase() ?? '')
: true)
)
}
/** Get the string parts for a Windows system of this key combo. */
getStringPartsWindows() {
return [this.ctrl && 'Ctrl', this.meta && 'Win', this.shift && 'Shift', this.alt && 'Alt', this.key].filter(Boolean)
}
/** Get a string representation of this key combination for Windows systems. */
toStringWindows() {
return this.getStringPartsWindows().join('+')
}
/** Get the string parts for a Linux / Unix system of this key combo. */
getStringPartsLinux() {
return [this.ctrl && 'Ctrl', this.meta && 'Super', this.shift && 'Shift', this.alt && 'Alt', this.key].filter(Boolean)
}
/** Get a string representation of this key combination for Linux / Unix systems. */
toStringLinux() {
return this.getStringPartsLinux().join('+')
}
/** Get the string parts for a MacOS system of this key combo. */
getStringPartsMac() {
return [this.ctrl && 'Ctrl', this.meta && 'Command', this.shift && 'Shift', this.alt && 'Option', this.key].filter(Boolean)
}
/** Get a string representation of this key combination for MacOS systems. */
toStringMac() {
return this.getStringPartsMac().join('+')
}
/** Get the string parts for this key combo. */
getStringParts() {
if (Deno.build.os === 'windows') return this.getStringPartsWindows()
if (Deno.build.os === 'linux') return this.getStringPartsLinux()
return this.getStringPartsMac()
}
/** Get the string representation for this key combo. */
toString() {
return this.getStringParts().join('+')
}
toJSON() {
return {
ctrl: this.ctrl,
shift: this.shift,
alt: this.alt,
meta: this.meta,
key: this.key,
}
}
/**
* Creates a KeyCombo instance from a keyboard event.
* @param event A keyboard event.
*/
static from(event: Keypress): KeyCombo {
return new KeyCombo({ ctrl: event.ctrlKey, shift: event.shiftKey, meta: event.metaKey, key: event.key })
}
}
export class KeyCombos {
constructor(private combos: readonly KeyCombo[]) {}
test(event: Keypress): boolean {
return this.combos.some(combo => combo.test(event))
}
static parse(str: string) {
const combos = str.split('|').map(KeyCombo.parse)
return new KeyCombos(combos)
}
}