Skip to content

Commit cddeb0a

Browse files
committed
Add special keys logic, refactor
1 parent 250c837 commit cddeb0a

File tree

7 files changed

+853
-444
lines changed

7 files changed

+853
-444
lines changed

packages/keyboard/src/core.ts

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
/*-----------------------------------------------------------------------------
4+
| Copyright (c) 2014-2017, PhosphorJS Contributors
5+
|
6+
| Distributed under the terms of the BSD 3-Clause License.
7+
|
8+
| The full license is in the file LICENSE, distributed with this software.
9+
|----------------------------------------------------------------------------*/
10+
11+
import { SPECIAL_KEYS } from './special-keys';
12+
13+
/**
14+
* An object which represents an abstract keyboard layout.
15+
*/
16+
export interface IKeyboardLayout {
17+
/**
18+
* The human readable name of the layout.
19+
*
20+
* This value is used primarily for display and debugging purposes.
21+
*/
22+
readonly name: string;
23+
24+
/**
25+
* Get an array of all key values supported by the layout.
26+
*
27+
* @returns A new array of the supported key values.
28+
*
29+
* #### Notes
30+
* This can be useful for authoring tools and debugging, when it's
31+
* necessary to know which keys are available for shortcut use.
32+
*/
33+
keys(): string[];
34+
35+
/**
36+
* Test whether the given key is a valid value for the layout.
37+
*
38+
* @param key - The user provided key to test for validity.
39+
*
40+
* @returns `true` if the key is valid, `false` otherwise.
41+
*/
42+
isValidKey(key: string): boolean;
43+
44+
/**
45+
* Test whether the given key is a modifier key.
46+
*
47+
* @param key - The user provided key.
48+
*
49+
* @returns `true` if the key is a modifier key, `false` otherwise.
50+
*
51+
* #### Notes
52+
* This is necessary so that we don't process modifier keys pressed
53+
* in the middle of the key sequence.
54+
* E.g. "Shift C Ctrl P" is actually 4 keydown events:
55+
* "Shift", "Shift P", "Ctrl", "Ctrl P",
56+
* and events for "Shift" and "Ctrl" should be ignored.
57+
*/
58+
isModifierKey(key: string): boolean;
59+
60+
/**
61+
* Get the key for a `'keydown'` event.
62+
*
63+
* @param event - The event object for a `'keydown'` event.
64+
*
65+
* @returns The associated key value, or an empty string if the event
66+
* does not represent a valid primary key.
67+
*/
68+
keyForKeydownEvent(event: KeyboardEvent): string;
69+
}
70+
71+
/**
72+
* A concrete implementation of [[IKeyboardLayout]] based on keycodes.
73+
*
74+
* The `keyCode` property of a `'keydown'` event is a browser and OS
75+
* specific representation of the physical key (not character) which
76+
* was pressed on a keyboard. While not the most convenient API, it
77+
* is currently the only one which works reliably on all browsers.
78+
*
79+
* This class accepts a user-defined mapping of keycode to key, which
80+
* allows for reliable shortcuts tailored to the user's system.
81+
*/
82+
export class KeycodeLayout implements IKeyboardLayout {
83+
/**
84+
* Construct a new keycode layout.
85+
*
86+
* @param name - The human readable name for the layout.
87+
*
88+
* @param codes - A mapping of keycode to key value.
89+
*
90+
* @param modifierKeys - Array of modifier key names
91+
*/
92+
constructor(
93+
name: string,
94+
keyCodes: KeycodeLayout.CodeMap,
95+
modifierKeys: string[] = [],
96+
codes: KeycodeLayout.ModernCodeMap = {}
97+
) {
98+
this.name = name;
99+
this._keyCodes = keyCodes;
100+
this._codes = codes;
101+
this._keys = KeycodeLayout.extractKeys(keyCodes, codes);
102+
this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys);
103+
}
104+
105+
/**
106+
* The human readable name of the layout.
107+
*/
108+
readonly name: string;
109+
110+
/**
111+
* Get an array of the key values supported by the layout.
112+
*
113+
* @returns A new array of the supported key values.
114+
*/
115+
keys(): string[] {
116+
return Object.keys(this._keys);
117+
}
118+
119+
/**
120+
* Test whether the given key is a valid value for the layout.
121+
*
122+
* @param key - The user provided key to test for validity.
123+
*
124+
* @returns `true` if the key is valid, `false` otherwise.
125+
*/
126+
isValidKey(key: string): boolean {
127+
return key in this._keys || Private.isSpecialCharacter(key);
128+
}
129+
130+
/**
131+
* Test whether the given key is a modifier key.
132+
*
133+
* @param key - The user provided key.
134+
*
135+
* @returns `true` if the key is a modifier key, `false` otherwise.
136+
*/
137+
isModifierKey(key: string): boolean {
138+
return key in this._modifierKeys;
139+
}
140+
141+
/**
142+
* Get the key for a `'keydown'` event.
143+
*
144+
* @param event - The event object for a `'keydown'` event.
145+
*
146+
* @returns The associated key value, or an empty string if
147+
* the event does not represent a valid primary key.
148+
*/
149+
keyForKeydownEvent(event: KeyboardEvent): string {
150+
if (
151+
event.code !== '' &&
152+
event.code !== 'Unidentified' &&
153+
event.code in this._codes
154+
) {
155+
return this._codes[event.code];
156+
}
157+
return (
158+
this._keyCodes[event.keyCode] ||
159+
(Private.isSpecialCharacter(event.key) ? event.key : '')
160+
);
161+
}
162+
163+
private _keys: KeycodeLayout.KeySet;
164+
private _keyCodes: KeycodeLayout.CodeMap;
165+
private _codes: KeycodeLayout.ModernCodeMap;
166+
private _modifierKeys: KeycodeLayout.KeySet;
167+
}
168+
169+
/**
170+
* The namespace for the `KeycodeLayout` class statics.
171+
*/
172+
export namespace KeycodeLayout {
173+
/**
174+
* A type alias for a keycode map.
175+
*/
176+
export type CodeMap = { readonly [keyCode: number]: string };
177+
178+
/**
179+
* A type alias for a code map.
180+
*/
181+
export type ModernCodeMap = { readonly [code: string]: string };
182+
183+
/**
184+
* A type alias for a key set.
185+
*/
186+
export type KeySet = { readonly [key: string]: boolean };
187+
188+
/**
189+
* Extract the set of keys from a code map.
190+
*
191+
* @param code - The code map of interest.
192+
*
193+
* @returns A set of the keys in the code map.
194+
*/
195+
export function extractKeys(
196+
keyCodes: CodeMap,
197+
codes: ModernCodeMap = {}
198+
): KeySet {
199+
let keys: any = Object.create(null);
200+
for (let c in keyCodes) {
201+
keys[keyCodes[c]] = true;
202+
}
203+
for (let c in codes) {
204+
keys[codes[c]] = true;
205+
}
206+
return keys as KeySet;
207+
}
208+
209+
/**
210+
* Convert array of keys to a key set.
211+
*
212+
* @param keys - The array that needs to be converted
213+
*
214+
* @returns A set of the keys in the array.
215+
*/
216+
export function convertToKeySet(keys: string[]): KeySet {
217+
let keySet = Object(null);
218+
for (let i = 0, n = keys.length; i < n; ++i) {
219+
keySet[keys[i]] = true;
220+
}
221+
return keySet;
222+
}
223+
}
224+
225+
/**
226+
* The namespace for the module implementation details.
227+
*/
228+
namespace Private {
229+
/**
230+
* Whether the key value can be considered a special character.
231+
*
232+
* @param key - The key value that is to be considered
233+
*/
234+
export function isSpecialCharacter(key: string): boolean {
235+
// If the value starts with an uppercase latin character and is followed by one
236+
// or more alphanumeric basic latin characters, it is likely a special key.
237+
return SPECIAL_KEYS.indexOf(key) !== -1;
238+
}
239+
}

0 commit comments

Comments
 (0)