@mapbox/mr-ui
Version:
UI components for Mapbox projects
333 lines (319 loc) • 8.08 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.transformCode = exports.parseCode = exports.formatCodeForDisplay = exports.formatCode = void 0;
var _isMac = require("./is-mac");
/**
* Modifier key should be Command on macOS and Ctrl for Windows/Linux.
*
* Using {@link isMac} instead of Mousetrap's native `'mod'` so it's more predictable.
*/
const transformCode = code => {
const mod = (0, _isMac.isMac)() ? 'command' : 'ctrl';
if (Array.isArray(code)) {
return code.map(item => item.split('mod').join(mod));
} else {
return code.split('mod').join(mod);
}
};
/**
* Keycodes, modifier codes,
* and anything else that can be typed,
* compiled into a big lookup table.
*/
exports.transformCode = transformCode;
const modifierCodes = {
// Shift key, ⇧
'⇧': 16,
shift: 16,
// CTRL key, on Mac: ⌃
'⌃': 17,
ctrl: 17,
// ALT key, on Mac: ⌥ (Alt)
'⌥': 18,
alt: 18,
option: 18,
// META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super)
'⌘': 91,
meta: 91,
cmd: 91,
command: 91,
super: 91,
win: 91
};
const modifierProperties = {
'16': 'shiftKey',
'17': 'ctrlKey',
'18': 'altKey',
'91': 'metaKey'
};
// Initiate hotKeys as an empty object to fight off flow errors.
const hotKeys = {};
Object.assign(hotKeys, {
// Backspace key, on Mac: ⌫ (Backspace)
'⌫': 8,
backspace: 8,
// Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥
'⇥': 9,
'⇆': 9,
tab: 9,
// Return key, ↩
'↩': 13,
return: 13,
enter: 13,
'⌅': 13,
// Pause/Break key
pause: 19,
'pause-break': 19,
// Caps Lock key, ⇪
'⇪': 20,
caps: 20,
'caps-lock': 20,
// Escape key, on Mac: ⎋, on Windows: Esc
'⎋': 27,
escape: 27,
esc: 27,
// Space key
space: 32,
// Page-Up key, or pgup, on Mac: ↖
'↖': 33,
pgup: 33,
'page-up': 33,
// Page-Down key, or pgdown, on Mac: ↘
'↘': 34,
pgdown: 34,
'page-down': 34,
// END key, on Mac: ⇟
'⇟': 35,
end: 35,
// HOME key, on Mac: ⇞
'⇞': 36,
home: 36,
// Insert key, or ins
ins: 45,
insert: 45,
// Delete key, on Mac: ⌦ (Delete)
'⌦': 46,
del: 46,
delete: 46,
// Left Arrow Key, or ←
'←': 37,
left: 37,
'arrow-left': 37,
'arrowleft': 37,
// Up Arrow Key, or ↑
'↑': 38,
up: 38,
'arrow-up': 38,
'arrowup': 38,
// Right Arrow Key, or →
'→': 39,
right: 39,
'arrow-right': 39,
'arrowright': 39,
// Up Arrow Key, or ↓
'↓': 40,
down: 40,
'arrow-down': 40,
'arrowdown': 40,
// odities, printing characters that come out wrong:
// Num-Multiply, or *
'*': 106,
star: 106,
asterisk: 106,
multiply: 106,
// Num-Plus or +
'+': 107,
plus: 107,
// Num-Subtract, or -
'-': 109,
subtract: 109,
// Semicolon
';': 186,
semicolon: 186,
// = or equals
'=': 187,
equals: 187,
// Comma, or ,
',': 188,
comma: 188,
//'-': 189, //???
// Period, or ., or full-stop
'.': 190,
period: 190,
'full-stop': 190,
// Slash, or /, or forward-slash
'/': 191,
slash: 191,
'forward-slash': 191,
// Tick, or `, or back-quote
'`': 192,
tick: 192,
'back-quote': 192,
// Open bracket, or [
'[': 219,
'open-bracket': 219,
// Back slash, or \
'\\': 220,
'back-slash': 220,
// Close backet, or ]
']': 221,
'close-bracket': 221,
// Apostrophe, or Quote, or '
"'": 222,
quote: 222,
apostrophe: 222
});
// NUMPAD 0-9
let i = 95;
let n = 0;
while (++i < 106) {
hotKeys['num-' + n] = i;
++n;
}
// 0-9
i = 47;
n = 0;
while (++i < 58) {
hotKeys['' + n] = i;
++n;
}
// F1-F25
i = 111;
n = 1;
while (++i < 136) {
hotKeys['f' + n] = i;
++n;
}
// ;-a-z
i = 63;
while (++i < 91) {
hotKeys[String.fromCharCode(i).toLowerCase()] = i;
}
// these non-letter keys imply a shift key on US keyboards
const shiftEquivalents = [['~', '`'], ['!', '1'], ['@', '2'], ['#', '3'], ['$', '4'], ['%', '5'], ['^', '6'], ['&', '7'], ['*', '8'], ['(', '9'], [')', '0'], ['_', '-'], ['+', '='], [':', ';'], ['"', "'"], ['<', ','], ['>', '.'], ['?', '/']];
const unshiftedKeys = shiftEquivalents.reduce((memo, key) => {
memo[hotKeys[key[1]]] = key[0];
return memo;
}, {});
const shiftedKeys = shiftEquivalents.reduce((shiftedKeys, key) => {
shiftedKeys[key[0]] = hotKeys[key[1]];
return shiftedKeys;
}, {});
/**
* parse a key code given as a string
* into an object of expectations used
* to match keystrokes.
* @param {String} input a string of a keycode combination
* @returns {Object} expectations
* @example
* parseCode('a');
*/
const parseCode = input => {
const code = input.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g);
const event = {
shiftKey: false,
ctrlKey: false,
altKey: false,
metaKey: false,
keyCode: -1
};
if (Array.isArray(code)) {
for (let i = 0; i < code.length; i++) {
// Make sure to convert 'mod' into either 'command' or 'ctrl' based
// on platform.
let codePart = transformCode(code[i]);
// Normalise matching errors.
if (codePart === '++') {
codePart = '+';
}
if (typeof codePart === 'string') {
if (codePart in modifierCodes) {
const modCode = modifierCodes[codePart];
const propName = modifierProperties[modCode];
if (propName) {
event[propName] = true;
}
} else if (codePart in shiftedKeys) {
event.keyCode = shiftedKeys[codePart];
event.shiftKey = true;
} else if (codePart in hotKeys) {
event.keyCode = hotKeys[codePart];
}
}
}
}
return event;
};
exports.parseCode = parseCode;
const findShortestIdentifiers = set => {
const reversed = {};
let k;
for (k in set) {
const id = set[k];
if (reversed[id] === undefined) reversed[id] = [];
reversed[id].push(k);
}
const shorter = (a, b) => {
return a.length - b.length;
};
for (k in reversed) {
reversed[k] = reversed[k].sort(shorter)[0];
}
return reversed;
};
const shortest = {
modifierCodes: findShortestIdentifiers(modifierCodes),
keyCodes: findShortestIdentifiers(hotKeys)
};
/**
* Format a key code combination into an array of shortest-possible
* key identifiers
* @param {Object} input parsed keybinding
* @returns {Array<String>} array of identifiers
* @example
* formatCode(parseCode('a'));
*/
const formatCode = input => {
const formatted = [];
// The key code is the same for '!' and '1'. For '!', input.shiftKey = true.
// This little bit helps separate '!' from 'shift+cmd+z' (redo).
const shiftedCode = Object.prototype.hasOwnProperty.call(unshiftedKeys, input.keyCode) ? unshiftedKeys[input.keyCode] : false;
// The shift key is a modifier when used in context of redo, where the formatted
// output should show ⇧. For '!', we don't want to show '⇧ 1' (or '⇧ !').
const shiftKeyIsModifier = input.shiftKey && !shiftedCode;
if (shiftKeyIsModifier) {
// Add that shift key
formatted.push(shortest.modifierCodes[16]);
}
if (input.metaKey) formatted.push(shortest.modifierCodes[91]);
if (input.altKey) formatted.push(shortest.modifierCodes[18]);
if (input.ctrlKey) formatted.push(shortest.modifierCodes[17]);
// '!', '@', '#'
if (input.shiftKey && shiftedCode) {
formatted.push(shiftedCode);
} else if (input.keyCode !== -1) {
const shortCode = shortest.keyCodes[input.keyCode];
formatted.push(shortCode ? shortCode : input.keyCode.toString());
}
return formatted;
};
// Turn Mac-specific characters into OS-general ones.
exports.formatCode = formatCode;
const macKeyAlernatives = {
'⌘': 'Ctrl ',
'⌥': 'Alt ',
'⇧': 'Shift '
};
const formatCodeForDisplay = code => {
const isMacCached = (0, _isMac.isMac)();
return formatCode(parseCode(code)).map(key => {
// Circumventing bad glyphs in our monospaced font.
if (key === '⌫') {
return 'Del';
}
return isMacCached ? key : macKeyAlernatives[key] || key;
});
};
exports.formatCodeForDisplay = formatCodeForDisplay;