-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkeymap.js
More file actions
161 lines (152 loc) · 6.77 KB
/
keymap.js
File metadata and controls
161 lines (152 loc) · 6.77 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
// allows mapping specific key sequences to actions
'use strict';
(()=>{
const canonicalSpellings = 'ArrowDown ArrowLeft ArrowRight ArrowUp Backspace CapsLock Delete End Enter Escape Home Insert NumLock PageDown PageUp Pause ScrollLock Space Tab'.split(' ');
const lowercaseSpellings = canonicalSpellings.map(s => new RegExp(`\\b${s}\\b`, 'gi'));
// Microsoft SendKeys notation, https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/sendkeys-statement
// 'Return' is from VIM, https://vimhelp.org/intro.txt.html#%3CReturn%3E
const alternateSpellings = 'Down Left Right Up BS CapsLock Del End Return Esc Home Ins NumLock PGDN PGUP Break ScrollLock Spacebar Tab'.split(' ').map(s => new RegExp(`\\b${s}\\b`, 'gi'));
// modifier keys. VIM uses '-', jquery.hotkeys uses '+', https://github.com/jresig/jquery.hotkeys
// Not using meta-
const modifiers = [[/s(hift)?[+-]/gi, '+'], [/c(trl)?[+-]/gi, '^'], [/a(lt)?[+-]/gi, '%']];
function normalize (keyDescriptor){
keyDescriptor = keyDescriptor.trim().replaceAll(/ +/g, ' '); // collapse multiple spaces
lowercaseSpellings.forEach( (re, i) => { keyDescriptor = keyDescriptor.replaceAll(re, canonicalSpellings[i]) } );
alternateSpellings.forEach( (re, i) => { keyDescriptor = keyDescriptor.replaceAll(re, canonicalSpellings[i]) } );
// VIM key descriptors are enclosed in angle brackets; sendkeys are enclosed in braces
keyDescriptor = keyDescriptor.replaceAll(/(?<= |^)<([^>]+)>(?= |$)/g, '$1');
keyDescriptor = keyDescriptor.replaceAll(/{([^}]+|})}/g, '$1');
// uppercase function keys
keyDescriptor = keyDescriptor.replaceAll(/f(\d+)/g, 'F$1');
// it's easiest to turn modifiers into single keys, then reorder them and rename them
modifiers.forEach( pair => { keyDescriptor = keyDescriptor.replaceAll(...pair) } );
// normalize the order of ctrl-alt-shift
keyDescriptor = keyDescriptor.replaceAll(
/[+^%]+(?! |$)/g, // don't match a final [+^%], since that will be an actual character
match => (/\^/.test(match) ? 'ctrl-' : '') + (/%/.test(match) ? 'alt-' : '') + (/\+/.test(match) ? 'shift-' : '')
)
keyDescriptor = keyDescriptor.replaceAll(/shift-([a-zA-Z])\b/g, (match, letter) => letter.toUpperCase() );
return keyDescriptor;
}
// generates successive prefixes of lists of keyDescriptors
// turns 'alt-f x Enter' into [/^alt-f$/, /^alt-f x$/, /^alt-f x Enter$/]
// and /alt-x f\d+ (Enter|Escape)/i into [/^alt-x$/i, /^alt-x f\d+$/i, /^alt-x f\d+ (Enter|Escape)$/i]
function prefixREs(strOrRE){
let sources, ignoreCase;
if (!strOrRE.source){
// escape RegExp from https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions
sources = strOrRE.toString().trim().split(/\s+/).
map(normalize).
map(s => s.replaceAll(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&"));
ignoreCase = '';
}else{
sources = strOrRE.source.trim().split(/\s+/);
ignoreCase = strOrRE.ignoreCase ? 'i' : '';
}
return sources.reduce ( (accumulator, currentValue, i) => {
if (i == 0) return [RegExp(`^${currentValue}$`, ignoreCase)]; // ^...$ to match the entire key
const source = accumulator[i-1].source.replaceAll(/^\^|\$$/g,''); // strip off the previous ^...$
accumulator.push( new RegExp( `^${source} ${currentValue}$`, ignoreCase ) );
return accumulator;
}, []);
};
// ANSI keyboard codes
const specialKeys = {
'Backquote': '`',
'shift-Backquote': '~',
'shift-1': '!',
'shift-2': '@',
'shift-3': '#',
'shift-4': '$',
'shift-5': '%',
'shift-6': '^',
'shift-7': '&',
'shift-8': '*',
'shift-9': '(',
'shift-0': ')',
'Minus': '-',
'shift-Minus': '_',
'Equal': '=',
'shift-Equal': '+',
'BracketRight': '[',
'shift-BracketRight': '{',
'BracketLeft': ']',
'shift-BracketLeft': '}',
'Backslash': '\\',
'shift-Backslash': '|',
'Semicolon': ';',
'shift-Semicolon': ':',
'Quote': "'",
'shift-Quote': '"',
'Comma': ',',
'shift-Comma': '<',
'Period': '.',
'shift-Period': '>',
'Slash': '/',
'shift-Slash': '?',
};
function addKeyDescriptor(evt){
// create a "keyDescriptor" field in evt that represents the keystroke as a whole: "ctrl-alt-Delete" for instance.
const {code, shiftKey, ctrlKey, altKey} = evt;
let key = evt.key; // needs to be variable
if (!key || /^(?:shift|control|meta|alt)$/i.test(key)) return evt; // ignore undefined or modifier keys alone
// we use spaces to delimit keystrokes, so this needs to be changed; use the code
if (key == ' ') key = 'Space';
// In general, use the key field. However, modified letters (ctrl- or alt-) use the code field
// so that ctrl-A is the A key even on non-English keyboards.
if (ctrlKey || altKey){
if (/^(Key|Digit)\w$/.test(code)){
evt.keyDescriptor = code.charAt(code.length-1)[shiftKey ? 'toUpperCase' : 'toLowerCase']();
}else if (/^Numpad/.test(code)){
evt.keyDescriptor = key;
}else{
evt.keyDescriptor = code;
}
if (!/^[a-zA-Z]$/.test(evt.keyDescriptor) && shiftKey) evt.keyDescriptor = 'shift-'+evt.keyDescriptor;
if (evt.keyDescriptor in specialKeys) evt.keyDescriptor = specialKeys[evt.keyDescriptor];
}else{
evt.keyDescriptor = key;
// printable characters should ignore the shift; that's incorporated into the key generated
if (key.length !== 1 && shiftKey) evt.keymap = 'shift-'+evt.keymap;
}
if (altKey) evt.keyDescriptor = 'alt-'+evt.keyDescriptor;
if (ctrlKey) evt.keyDescriptor = 'ctrl-'+evt.keyDescriptor;
return evt;
}
function keymap (keyDescriptorTarget, handler, prefixHandler = ( evt => { evt.preventDefault() } )){
const prefixes = prefixREs(keyDescriptorTarget);
const prefixSymbol = Symbol();
const newHandler = function (evt){
const keyDescriptorSource = addKeyDescriptor(evt).keyDescriptor;
if (!keyDescriptorSource) return; // it is a modifier key
evt.keymapFilter = keyDescriptorTarget;
const el = evt.currentTarget;
let currentSequence = keyDescriptorSource;
if (el[prefixSymbol]) currentSequence = `${el[prefixSymbol]} ${currentSequence}`;
while (currentSequence){
const length = currentSequence.split(' ').length;
if ( prefixes[length-1].test(currentSequence) ){
// we have a match
evt.keymapSequence = el[prefixSymbol] = currentSequence;
prefixHandler.apply(this, arguments);
if (length == prefixes.length){
// we have a match for the whole thing
delete el[prefixSymbol];
return handler.apply(this, arguments);
}
return;
}
// if we get here, then we do not have a match. Maybe we started too soon (looking for 'a b c', got 'a a b c' and matched the beginning of the
// sequence at the first 'a', but that was wrong, so take off the first 'a' and try again
currentSequence = currentSequence.replace(/^\S+[ ]?/, '');
}
delete el[prefixSymbol]; // no matches at all.
};
newHandler.keymapPrefix = prefixSymbol;
newHandler.keymapFilter = keyDescriptorTarget;
return newHandler;
}
globalThis.keymap = keymap;
keymap.normalize = normalize;
keymap.addKeyDescriptor = addKeyDescriptor;
})();