@cxl/keyboard 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/amd/index.d.ts +19 -13
- package/amd/index.js +1 -1
- package/index.d.ts +19 -13
- package/index.js +90 -55
- package/index.min.js +1 -1
- package/mjs/index.d.ts +19 -13
- package/mjs/index.js +1 -1
- package/package.json +1 -1
- package/test.js +103 -15
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Keyboard Handling Library
|
|
|
6
6
|
|
|
7
7
|
## Project Details
|
|
8
8
|
|
|
9
|
-
- Branch Version: [
|
|
9
|
+
- Branch Version: [1.0.0](https://npmjs.com/package/@cxl/keyboard/v/1.0.0)
|
|
10
10
|
- License: UNLICENSED
|
|
11
11
|
- Documentation: [Link](https://cxlio.github.io/cxl/keyboard)
|
|
12
12
|
- Report Issues: [Github](https://github.com/cxlio/cxl/issues)
|
package/amd/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ interface Key {
|
|
|
5
5
|
shiftKey: boolean;
|
|
6
6
|
metaKey: boolean;
|
|
7
7
|
key: string;
|
|
8
|
+
code: string;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* The `KeyboardOptions` interface defines the configuration needed to handle keyboard interactions with
|
|
@@ -38,10 +39,24 @@ export interface KeyboardOptions {
|
|
|
38
39
|
*/
|
|
39
40
|
capture?: boolean;
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The `KeyboardLayout` interface describes how a keyboard layout should be structured internally.
|
|
44
|
+
*/
|
|
41
45
|
export interface KeyboardLayout {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* A mapping of shifted keys to their non-shifted counterparts, used to accurately
|
|
48
|
+
* interpret which character a shifted key press represents.
|
|
49
|
+
*/
|
|
50
|
+
shiftMap: Record<string, string | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Maps the representation of keys when combined with the alt key.
|
|
53
|
+
*/
|
|
54
|
+
translate(ev: Key): string;
|
|
55
|
+
/**
|
|
56
|
+
* Indicates whether the 'meta' or 'ctrl' key is the primary modifier used for commands,
|
|
57
|
+
* differing based on the operating system (e.g., 'metaKey' for macOS).
|
|
58
|
+
*/
|
|
59
|
+
modKey?: 'metaKey' | 'ctrlKey';
|
|
45
60
|
}
|
|
46
61
|
export declare const KeyboardLayoutData: Record<string, KeyboardLayout>;
|
|
47
62
|
/**
|
|
@@ -63,16 +78,7 @@ export declare const KeyboardLayoutData: Record<string, KeyboardLayout>;
|
|
|
63
78
|
* @return A cleanup function that removes the event listener.
|
|
64
79
|
*/
|
|
65
80
|
export declare function handleKeyboard({ element, onKey, delay, layout, capture, }: KeyboardOptions): () => void;
|
|
66
|
-
|
|
67
|
-
* This function, `parseKey`, takes a key combination string and splits it into a sequence of `Key` objects.
|
|
68
|
-
* It uses regular expressions to identify each part of the key combination, accounting for modifier keys
|
|
69
|
-
* like 'shift', 'ctrl', 'alt', 'meta', and 'mod', the latter being dependent on the `modKey` from the
|
|
70
|
-
* `KeyboardLayout` (either 'ctrlKey' or 'metaKey' depending on the platform). For non-modifier keys, it
|
|
71
|
-
* checks if the `shiftKey` is active and if the key should be shifted using the `shiftMap`. This logic
|
|
72
|
-
* ensures the function captures accurate key state details in preparation for further handling like matching
|
|
73
|
-
* key sequences in the `handleKeyboard` function.
|
|
74
|
-
*/
|
|
75
|
-
export declare function parseKey(key: string, { modKey, shiftedKeys, shiftMap }?: KeyboardLayout): Key[];
|
|
81
|
+
export declare function parseKey(key: string, layout?: KeyboardLayout): Key[];
|
|
76
82
|
/**
|
|
77
83
|
* Converts a string representing a key combination into a normalized string format.
|
|
78
84
|
* This function takes a `key` string and an optional `layout` object (defaults to the user's layout).
|
package/amd/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
define("@cxl/keyboard",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.KeyboardLayoutData=void 0,t.handleKeyboard=function({element:e,onKey:a,delay:n,layout:r,capture:s}){const
|
|
1
|
+
define("@cxl/keyboard",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.KeyboardLayoutData=void 0,t.handleKeyboard=function({element:e,onKey:a,delay:n,layout:r,capture:s}){const c=void 0===n?250:n,y=o.language;let u=[],d=0;r||(r=t.KeyboardLayoutData[y]||t.KeyboardLayoutData["en-US"]);const f=l(r);function K(e){const t=i(e,f);let o=Date.now();if(!t)return;o-d<c?u.push(t):u=[t];const n=u.slice(0);do{if(!1!==a(n.join(" "),u)){e.stopPropagation(),e.preventDefault(),o=0;break}n.shift()}while(n.length);d=o}return e.addEventListener("keydown",K,{capture:s}),()=>e.removeEventListener("keydown",K,{capture:s})},t.parseKey=function(e,t=s()){return c(e,l(t))},t.normalize=function(e,t=s()){const o=l(t);return c(e,o).map((e=>i(e,o))).join(" ")};const o="undefined"!=typeof window?window.navigator:{language:"en-US",platform:"nodejs"},a=/Mac|iPod|iPhone|iPad/,n=/(shift|ctrl|control|alt|option|meta|command|cmd|mod|[^\s+]+)(\s*\+|\s)?/g,r={ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right",Escape:"esc",Space:"space",Backquote:"`",Minus:"-",Equal:"=",BracketLeft:"[",BracketRight:"]",Quote:"'",Backslash:"\\",Apostrophe:"'",Semicolon:";",Comma:",",Period:".",Slash:"/"};function i(e,{translate:t}){const o=e.key;if(!o||"Shift"===o||"Alt"===o||"Meta"===o||"Control"===o||"Dead"===o&&!e.altKey)return"";const a=t(e);let n;return e.ctrlKey&&(n="ctrl"),e.altKey&&(n=n?n+"+alt":"alt"),e.shiftKey&&(n=n?n+"+shift":"shift"),e.metaKey&&(n=n?n+"+meta":"meta"),n?n+"+"+a:a}function s(){return t.KeyboardLayoutData[o?.language]||t.KeyboardLayoutData["en-US"]}function c(e,{modKey:t,shiftMap:o}){const a=[];let r,i={ctrlKey:!1,shiftKey:!1,metaKey:!1,altKey:!1,key:"",code:""};for(;r=n.exec(e.toLowerCase());){const e=r[1];if("shift"===e)i.shiftKey=!0;else if("ctrl"===e||"control"===e)i.ctrlKey=!0;else if("alt"===e||"option"===e)i.altKey=!0;else if("meta"===e||"cmd"===e||"command"===e)i.metaKey=!0;else if("mod"===e)i[t]=!0;else{const t=o[e];t?(i.key=t,i.shiftKey=!0):i.key=e}"+"!==r[2]&&(a.push(i),i={ctrlKey:!1,shiftKey:!1,metaKey:!1,altKey:!1,key:"",code:""})}return a}function l(e){return{modKey:a.test(o.platform)?"metaKey":"ctrlKey",...e}}t.KeyboardLayoutData={"en-US":{shiftMap:{"?":"/",">":".","<":",",'"':"'",":":";","{":"[","}":"]","|":"\\","~":"`","+":"=",_:"-","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0"},translate:({code:e,key:t})=>r[e]||(e.startsWith("Key")?e.slice(3).toLowerCase():e.startsWith("Digit")?e.slice(5):t.toLowerCase())}}}));
|
package/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ interface Key {
|
|
|
5
5
|
shiftKey: boolean;
|
|
6
6
|
metaKey: boolean;
|
|
7
7
|
key: string;
|
|
8
|
+
code: string;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* The `KeyboardOptions` interface defines the configuration needed to handle keyboard interactions with
|
|
@@ -38,10 +39,24 @@ export interface KeyboardOptions {
|
|
|
38
39
|
*/
|
|
39
40
|
capture?: boolean;
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The `KeyboardLayout` interface describes how a keyboard layout should be structured internally.
|
|
44
|
+
*/
|
|
41
45
|
export interface KeyboardLayout {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* A mapping of shifted keys to their non-shifted counterparts, used to accurately
|
|
48
|
+
* interpret which character a shifted key press represents.
|
|
49
|
+
*/
|
|
50
|
+
shiftMap: Record<string, string | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* Maps the representation of keys when combined with the alt key.
|
|
53
|
+
*/
|
|
54
|
+
translate(ev: Key): string;
|
|
55
|
+
/**
|
|
56
|
+
* Indicates whether the 'meta' or 'ctrl' key is the primary modifier used for commands,
|
|
57
|
+
* differing based on the operating system (e.g., 'metaKey' for macOS).
|
|
58
|
+
*/
|
|
59
|
+
modKey?: 'metaKey' | 'ctrlKey';
|
|
45
60
|
}
|
|
46
61
|
export declare const KeyboardLayoutData: Record<string, KeyboardLayout>;
|
|
47
62
|
/**
|
|
@@ -63,16 +78,7 @@ export declare const KeyboardLayoutData: Record<string, KeyboardLayout>;
|
|
|
63
78
|
* @return A cleanup function that removes the event listener.
|
|
64
79
|
*/
|
|
65
80
|
export declare function handleKeyboard({ element, onKey, delay, layout, capture, }: KeyboardOptions): () => void;
|
|
66
|
-
|
|
67
|
-
* This function, `parseKey`, takes a key combination string and splits it into a sequence of `Key` objects.
|
|
68
|
-
* It uses regular expressions to identify each part of the key combination, accounting for modifier keys
|
|
69
|
-
* like 'shift', 'ctrl', 'alt', 'meta', and 'mod', the latter being dependent on the `modKey` from the
|
|
70
|
-
* `KeyboardLayout` (either 'ctrlKey' or 'metaKey' depending on the platform). For non-modifier keys, it
|
|
71
|
-
* checks if the `shiftKey` is active and if the key should be shifted using the `shiftMap`. This logic
|
|
72
|
-
* ensures the function captures accurate key state details in preparation for further handling like matching
|
|
73
|
-
* key sequences in the `handleKeyboard` function.
|
|
74
|
-
*/
|
|
75
|
-
export declare function parseKey(key: string, { modKey, shiftedKeys, shiftMap }?: KeyboardLayout): Key[];
|
|
81
|
+
export declare function parseKey(key: string, layout?: KeyboardLayout): Key[];
|
|
76
82
|
/**
|
|
77
83
|
* Converts a string representing a key combination into a normalized string format.
|
|
78
84
|
* This function takes a `key` string and an optional `layout` object (defaults to the user's layout).
|
package/index.js
CHANGED
|
@@ -11,34 +11,27 @@ const navigator = typeof window !== 'undefined'
|
|
|
11
11
|
const IS_MAC = /Mac|iPod|iPhone|iPad/;
|
|
12
12
|
const PARSE_KEY = /(shift|ctrl|control|alt|option|meta|command|cmd|mod|[^\s+]+)(\s*\+|\s)?/g;
|
|
13
13
|
const SHIFT_MAP = {
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
"'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'4'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
};
|
|
36
|
-
exports.KeyboardLayoutData = {
|
|
37
|
-
'en-US': {
|
|
38
|
-
shiftedKeys: Object.values(SHIFT_MAP),
|
|
39
|
-
shiftMap: SHIFT_MAP,
|
|
40
|
-
modKey: IS_MAC.test(navigator.platform) ? 'metaKey' : 'ctrlKey',
|
|
41
|
-
},
|
|
14
|
+
'?': '/',
|
|
15
|
+
'>': '.',
|
|
16
|
+
'<': ',',
|
|
17
|
+
'"': "'",
|
|
18
|
+
':': ';',
|
|
19
|
+
'{': '[',
|
|
20
|
+
'}': ']',
|
|
21
|
+
'|': '\\',
|
|
22
|
+
'~': '`',
|
|
23
|
+
'+': '=',
|
|
24
|
+
_: '-',
|
|
25
|
+
'!': '1',
|
|
26
|
+
'@': '2',
|
|
27
|
+
'#': '3',
|
|
28
|
+
$: '4',
|
|
29
|
+
'%': '5',
|
|
30
|
+
'^': '6',
|
|
31
|
+
'&': '7',
|
|
32
|
+
'*': '8',
|
|
33
|
+
'(': '9',
|
|
34
|
+
')': '0',
|
|
42
35
|
};
|
|
43
36
|
const TranslateKey = {
|
|
44
37
|
ArrowUp: 'up',
|
|
@@ -46,31 +39,63 @@ const TranslateKey = {
|
|
|
46
39
|
ArrowLeft: 'left',
|
|
47
40
|
ArrowRight: 'right',
|
|
48
41
|
Escape: 'esc',
|
|
49
|
-
|
|
42
|
+
Space: 'space',
|
|
43
|
+
Backquote: '`',
|
|
44
|
+
Minus: '-',
|
|
45
|
+
Equal: '=',
|
|
46
|
+
BracketLeft: '[',
|
|
47
|
+
BracketRight: ']',
|
|
48
|
+
Quote: "'",
|
|
49
|
+
Backslash: '\\',
|
|
50
|
+
Apostrophe: "'",
|
|
51
|
+
Semicolon: ';',
|
|
52
|
+
Comma: ',',
|
|
53
|
+
Period: '.',
|
|
54
|
+
Slash: '/',
|
|
55
|
+
};
|
|
56
|
+
exports.KeyboardLayoutData = {
|
|
57
|
+
'en-US': {
|
|
58
|
+
shiftMap: SHIFT_MAP,
|
|
59
|
+
translate({ code, key }) {
|
|
60
|
+
const translated = TranslateKey[code];
|
|
61
|
+
if (translated)
|
|
62
|
+
return translated;
|
|
63
|
+
if (code.startsWith('Key'))
|
|
64
|
+
return code.slice(3).toLowerCase();
|
|
65
|
+
if (code.startsWith('Digit'))
|
|
66
|
+
return code.slice(5);
|
|
67
|
+
return key.toLowerCase();
|
|
68
|
+
},
|
|
69
|
+
},
|
|
50
70
|
};
|
|
51
71
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
72
|
+
* The `keyboardEventToString` function converts a `Key` object to a string that represents the key combination pressed.
|
|
73
|
+
* It checks individual modifier keys (`ctrlKey`, `altKey`, `shiftKey`, and `metaKey`) and constructs a concatenated
|
|
74
|
+
* string reflecting these modifiers followed by the translated character key.
|
|
75
|
+
*
|
|
76
|
+
* Exceptions include when the `key` is 'Shift', 'Alt', 'Meta', 'Control', or 'Dead' without an `altKey`,
|
|
77
|
+
* as these are discarded (represented by an empty string). If no modifiers are pressed, it directly returns
|
|
78
|
+
* the translated character.
|
|
79
|
+
*
|
|
80
|
+
* This conversion provides a consistent way to describe key combinations as strings, essential for handling
|
|
81
|
+
* and processing keyboard shortcuts or sequences in the application.
|
|
58
82
|
*/
|
|
59
|
-
function keyboardEventToString(ev, {
|
|
83
|
+
function keyboardEventToString(ev, { translate }) {
|
|
60
84
|
const key = ev.key;
|
|
61
85
|
if (!key ||
|
|
62
86
|
key === 'Shift' ||
|
|
63
87
|
key === 'Alt' ||
|
|
64
88
|
key === 'Meta' ||
|
|
65
|
-
key === 'Control'
|
|
89
|
+
key === 'Control' ||
|
|
90
|
+
(key === 'Dead' && !ev.altKey))
|
|
66
91
|
return '';
|
|
67
|
-
const ch =
|
|
92
|
+
const ch = translate(ev);
|
|
68
93
|
let result;
|
|
69
94
|
if (ev.ctrlKey)
|
|
70
95
|
result = 'ctrl';
|
|
71
96
|
if (ev.altKey)
|
|
72
97
|
result = result ? result + '+alt' : 'alt';
|
|
73
|
-
if (ev.shiftKey
|
|
98
|
+
if (ev.shiftKey)
|
|
74
99
|
result = result ? result + '+shift' : 'shift';
|
|
75
100
|
if (ev.metaKey)
|
|
76
101
|
result = result ? result + '+meta' : 'meta';
|
|
@@ -99,7 +124,8 @@ function handleKeyboard({ element, onKey, delay, layout, capture, }) {
|
|
|
99
124
|
const locale = navigator.language;
|
|
100
125
|
let sequence = [];
|
|
101
126
|
let lastT = 0;
|
|
102
|
-
|
|
127
|
+
layout || (layout = exports.KeyboardLayoutData[locale] || exports.KeyboardLayoutData['en-US']);
|
|
128
|
+
const newLayout = augmentLayout(layout);
|
|
103
129
|
function handler(ev) {
|
|
104
130
|
const k = keyboardEventToString(ev, newLayout);
|
|
105
131
|
let t = Date.now();
|
|
@@ -124,6 +150,10 @@ function handleKeyboard({ element, onKey, delay, layout, capture, }) {
|
|
|
124
150
|
element.addEventListener('keydown', handler, { capture });
|
|
125
151
|
return () => element.removeEventListener('keydown', handler, { capture });
|
|
126
152
|
}
|
|
153
|
+
// Retrieves the default `KeyboardLayout` based on the current user's language settings from `navigator.language`.
|
|
154
|
+
// Falls back to the 'en-US' layout if the user's specific language is not available in `KeyboardLayoutData`.
|
|
155
|
+
// This function helps ensure that the keyboard handling can adapt to the most appropriate layout for the user,
|
|
156
|
+
// facilitating the accurate interpretation of key sequences and modifiers according to locale preferences.
|
|
127
157
|
function getDefaultLayout() {
|
|
128
158
|
return (exports.KeyboardLayoutData[navigator?.language] || exports.KeyboardLayoutData['en-US']);
|
|
129
159
|
}
|
|
@@ -134,6 +164,7 @@ function newKey() {
|
|
|
134
164
|
metaKey: false,
|
|
135
165
|
altKey: false,
|
|
136
166
|
key: '',
|
|
167
|
+
code: '',
|
|
137
168
|
};
|
|
138
169
|
}
|
|
139
170
|
/**
|
|
@@ -145,7 +176,7 @@ function newKey() {
|
|
|
145
176
|
* ensures the function captures accurate key state details in preparation for further handling like matching
|
|
146
177
|
* key sequences in the `handleKeyboard` function.
|
|
147
178
|
*/
|
|
148
|
-
function
|
|
179
|
+
function _parseKey(key, { modKey, shiftMap }) {
|
|
149
180
|
const sequence = [];
|
|
150
181
|
let match;
|
|
151
182
|
let event = newKey();
|
|
@@ -162,19 +193,13 @@ function parseKey(key, { modKey, shiftedKeys, shiftMap } = getDefaultLayout()) {
|
|
|
162
193
|
else if (ch === 'mod')
|
|
163
194
|
event[modKey] = true;
|
|
164
195
|
else {
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
const shifted = shiftMap[ch];
|
|
172
|
-
if (shifted) {
|
|
173
|
-
event.key = shifted;
|
|
174
|
-
event.shiftKey = false;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
196
|
+
const shifted = shiftMap[ch];
|
|
197
|
+
if (shifted) {
|
|
198
|
+
event.key = shifted;
|
|
199
|
+
event.shiftKey = true;
|
|
177
200
|
}
|
|
201
|
+
else
|
|
202
|
+
event.key = ch;
|
|
178
203
|
}
|
|
179
204
|
if (match[2] !== '+') {
|
|
180
205
|
sequence.push(event);
|
|
@@ -183,6 +208,15 @@ function parseKey(key, { modKey, shiftedKeys, shiftMap } = getDefaultLayout()) {
|
|
|
183
208
|
}
|
|
184
209
|
return sequence;
|
|
185
210
|
}
|
|
211
|
+
function augmentLayout(layout) {
|
|
212
|
+
return {
|
|
213
|
+
modKey: IS_MAC.test(navigator.platform) ? 'metaKey' : 'ctrlKey',
|
|
214
|
+
...layout,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function parseKey(key, layout = getDefaultLayout()) {
|
|
218
|
+
return _parseKey(key, augmentLayout(layout));
|
|
219
|
+
}
|
|
186
220
|
/**
|
|
187
221
|
* Converts a string representing a key combination into a normalized string format.
|
|
188
222
|
* This function takes a `key` string and an optional `layout` object (defaults to the user's layout).
|
|
@@ -193,6 +227,7 @@ function parseKey(key, { modKey, shiftedKeys, shiftMap } = getDefaultLayout()) {
|
|
|
193
227
|
* for ensuring consistent representation of key sequences across different keyboard layouts.
|
|
194
228
|
*/
|
|
195
229
|
function normalize(key, layout = getDefaultLayout()) {
|
|
196
|
-
const
|
|
197
|
-
|
|
230
|
+
const newLayout = augmentLayout(layout);
|
|
231
|
+
const sequence = _parseKey(key, newLayout);
|
|
232
|
+
return sequence.map(key => keyboardEventToString(key, newLayout)).join(' ');
|
|
198
233
|
}
|
package/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.KeyboardLayoutData=void 0,exports.handleKeyboard=handleKeyboard,exports.parseKey=parseKey,exports.normalize=normalize;const navigator="undefined"!=typeof window?window.navigator:{language:"en-US",platform:"nodejs"},IS_MAC=/Mac|iPod|iPhone|iPad/,PARSE_KEY=/(shift|ctrl|control|alt|option|meta|command|cmd|mod|[^\s+]+)(\s*\+|\s)?/g,SHIFT_MAP={"
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.KeyboardLayoutData=void 0,exports.handleKeyboard=handleKeyboard,exports.parseKey=parseKey,exports.normalize=normalize;const navigator="undefined"!=typeof window?window.navigator:{language:"en-US",platform:"nodejs"},IS_MAC=/Mac|iPod|iPhone|iPad/,PARSE_KEY=/(shift|ctrl|control|alt|option|meta|command|cmd|mod|[^\s+]+)(\s*\+|\s)?/g,SHIFT_MAP={"?":"/",">":".","<":",",'"':"'",":":";","{":"[","}":"]","|":"\\","~":"`","+":"=",_:"-","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0"},TranslateKey={ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right",Escape:"esc",Space:"space",Backquote:"`",Minus:"-",Equal:"=",BracketLeft:"[",BracketRight:"]",Quote:"'",Backslash:"\\",Apostrophe:"'",Semicolon:";",Comma:",",Period:".",Slash:"/"};function keyboardEventToString(e,{translate:t}){const a=e.key;if(!a||"Shift"===a||"Alt"===a||"Meta"===a||"Control"===a||"Dead"===a&&!e.altKey)return"";const o=t(e);let n;return e.ctrlKey&&(n="ctrl"),e.altKey&&(n=n?n+"+alt":"alt"),e.shiftKey&&(n=n?n+"+shift":"shift"),e.metaKey&&(n=n?n+"+meta":"meta"),n?n+"+"+o:o}function handleKeyboard({element:e,onKey:t,delay:a,layout:o,capture:n}){const r=void 0===a?250:a,s=navigator.language;let i=[],y=0;o||(o=exports.KeyboardLayoutData[s]||exports.KeyboardLayoutData["en-US"]);const l=augmentLayout(o);function u(e){const a=keyboardEventToString(e,l);let o=Date.now();if(!a)return;o-y<r?i.push(a):i=[a];const n=i.slice(0);do{if(!1!==t(n.join(" "),i)){e.stopPropagation(),e.preventDefault(),o=0;break}n.shift()}while(n.length);y=o}return e.addEventListener("keydown",u,{capture:n}),()=>e.removeEventListener("keydown",u,{capture:n})}function getDefaultLayout(){return exports.KeyboardLayoutData[navigator?.language]||exports.KeyboardLayoutData["en-US"]}function newKey(){return{ctrlKey:!1,shiftKey:!1,metaKey:!1,altKey:!1,key:"",code:""}}function _parseKey(e,{modKey:t,shiftMap:a}){const o=[];let n,r=newKey();for(;n=PARSE_KEY.exec(e.toLowerCase());){const e=n[1];if("shift"===e)r.shiftKey=!0;else if("ctrl"===e||"control"===e)r.ctrlKey=!0;else if("alt"===e||"option"===e)r.altKey=!0;else if("meta"===e||"cmd"===e||"command"===e)r.metaKey=!0;else if("mod"===e)r[t]=!0;else{const t=a[e];t?(r.key=t,r.shiftKey=!0):r.key=e}"+"!==n[2]&&(o.push(r),r=newKey())}return o}function augmentLayout(e){return{modKey:IS_MAC.test(navigator.platform)?"metaKey":"ctrlKey",...e}}function parseKey(e,t=getDefaultLayout()){return _parseKey(e,augmentLayout(t))}function normalize(e,t=getDefaultLayout()){const a=augmentLayout(t);return _parseKey(e,a).map((e=>keyboardEventToString(e,a))).join(" ")}exports.KeyboardLayoutData={"en-US":{shiftMap:SHIFT_MAP,translate({code:e,key:t}){const a=TranslateKey[e];return a||(e.startsWith("Key")?e.slice(3).toLowerCase():e.startsWith("Digit")?e.slice(5):t.toLowerCase())}}};
|
package/mjs/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ interface Key {
|
|
|
4
4
|
shiftKey: boolean;
|
|
5
5
|
metaKey: boolean;
|
|
6
6
|
key: string;
|
|
7
|
+
code: string;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* The `KeyboardOptions` interface defines the configuration needed to handle keyboard interactions with
|
|
@@ -37,10 +38,24 @@ export interface KeyboardOptions {
|
|
|
37
38
|
*/
|
|
38
39
|
capture?: boolean;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The `KeyboardLayout` interface describes how a keyboard layout should be structured internally.
|
|
43
|
+
*/
|
|
40
44
|
export interface KeyboardLayout {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
/**
|
|
46
|
+
* A mapping of shifted keys to their non-shifted counterparts, used to accurately
|
|
47
|
+
* interpret which character a shifted key press represents.
|
|
48
|
+
*/
|
|
49
|
+
shiftMap: Record<string, string | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Maps the representation of keys when combined with the alt key.
|
|
52
|
+
*/
|
|
53
|
+
translate(ev: Key): string;
|
|
54
|
+
/**
|
|
55
|
+
* Indicates whether the 'meta' or 'ctrl' key is the primary modifier used for commands,
|
|
56
|
+
* differing based on the operating system (e.g., 'metaKey' for macOS).
|
|
57
|
+
*/
|
|
58
|
+
modKey?: 'metaKey' | 'ctrlKey';
|
|
44
59
|
}
|
|
45
60
|
export declare const KeyboardLayoutData: Record<string, KeyboardLayout>;
|
|
46
61
|
/**
|
|
@@ -62,16 +77,7 @@ export declare const KeyboardLayoutData: Record<string, KeyboardLayout>;
|
|
|
62
77
|
* @return A cleanup function that removes the event listener.
|
|
63
78
|
*/
|
|
64
79
|
export declare function handleKeyboard({ element, onKey, delay, layout, capture, }: KeyboardOptions): () => void;
|
|
65
|
-
|
|
66
|
-
* This function, `parseKey`, takes a key combination string and splits it into a sequence of `Key` objects.
|
|
67
|
-
* It uses regular expressions to identify each part of the key combination, accounting for modifier keys
|
|
68
|
-
* like 'shift', 'ctrl', 'alt', 'meta', and 'mod', the latter being dependent on the `modKey` from the
|
|
69
|
-
* `KeyboardLayout` (either 'ctrlKey' or 'metaKey' depending on the platform). For non-modifier keys, it
|
|
70
|
-
* checks if the `shiftKey` is active and if the key should be shifted using the `shiftMap`. This logic
|
|
71
|
-
* ensures the function captures accurate key state details in preparation for further handling like matching
|
|
72
|
-
* key sequences in the `handleKeyboard` function.
|
|
73
|
-
*/
|
|
74
|
-
export declare function parseKey(key: string, { modKey, shiftedKeys, shiftMap }?: KeyboardLayout): Key[];
|
|
80
|
+
export declare function parseKey(key: string, layout?: KeyboardLayout): Key[];
|
|
75
81
|
/**
|
|
76
82
|
* Converts a string representing a key combination into a normalized string format.
|
|
77
83
|
* This function takes a `key` string and an optional `layout` object (defaults to the user's layout).
|
package/mjs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const navigator="undefined"!=typeof window?window.navigator:{language:"en-US",platform:"nodejs"},IS_MAC=/Mac|iPod|iPhone|iPad/,PARSE_KEY=/(shift|ctrl|control|alt|option|meta|command|cmd|mod|[^\s+]+)(\s*\+|\s)?/g,SHIFT_MAP={"
|
|
1
|
+
const navigator="undefined"!=typeof window?window.navigator:{language:"en-US",platform:"nodejs"},IS_MAC=/Mac|iPod|iPhone|iPad/,PARSE_KEY=/(shift|ctrl|control|alt|option|meta|command|cmd|mod|[^\s+]+)(\s*\+|\s)?/g,SHIFT_MAP={"?":"/",">":".","<":",",'"':"'",":":";","{":"[","}":"]","|":"\\","~":"`","+":"=",_:"-","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0"},TranslateKey={ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right",Escape:"esc",Space:"space",Backquote:"`",Minus:"-",Equal:"=",BracketLeft:"[",BracketRight:"]",Quote:"'",Backslash:"\\",Apostrophe:"'",Semicolon:";",Comma:",",Period:".",Slash:"/"};export const KeyboardLayoutData={"en-US":{shiftMap:SHIFT_MAP,translate:({code:e,key:t})=>TranslateKey[e]||(e.startsWith("Key")?e.slice(3).toLowerCase():e.startsWith("Digit")?e.slice(5):t.toLowerCase())}};function keyboardEventToString(e,{translate:t}){const a=e.key;if(!a||"Shift"===a||"Alt"===a||"Meta"===a||"Control"===a||"Dead"===a&&!e.altKey)return"";const o=t(e);let n;return e.ctrlKey&&(n="ctrl"),e.altKey&&(n=n?n+"+alt":"alt"),e.shiftKey&&(n=n?n+"+shift":"shift"),e.metaKey&&(n=n?n+"+meta":"meta"),n?n+"+"+o:o}export function handleKeyboard({element:e,onKey:t,delay:a,layout:o,capture:n}){const r=void 0===a?250:a,i=navigator.language;let s=[],y=0;o||(o=KeyboardLayoutData[i]||KeyboardLayoutData["en-US"]);const l=augmentLayout(o);function u(e){const a=keyboardEventToString(e,l);let o=Date.now();if(!a)return;o-y<r?s.push(a):s=[a];const n=s.slice(0);do{if(!1!==t(n.join(" "),s)){e.stopPropagation(),e.preventDefault(),o=0;break}n.shift()}while(n.length);y=o}return e.addEventListener("keydown",u,{capture:n}),()=>e.removeEventListener("keydown",u,{capture:n})}function getDefaultLayout(){return KeyboardLayoutData[navigator?.language]||KeyboardLayoutData["en-US"]}function newKey(){return{ctrlKey:!1,shiftKey:!1,metaKey:!1,altKey:!1,key:"",code:""}}function _parseKey(e,{modKey:t,shiftMap:a}){const o=[];let n,r=newKey();for(;n=PARSE_KEY.exec(e.toLowerCase());){const e=n[1];if("shift"===e)r.shiftKey=!0;else if("ctrl"===e||"control"===e)r.ctrlKey=!0;else if("alt"===e||"option"===e)r.altKey=!0;else if("meta"===e||"cmd"===e||"command"===e)r.metaKey=!0;else if("mod"===e)r[t]=!0;else{const t=a[e];t?(r.key=t,r.shiftKey=!0):r.key=e}"+"!==n[2]&&(o.push(r),r=newKey())}return o}function augmentLayout(e){return{modKey:IS_MAC.test(navigator.platform)?"metaKey":"ctrlKey",...e}}export function parseKey(e,t=getDefaultLayout()){return _parseKey(e,augmentLayout(t))}export function normalize(e,t=getDefaultLayout()){const a=augmentLayout(t);return _parseKey(e,a).map((e=>keyboardEventToString(e,a))).join(" ")}
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -17,29 +17,51 @@ exports.default = (0, spec_1.spec)('keyboard', s => {
|
|
|
17
17
|
a.equal((0, index_js_1.normalize)('ctrl+shift+x'), 'ctrl+shift+x');
|
|
18
18
|
});
|
|
19
19
|
it.should('parse mod key', a => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const layout = {
|
|
21
|
+
...index_js_1.KeyboardLayoutData['en-US'],
|
|
22
|
+
modKey: 'ctrlKey',
|
|
23
|
+
};
|
|
24
|
+
a.equal((0, index_js_1.normalize)('mod+x', layout), 'ctrl+x');
|
|
25
|
+
a.equal((0, index_js_1.normalize)('x+mod', layout), 'ctrl+x');
|
|
26
|
+
a.equal((0, index_js_1.normalize)('MOD+X', layout), 'ctrl+x');
|
|
27
|
+
layout.modKey = 'metaKey';
|
|
28
|
+
a.equal((0, index_js_1.normalize)('mod+x', layout), 'meta+x');
|
|
29
|
+
a.equal((0, index_js_1.normalize)('x+mod', layout), 'meta+x');
|
|
30
|
+
a.equal((0, index_js_1.normalize)('MOD+X', layout), 'meta+x');
|
|
23
31
|
});
|
|
24
32
|
it.should('normalize shifted keys', a => {
|
|
25
|
-
a.equal((0, index_js_1.normalize)('
|
|
33
|
+
a.equal((0, index_js_1.normalize)('%'), 'shift+5');
|
|
26
34
|
a.equal((0, index_js_1.normalize)('shift+A'), 'shift+a');
|
|
27
35
|
a.equal((0, index_js_1.normalize)('shift+y'), 'shift+y');
|
|
28
|
-
a.equal((0, index_js_1.normalize)('shift+ctrl+5'), 'ctrl
|
|
36
|
+
a.equal((0, index_js_1.normalize)('shift+ctrl+5'), 'ctrl+shift+5');
|
|
37
|
+
a.equal((0, index_js_1.normalize)('ctrl+%'), 'ctrl+shift+5');
|
|
29
38
|
});
|
|
30
39
|
it.should('handle non character keys', a => {
|
|
31
|
-
a.equal((0, index_js_1.normalize)(':'), '
|
|
40
|
+
a.equal((0, index_js_1.normalize)(':'), 'shift+;');
|
|
32
41
|
a.equal((0, index_js_1.normalize)('/'), '/');
|
|
33
42
|
a.equal((0, index_js_1.normalize)('.'), '.');
|
|
34
|
-
a.equal((0, index_js_1.normalize)('?'), '
|
|
35
|
-
a.equal((0, index_js_1.normalize)('shift+/'), '
|
|
36
|
-
a.equal(
|
|
37
|
-
a.equal((0, index_js_1.normalize)('
|
|
43
|
+
a.equal((0, index_js_1.normalize)('?'), 'shift+/');
|
|
44
|
+
a.equal((0, index_js_1.normalize)('shift+/'), 'shift+/');
|
|
45
|
+
//a.equal(normalize('shift++'), 'shift+=');
|
|
46
|
+
a.equal((0, index_js_1.normalize)('shift+?'), 'shift+/');
|
|
47
|
+
a.equal((0, index_js_1.normalize)('ctrl+:'), 'ctrl+shift+;');
|
|
48
|
+
a.equal((0, index_js_1.normalize)('ctrl+shift+?'), 'ctrl+shift+/');
|
|
49
|
+
a.equal((0, index_js_1.normalize)(';'), ';');
|
|
38
50
|
});
|
|
39
51
|
it.should('normalize special keys with modifiers', a => {
|
|
40
52
|
a.equal((0, index_js_1.normalize)('ctrl+alt+del'), 'ctrl+alt+del');
|
|
41
53
|
a.equal((0, index_js_1.normalize)('ctrl+shift+esc'), 'ctrl+shift+esc');
|
|
42
54
|
});
|
|
55
|
+
it.should('handle case sensitivity', a => {
|
|
56
|
+
a.equal((0, index_js_1.normalize)('CTRL+A'), 'ctrl+a');
|
|
57
|
+
a.equal((0, index_js_1.normalize)('Shift+Y'), 'shift+y');
|
|
58
|
+
});
|
|
59
|
+
/*it.should('handle complex sequences', a => {
|
|
60
|
+
a.equal(
|
|
61
|
+
normalize(' ctrl + shift + x ctrl+y'),
|
|
62
|
+
'ctrl+shift+x ctrl+y',
|
|
63
|
+
);
|
|
64
|
+
});*/
|
|
43
65
|
});
|
|
44
66
|
s.test('parseKey', it => {
|
|
45
67
|
it.should('correctly parse simple keys without modifiers', a => {
|
|
@@ -78,17 +100,51 @@ exports.default = (0, spec_1.spec)('keyboard', s => {
|
|
|
78
100
|
a.equal(result[1].ctrlKey, true);
|
|
79
101
|
});
|
|
80
102
|
it.should('map mod key to the correct modifier', a => {
|
|
81
|
-
const
|
|
82
|
-
const result = (0, index_js_1.parseKey)('mod+c');
|
|
103
|
+
const layout = index_js_1.KeyboardLayoutData['en-US'];
|
|
104
|
+
const result = (0, index_js_1.parseKey)('mod+c', { ...layout, modKey: 'ctrlKey' });
|
|
83
105
|
a.equal(result.length, 1);
|
|
84
106
|
a.equal(result[0].key, 'c');
|
|
85
|
-
a.equal(result[0]
|
|
107
|
+
a.equal(result[0].ctrlKey, true);
|
|
108
|
+
const result2 = (0, index_js_1.parseKey)('mod+c', { ...layout, modKey: 'metaKey' });
|
|
109
|
+
a.equal(result2.length, 1);
|
|
110
|
+
a.equal(result2[0].key, 'c');
|
|
111
|
+
a.equal(result2[0].metaKey, true);
|
|
86
112
|
});
|
|
87
113
|
it.should('handle shifted characters correctly', a => {
|
|
88
114
|
const result = (0, index_js_1.parseKey)('shift+1');
|
|
89
115
|
a.equal(result.length, 1);
|
|
90
|
-
a.equal(result[0].key, '
|
|
91
|
-
a.equal(result[0].shiftKey,
|
|
116
|
+
a.equal(result[0].key, '1'); // assuming shift+1 maps to '@' in shiftMap
|
|
117
|
+
a.equal(result[0].shiftKey, true);
|
|
118
|
+
});
|
|
119
|
+
it.should('handle keys with special characters', a => {
|
|
120
|
+
const result = (0, index_js_1.parseKey)('ctrl+shift+1');
|
|
121
|
+
a.equal(result.length, 1);
|
|
122
|
+
a.equal(result[0].key, '1');
|
|
123
|
+
a.equal(result[0].ctrlKey, true);
|
|
124
|
+
a.equal(result[0].shiftKey, true);
|
|
125
|
+
});
|
|
126
|
+
it.should('handle unsupported modifier combinations', a => {
|
|
127
|
+
const result = (0, index_js_1.parseKey)('ctrl+alt+meta+z');
|
|
128
|
+
a.equal(result.length, 1);
|
|
129
|
+
a.equal(result[0].key, 'z');
|
|
130
|
+
a.equal(result[0].ctrlKey, true);
|
|
131
|
+
a.equal(result[0].altKey, true);
|
|
132
|
+
a.equal(result[0].metaKey, true);
|
|
133
|
+
});
|
|
134
|
+
it.should('correctly parse special character keys', a => {
|
|
135
|
+
const result = (0, index_js_1.parseKey)('ctrl+alt+!');
|
|
136
|
+
a.equal(result.length, 1);
|
|
137
|
+
a.equal(result[0].key, '1');
|
|
138
|
+
a.equal(result[0].ctrlKey, true);
|
|
139
|
+
a.equal(result[0].altKey, true);
|
|
140
|
+
a.equal(result[0].shiftKey, true);
|
|
141
|
+
});
|
|
142
|
+
it.should('handle unsupported character sequences', a => {
|
|
143
|
+
const result = (0, index_js_1.parseKey)('ctrl+alt+ctrl+z');
|
|
144
|
+
a.equal(result.length, 1);
|
|
145
|
+
a.equal(result[0].key, 'z');
|
|
146
|
+
a.equal(result[0].ctrlKey, true);
|
|
147
|
+
a.equal(result[0].altKey, true);
|
|
92
148
|
});
|
|
93
149
|
});
|
|
94
150
|
s.test('handleKeyboard', it => {
|
|
@@ -147,5 +203,37 @@ exports.default = (0, spec_1.spec)('keyboard', s => {
|
|
|
147
203
|
dispose();
|
|
148
204
|
a.equal(off.lastEvent?.called, 1);
|
|
149
205
|
});
|
|
206
|
+
it.should('handle rapid key presses correctly', (a) => {
|
|
207
|
+
const elementMock = a.element('div');
|
|
208
|
+
const on = a.spyFn(elementMock, 'addEventListener');
|
|
209
|
+
const onKeyMock = (0, spec_1.mockFn)(key => key === 'x y');
|
|
210
|
+
const dispose = (0, index_js_1.handleKeyboard)({
|
|
211
|
+
element: elementMock,
|
|
212
|
+
onKey: onKeyMock,
|
|
213
|
+
delay: 100,
|
|
214
|
+
layout: undefined,
|
|
215
|
+
capture: false,
|
|
216
|
+
});
|
|
217
|
+
const eventX = new KeyboardEvent('keydown', { key: 'x' });
|
|
218
|
+
const eventY = new KeyboardEvent('keydown', { key: 'y' });
|
|
219
|
+
a.assert(typeof on.lastEvent?.arguments[1] === 'function');
|
|
220
|
+
on.lastEvent?.arguments[1](eventX);
|
|
221
|
+
on.lastEvent?.arguments[1](eventY);
|
|
222
|
+
a.equal(onKeyMock.lastArguments?.[0], 'x y');
|
|
223
|
+
dispose();
|
|
224
|
+
});
|
|
225
|
+
it.should('verify event listener is removed on dispose', (a) => {
|
|
226
|
+
const elementMock = a.element('div');
|
|
227
|
+
const off = a.spyFn(elementMock, 'removeEventListener');
|
|
228
|
+
const dispose = (0, index_js_1.handleKeyboard)({
|
|
229
|
+
element: elementMock,
|
|
230
|
+
onKey: (0, spec_1.mockFn)(() => true),
|
|
231
|
+
delay: 100,
|
|
232
|
+
layout: undefined,
|
|
233
|
+
capture: false,
|
|
234
|
+
});
|
|
235
|
+
dispose();
|
|
236
|
+
a.equal(off.lastEvent?.called, 1);
|
|
237
|
+
});
|
|
150
238
|
});
|
|
151
239
|
});
|