UNPKG

react-hotkeys

Version:

A declarative library for handling hotkeys and focus within a React application

1,662 lines (1,417 loc) 262 kB
/** * ISC License * * Copyright (c) 2018, Aleck Greenham * * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('prop-types'), require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'prop-types', 'react'], factory) : (global = global || self, factory(global.ReactHotkeys = {}, global.PropTypes, global.React)); }(this, function (exports, PropTypes, React) { 'use strict'; PropTypes = PropTypes && PropTypes.hasOwnProperty('default') ? PropTypes['default'] : PropTypes; var React__default = 'default' in React ? React['default'] : React; function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; } function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function dictionaryFrom(array) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; return array.reduce(function (memo, element) { memo[element] = value || { value: element }; return memo; }, {}); } /** * Default configuration values * @private */ var _defaultConfiguration = { /** * The level of logging of its own behaviour React HotKeys should perform. * @type {LogLevel} */ logLevel: 'warn', /** * Default key event key maps are bound to, if left unspecified * @type {KeyEventName} */ defaultKeyEvent: 'keydown', /** * The default component type to wrap HotKey components' children in, to provide * the required focus and keyboard event listening for HotKeys to function */ defaultComponent: 'div', /** * The default tabIndex value passed to the wrapping component used to contain * HotKey components' children. -1 skips focusing the element when tabbing through * the DOM, but allows focusing programmatically. */ defaultTabIndex: '-1', /** * The HTML tags that React HotKeys should ignore key events from. This only works * if you are using the default ignoreEventsCondition function. * @type {String[]} */ ignoreTags: ['input', 'select', 'textarea'], /** * Whether to allow hard sequences, or the binding of handlers to actions that have * names that are valid key sequences, which implicitly define actions that are * triggered by that key sequence */ enableHardSequences: false, /** * Whether to ignore changes to keyMap and handlers props by default (this reduces * a significant amount of unnecessarily resetting internal state) * * @type {boolean} */ ignoreKeymapAndHandlerChangesByDefault: true, /** * The function used to determine whether a key event should be ignored by React * Hotkeys. By default, keyboard events originating elements with a tag name in * ignoreTags, or a isContentEditable property of true, are ignored. * * @type {Function<KeyboardEvent>} */ ignoreEventsCondition: function ignoreEventsCondition(event) { var target = event.target; if (target && target.tagName) { var tagName = target.tagName.toLowerCase(); return Configuration.option('_ignoreTagsDict')[tagName] || target.isContentEditable; } else { return false; } }, /** * Whether to ignore repeated keyboard events when a key is being held down * @type {boolean} */ ignoreRepeatedEventsWhenKeyHeldDown: true, /** * Whether React HotKeys should simulate keypress events for the keys that do not * natively emit them. * @type {boolean} */ simulateMissingKeyPressEvents: true, /** * Whether to call stopPropagation() on events after they are handled (preventing * the event from bubbling up any further, both within React Hotkeys and any other * event listeners bound in React). * * This does not affect the behaviour of React Hotkeys, but rather what happens to * the event once React Hotkeys is done with it (whether it's allowed to propagate * any further through the Render tree). * @type {boolean} */ stopEventPropagationAfterHandling: true, /** * Whether to call stopPropagation() on events after they are ignored (preventing * the event from bubbling up any further, both within React Hotkeys and any other * event listeners bound in React). * * This does not affect the behaviour of React Hotkeys, but rather what happens to * the event once React Hotkeys is done with it (whether it's allowed to propagate * any further through the Render tree). * @type {boolean} */ stopEventPropagationAfterIgnoring: true, /** * Whether to allow combination submatches - e.g. if there is an action bound to * cmd, pressing shift+cmd will *not* trigger that action when * allowCombinationSubmatches is false. * * @note This option is ignored for combinations involving command (Meta) and * submatches are <i>always</i> allowed because Meta hides keyup events * of other keys, so until Command is released, it's impossible to know * if one of the keys that has also been pressed has been released. * @see https://github.com/greena13/react-hotkeys/pull/207 * @type {boolean} */ allowCombinationSubmatches: false, /** * A mapping of custom key codes to key names that you can then use in your * key sequences * @type {Object.<Number, KeyName>} */ customKeyCodes: {} }; var _configuration = _objectSpread({}, _defaultConfiguration); /** * Turn our array of tags to ignore into a dictionary, for faster lookup */ _configuration._ignoreTagsDict = dictionaryFrom(_configuration.ignoreTags, true); /** * Handles getting and setting global configuration values, that affect how * React Hotkeys behaves * @class */ var Configuration = /*#__PURE__*/ function () { function Configuration() { _classCallCheck(this, Configuration); } _createClass(Configuration, null, [{ key: "init", /** * Merges the specified configuration options with the current values. * @see _configuration */ value: function init(configuration) { var _this = this; var ignoreTags = configuration.ignoreTags, customKeyCodes = configuration.customKeyCodes; if (ignoreTags) { configuration._ignoreTagsDict = dictionaryFrom(configuration.ignoreTags); } if (customKeyCodes) { configuration._customKeyNamesDict = dictionaryFrom(Object.values(configuration.customKeyCodes)); } Object.keys(configuration).forEach(function (key) { _this.set(key, configuration[key]); }); } /** * Sets a single configuration value by name * @param {string} key - Name of the configuration value to set * @param {*} value - New value to set */ }, { key: "set", value: function set(key, value) { _configuration[key] = value; } }, { key: "reset", value: function reset(key) { _configuration[key] = _defaultConfiguration[key]; } /** * Gets a single configuration value by name * @param {string} key - Name of the configuration value * @returns {*} Configuration value */ }, { key: "option", value: function option(key) { return _configuration[key]; } }]); return Configuration; }(); /** * Encapsulates all logging behaviour and provides the ability to specify the level * of logging desired. * @class */ var Logger = /*#__PURE__*/ function () { _createClass(Logger, [{ key: "noop", /** * Icons prefixed to the start of logging statements that cycled through each * time a focus tree changes, making it easier to quickly spot events related * to the same focus tree. */ /** * Icons prefixed to the start of logging statements that cycled through each * time a component ID changes, making it easier to quickly spot events related * to the same component. */ /** * Icons prefixed to the start of logging statements that cycled through each * time an event ID changes, making it easier to quickly trace the path of KeyEvent * objects as they propagate through multiple components. */ /** * The level of logging to perform * @typedef {'none'|'error'|'warn'|'info'|'debug'|'verbose'} LogLevel */ /** * Levels of log severity - the higher the log level, the greater the amount (and * lesser the importance) of information logged to the console about React HotKey's * behaviour * @enum {number} LogLevel */ value: function noop() {} /** * By default, calls to all log severities are a no-operation. It's only when the * user specifies a log level, are they replaced with logging statements * @type {Logger.noop} */ }]); function Logger() { var _this = this; var logLevel = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'warn'; _classCallCheck(this, Logger); _defineProperty(this, "verbose", this.noop); _defineProperty(this, "debug", this.noop); _defineProperty(this, "info", this.noop); _defineProperty(this, "warn", this.noop); _defineProperty(this, "error", this.noop); this.logLevel = this.constructor.levels[logLevel]; if (this.logLevel >= this.constructor.levels.error) { this.error = console.error; } else { return; } if (this.logLevel >= this.constructor.levels.warn) { this.warn = console.warn; } else { return; } ['info', 'debug', 'verbose'].some(function (logLevel) { if (_this.logLevel >= _this.constructor.levels[logLevel]) { _this[logLevel] = console.log; return false; } return true; }); } return Logger; }(); _defineProperty(Logger, "logIcons", ['📕', '📗', '📘', '📙']); _defineProperty(Logger, "componentIcons", ['🔺', '⭐️', '🔷', '🔶', '⬛️']); _defineProperty(Logger, "eventIcons", ['❤️', '💚', '💙', '💛', '💜', '🧡']); _defineProperty(Logger, "levels", { none: 0, error: 1, warn: 2, info: 3, debug: 4, verbose: 5 }); /** * @typedef {number} KeyEventType index (0-2) of which position in an event record * a particular event is located */ /** * Enum for types of key events * @readonly * @enum {KeyEventType} */ var KeyEventType = { keydown: 0, keypress: 1, keyup: 2 }; var ModifierFlagsDictionary = { Shift: ['shiftKey'], Meta: ['metaKey'], Control: ['ctrlKey'], Alt: ['altKey'] }; /** * Dictionary of symbols that correspond to keys when pressed with the shift key * also held down. Used for combinations involving the shift key and one or more * others. (e.g. shift+1) */ var ShiftedKeysDictionary = { '`': ['~'], '1': ['!'], '2': ['@', /** UK Keyboard: **/ '"'], '3': ['#', /** UK Keyboard: **/ '£'], '4': ['$'], '5': ['%'], '6': ['^'], '7': ['&'], '8': ['*'], '9': ['('], '0': [')'], '-': ['_'], '=': ['plus'], ';': [':'], "'": ['"', /** UK Keyboard: **/ '@'], ',': ['<'], '.': ['>'], '/': ['?'], '\\': ['|'], '[': ['{'], ']': ['}'], /** * UK Keyboard: */ '#': ['~'] }; /** * Returns the corresponding symbol or character for a particular key, when it is * pressed with the shift key also held down * @param {NormalizedKeyName} keyName Name of the key * @returns {ReactKeyName[]} Symbol or character for the key, when it is pressed with the * shift key */ function resolveShiftedAlias(keyName) { return ShiftedKeysDictionary[keyName] || [keyName.length === 1 ? keyName.toUpperCase() : keyName]; } function hasKey(object, key) { return object.hasOwnProperty(key); } function invertArrayDictionary(dictionary) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return Object.keys(dictionary).reduce(function (memo, key) { var arrayValue = dictionary[key]; arrayValue.forEach(function (shiftedKey) { if (!hasKey(memo, shiftedKey)) { memo[shiftedKey] = []; } memo[shiftedKey].push(key); }); if (options.includeOriginal) { if (!hasKey(memo, key)) { memo[key] = []; } memo[key] = [].concat(_toConsumableArray(memo[key]), _toConsumableArray(arrayValue)); } return memo; }, {}); } var UnshiftedKeysDictionary = invertArrayDictionary(ShiftedKeysDictionary); /** * Returns the name of the key that must be pressed with the shift key, to yield the * specified symbol * @param {NormalizedKeyName} keyName Name of the key * @returns {ReactKeyName[]} Name of the key that must be pressed with the shift key, to * yield the specified symbol */ function resolveUnshiftedAlias(keyName) { return UnshiftedKeysDictionary[keyName] || [keyName.length === 1 ? keyName.toLowerCase() : keyName]; } /** * A dictionary of key aliases to make it easier to specify key maps that work * across different keyboard layouts and operating systems - this builds on top * of what React already does. */ var KeyOSAndLayoutAliasesDictionary = {}; var KeyOSAndLayoutAliasesDictionary$1 = invertArrayDictionary(KeyOSAndLayoutAliasesDictionary, { includeOriginal: true }); function isString(object) { return typeof object === 'string'; } function stripSuperfluousWhitespace(target) { if (isString(target)) { return target.trim().replace(/\s+/g, ' '); } return target; } /** * A mapping between Mousetrap syntax and React syntax to support the use of both */ var MousetrapToReactKeyNamesDictionary = { /** * Generic */ 'tab': 'Tab', 'capslock': 'CapsLock', 'shift': 'Shift', 'meta': 'Meta', 'alt': 'Alt', 'ctrl': 'Control', 'space': ' ', 'spacebar': ' ', 'escape': 'Escape', 'esc': 'Escape', 'left': 'ArrowLeft', 'right': 'ArrowRight', 'up': 'ArrowUp', 'down': 'ArrowDown', /** * Mac */ 'return': 'Enter', 'del': 'Delete', 'command': 'Meta', 'option': 'Alt', /** * Windows */ 'enter': 'Enter', 'backspace': 'Backspace', 'ins': 'Insert', 'pageup': 'PageUp', 'pagedown': 'PageDown', 'end': 'End', 'home': 'Home', 'contextmenu': 'ContextMenu', 'numlock': 'Clear' }; /** * A mapping between key names and official names */ var KeyShorthandDictionary = { 'cmd': 'Meta' }; /** * @typedef {string} KeyName Name of the keyboard key */ /** * @typedef {string} ReactKeyName Name used by React to refer to key */ /** * Returns the name for the specified key used by React. Supports translating key aliases * used by mousetrap to their counterparts in React * @param {KeyName} keyName Name of the key to resolve to the React equivalent * @returns {ReactKeyName} Name used by React to refer to the key */ function standardizeKeyName(keyName) { var _keyName = keyName.toLowerCase(); return MousetrapToReactKeyNamesDictionary[_keyName] || KeyShorthandDictionary[_keyName] || (keyName.match(/^f\d+$/) ? keyName.toUpperCase() : keyName); } /** * Translation from legacy `keyCode` to HTML5 `key` * Only special keys supported, all others depend on keyboard layout or browser * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names */ var translateToKey = { '8': 'Backspace', '9': 'Tab', '12': 'Clear', '13': 'Enter', '16': 'Shift', '17': 'Control', '18': 'Alt', '19': 'Pause', '20': 'CapsLock', '27': 'Escape', '32': ' ', '33': 'PageUp', '34': 'PageDown', '35': 'End', '36': 'Home', '37': 'ArrowLeft', '38': 'ArrowUp', '39': 'ArrowRight', '40': 'ArrowDown', '45': 'Insert', '46': 'Delete', '112': 'F1', '113': 'F2', '114': 'F3', '115': 'F4', '116': 'F5', '117': 'F6', '118': 'F7', '119': 'F8', '120': 'F9', '121': 'F10', '122': 'F11', '123': 'F12', '144': 'NumLock', '145': 'ScrollLock', '224': 'Meta' }; /** * Dictionary of keys whose name is not a single symbol or character */ var NonPrintableKeysDictionary = dictionaryFrom(Object.values(translateToKey), true); /** * Whether the specified key is a valid key name that is not a single character or * symbol * @param {ReactKeyName} keyName Name of the key * @returns {boolean} Whether the key is a valid special key */ function isNonPrintableKeyName(keyName) { return !!NonPrintableKeysDictionary[keyName]; } /** * Whether the specified key name is among those defined as custom key codes * @param {ReactKeyName} keyName Name of the key * @returns {boolean} true if keyName matches a custom key name */ function isCustomKeyName(keyName) { return Configuration.option('_customKeyNamesDict')[keyName]; } function isValidKey(keyName) { return isNonPrintableKeyName(keyName) || String.fromCharCode(keyName.charCodeAt(0)) === keyName || isCustomKeyName(keyName); } var InvalidKeyNameError = /*#__PURE__*/ function (_Error) { _inherits(InvalidKeyNameError, _Error); function InvalidKeyNameError() { var _getPrototypeOf2; var _this; _classCallCheck(this, InvalidKeyNameError); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(InvalidKeyNameError)).call.apply(_getPrototypeOf2, [this].concat(args))); _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "name", 'InvalidKeyNameError'); return _this; } return InvalidKeyNameError; }(_wrapNativeSuper(Error)); /** * Returns a normalized KeyCombinationString (with the key names in the combination * sorted in alphabetical order) * @param {KeyName[]} keys List of key names to sort and reconstitute as a * KeyCombinationString * @returns {NormalizedKeyCombinationString} Normalized KeyCombinationString */ function normalizedCombinationId(keys) { return keys.sort().join('+'); } /** * Parses KeySequenceStrings and returns KeySequenceOptions * * Used primarily to parse strings describing hot key sequences and combinations * so that they may be matched with key events when they occur. * @class */ var KeySequenceParser = /*#__PURE__*/ function () { function KeySequenceParser() { _classCallCheck(this, KeySequenceParser); } _createClass(KeySequenceParser, null, [{ key: "parse", /** * @typedef {Object} BasicKeyCombination Object containing the basic information that * describes a key combination * @property {KeyCombinationString} id - String description of keys involved in the key * combination * @property {number} size - Number of keys involved in the combination * @property {Object.<KeyName, Boolean>} keyDictionary - Dictionary of key names involved * in the key combination * @property {KeyEventType} keyEventType - Record index for key event that * the matcher should match on */ /** * @typedef {string} KeySequenceString String describing a sequence of one or more key * combinations with whitespace separating key combinations in the sequence and '+' * separating keys within a key combination. */ /** * @typedef {KeySequenceString} NormalizedKeySequenceId key sequence id with all of the * combination id's normalized */ /** * @typedef {Object} BasicKeySequence Object containing the basic information that * describes a key sequence * @property {NormalizedKeySequenceId} prefix - Normalized key sequence id * @property {number} size - Number of combinations involved in the sequence */ /** * @typedef {Object} KeySequenceObject Object containing description of a key sequence * to compared against key events * @property {KeySequenceString} id Id describing key sequence used for matching against * key events * @property {ComponentId} componentId Id associated with the HotKeys component * that registered the key sequence * @property {BasicKeyCombination[]} sequence A list of key combinations involved in * the sequence * @property {number} size Number of key combinations in the key sequence * @property {KeyEventType} keyEventType Index that matches key event type * @property {ActionName} actionName Name of the action that should be triggered if a * keyboard event matching the sequence and event type occur */ /** * @typedef {Object} KeySequenceOptions Object containing the results of parsing a * KeySequenceString * @property {BasicKeyCombination} combination Properties of the final combination in * the sequence * @property {BasicKeySequence} sequence Properties of the sequence of keys leading * up to the final combination */ /** * Parses a KeySequenceString and returns a KeySequenceOptions object containing * information about the sequence in a format that is easier to query * @param {KeySequenceString} sequenceString String describing a key sequence to * parse * @param {Object} options Configuration object describing how the KeySequenceString * should be parsed. * @param {KeyEventType} options.keyEventType Event record index indicating * what key event the sequence should match * @param {boolean} options.ensureValidKeys Whether to throw an exception if an invalid * key name is found in the key combination string. * @returns {KeySequenceOptions} Object containing information about the key * sequence described by the KeySequenceString */ value: function parse(sequenceString) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var trimmedSequenceString = stripSuperfluousWhitespace(sequenceString); var keyCombinationsArray = trimmedSequenceString.split(' '); try { var nonTerminalCombinations = keyCombinationsArray.slice(0, keyCombinationsArray.length - 1); var terminalCombination = keyCombinationsArray[keyCombinationsArray.length - 1]; var prefix = nonTerminalCombinations.map(function (keyCombination) { var keysInComboDict = parseCombination(keyCombination, options); return normalizedCombinationId(Object.keys(keysInComboDict)); }).join(' '); var keysInComboDict = parseCombination(terminalCombination, options); var normalizedComboString = normalizedCombinationId(Object.keys(keysInComboDict)); var combination = { id: normalizedComboString, keyDictionary: keysInComboDict, keyEventType: options.keyEventType, size: Object.keys(keysInComboDict).length }; return { sequence: { prefix: prefix, size: nonTerminalCombinations.length + 1 }, combination: combination }; } catch (InvalidKeyNameError$$1) { return { sequence: null, combination: null }; } } }]); return KeySequenceParser; }(); /** * @typedef {string} KeyCombinationString String describing a combination of one or more * keys separated by '+' */ /** * @typedef {KeyCombinationString} NormalizedKeyCombinationString key combination id where * the keys have been normalized (sorted in alphabetical order) */ /** * @typedef {Object.<ReactKeyName, Boolean>} KeyDictionary Registry of the names * of keys in a particular combination, for easy/quick checking if a particular * key is in the key combination */ /** * Parses a key combination string and returns the corresponding KeyDictionary * @param {KeyCombinationString} string Describes key combination * @param {Object} options Options hash of how the string should be parsed * @param {boolean} options.ensureValidKeys Whether to throw an exception if an invalid * key name is found in the key combination string. * @returns {KeyDictionary} Dictionary of keys in the parsed combination */ function parseCombination(string) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; return string.replace(/^\+|(\s|[^+]\+)\+/, '$1plus').split('+').reduce(function (keyDictionary, keyName) { var finalKeyName = standardizeKeyName(keyName); if (options.ensureValidKeys) { if (!isValidKey(finalKeyName)) { throw new InvalidKeyNameError(); } } keyDictionary[finalKeyName] = true; return keyDictionary; }, {}); } /** * A dictionary of symbols for each key, when pressed with the alt key also held. * Used for combinations that involve the alt key and one or more others. (e.g. * shift+a) */ var AltedKeysDictionary = { '`': ['`'], '1': ['¡'], '2': ['™'], '3': ['£'], '4': ['¢'], '5': ['∞'], '6': ['§'], '7': ['¶'], '8': ['•'], '9': ['ª'], '0': ['º'], '-': ['–'], '=': ['≠'], 'a': ['å'], 'b': ['∫'], 'c': ['ç'], 'd': ['∂'], 'e': ['´'], 'f': ['ƒ'], 'g': ['©'], 'h': ['˙'], 'i': ['ˆ'], 'j': ['∆'], 'k': ['˚'], 'l': ['¬'], 'm': ['µ'], 'n': ['˜'], 'o': ['ø'], 'p': ['π'], 'q': ['œ'], 'r': ['®'], 's': ['ß'], 't': ['†'], 'u': ['¨'], 'v': ['√'], 'w': ['∑'], 'x': ['≈'], 'y': ['¥'], 'z': ['Ω'], '[': ['“'], ']': ['‘'], "\\": ['«'], "'": ['æ'], ';': ['…'], ',': ['≤'], '.': ['≥'], '/': ['÷'] }; var UnaltedKeysDictionary = invertArrayDictionary(AltedKeysDictionary); /** * Returns the name of the key that must be pressed with the alt key, to yield the * specified symbol * @param {ReactKeyName} keyName Name of the key * @returns {ReactKeyName[]} Name of the key that must be pressed with the alt key, to * yield the specified symbol */ function resolveUnaltedAlias(keyName) { return UnaltedKeysDictionary[keyName] || [keyName]; } /** * Returns the corresponding symbol or character for a particular key, when it is * pressed with the alt key also held down * @param {NormalizedKeyName} keyName Name of the key * @returns {ReactKeyName[]} Symbol or character for the key, when it is pressed with the * alt key */ function resolveAltedAlias(keyName) { return AltedKeysDictionary[keyName] || [keyName]; } /** * A dictionary of symbols for each key, when pressed with the alt and shift key also * held. Used for combinations that involve the shift and alt key and one or more * others (e.g. shift+alt+a) */ var AltShiftedKeysDictionary = { '`': ['`'], '1': ['⁄'], '2': ['€'], '3': ['‹'], '4': ['›'], '5': ['fi'], '6': ['fl'], '7': ['‡'], '8': ['°'], '9': ['·'], '0': ['‚'], '-': ['—'], '=': ['±'], 'a': ['Å'], 'b': ['ı'], 'c': ['Ç'], 'd': ['Î'], 'e': ['´'], 'f': ['Ï'], 'g': ['˝'], 'h': ['Ó'], 'i': ['ˆ'], 'j': ['Ô'], 'k': [''], 'l': ['Ò'], 'm': ['Â'], 'n': ['˜'], 'o': ['Ø'], 'p': ['π'], 'q': ['Œ'], 'r': ['‰'], 's': ['Í'], 't': ['Î'], 'u': ['¨'], 'v': ['◊'], 'w': ['„'], 'x': ['˛'], 'y': ['Á'], 'z': ['¸'], '[': ['”'], ']': ['’'], "\\": ['»'], "'": ['Æ'], ';': ['Ú'], ',': ['¯'], '.': ['˘'] }; var UnaltShiftedKeysDictionary = invertArrayDictionary(AltShiftedKeysDictionary); /** * Returns the name of the key that must be pressed with the shift and alt keys, * to yield the specified symbol * @param {ReactKeyName} keyName Name of the key * @returns {ReactKeyName[]} Name of the key that must be pressed with the alt key, to * yield the specified symbol */ function resolveUnaltShiftedAlias(keyName) { return UnaltShiftedKeysDictionary[keyName] || resolveUnshiftedAlias(keyName); } /** * Returns the corresponding symbol or character for a particular key, when it is * pressed with the alt and shift keys also held down * @param {NormalizedKeyName} keyName Name of the key * @returns {ReactKeyName[]} Symbol or character for the key, when it is pressed with the * alt and shit keys */ function resolveAltShiftedAlias(keyName) { return AltShiftedKeysDictionary[keyName] || [keyName]; } /** * Serializes instances of KeyCombination to KeyCombinationString. * * Used primarily to serialize string representations of key events as they happen. * @class */ var KeyCombinationSerializer = /*#__PURE__*/ function () { function KeyCombinationSerializer() { _classCallCheck(this, KeyCombinationSerializer); } _createClass(KeyCombinationSerializer, null, [{ key: "serialize", /** * Returns a string representation of a single KeyCombination * @param {KeyCombination} keyCombination KeyCombination to serialize * @returns {string[]} Serialization of KeyCombination */ value: function serialize(keyCombination) { var combinationIncludesShift = keyCombination['Shift']; var combinationIncludesAlt = keyCombination['Alt']; var keyCombinationIdDict = {}; /** * List of key names in alphabetical order * @type {string[]} */ var sortedKeys = Object.keys(keyCombination).sort(); sortedKeys.forEach(function (keyName) { var keyAliases = []; if (combinationIncludesShift) { if (combinationIncludesAlt) { var unaltShiftedKeyNames = resolveUnaltShiftedAlias(keyName); var altShiftedKeyNames = resolveAltShiftedAlias(keyName); keyAliases = [].concat(_toConsumableArray(keyAliases), [keyName], _toConsumableArray(unaltShiftedKeyNames), _toConsumableArray(altShiftedKeyNames)); } else { var unshiftedKeyNames = resolveUnshiftedAlias(keyName); var shiftedKeyNames = resolveShiftedAlias(keyName); keyAliases = [].concat(_toConsumableArray(keyAliases), [keyName], _toConsumableArray(unshiftedKeyNames), _toConsumableArray(shiftedKeyNames)); } } else if (combinationIncludesAlt) { var unaltedKeyNames = resolveUnaltedAlias(keyName); var altedKeyNames = resolveAltedAlias(keyName); keyAliases = [].concat(_toConsumableArray(keyAliases), [keyName], _toConsumableArray(unaltedKeyNames), _toConsumableArray(altedKeyNames)); } else { keyAliases.push(keyName); var keyAlias = KeyOSAndLayoutAliasesDictionary$1[keyName]; if (keyAlias) { keyAliases = [].concat(_toConsumableArray(keyAliases), _toConsumableArray(keyAlias)); } } var keyCombinationIds = Object.keys(keyCombinationIdDict); if (keyCombinationIds.length > 0) { keyCombinationIds.forEach(function (keyCombinationId) { keyAliases.forEach(function (keyAlias) { keyCombinationIdDict[keyCombinationId + "+".concat(keyAlias)] = _objectSpread({}, keyCombinationIdDict[keyCombinationId], _defineProperty({}, keyAlias, true)); }); delete keyCombinationIdDict[keyCombinationId]; }); } else { keyAliases.forEach(function (keyAlias) { keyCombinationIdDict[keyAlias] = _defineProperty({}, keyAlias, true); }); } }); return Object.values(keyCombinationIdDict).map(function (keysInCombo) { return Object.keys(keysInCombo).sort().join('+'); }); } /** * Whether the specified key sequence is valid (is of the correct format and contains * combinations consisting entirely of valid keys) * @param {KeySequenceString} keySequence Key sequence to validate * @returns {boolean} Whether the key sequence is valid */ }, { key: "isValidKeySerialization", value: function isValidKeySerialization(keySequence) { if (keySequence.length > 0) { return !!KeySequenceParser.parse(keySequence, { ensureValidKeys: true }).combination; } else { return false; } } }]); return KeyCombinationSerializer; }(); /** * Enum for index values for KeyEvents * @readonly * @enum {number} */ var KeyEventSequenceIndex = { previous: 0, current: 1 }; /** * Returns a list of accepted aliases for the specified key * @param {NormalizedKeyName} keyName Name of the key * @returns {ReactKeyName[]} List of key aliases */ function resolveKeyAlias(keyName) { return KeyOSAndLayoutAliasesDictionary$1[keyName] || [keyName]; } function applicableAliasFunctions(keyDictionary) { if (keyDictionary['Shift']) { if (keyDictionary['Alt']) { return [resolveAltShiftedAlias, resolveUnaltShiftedAlias]; } else { return [resolveShiftedAlias, resolveUnshiftedAlias]; } } else { if (keyDictionary['Alt']) { return [resolveAltedAlias, resolveUnaltedAlias]; } else { var nop = function nop(keyName) { return [keyName]; }; return [nop, nop]; } } } function isUndefined(object) { return typeof object === 'undefined'; } /** * @typedef {number} KeyEventState */ /** * Enum for different states a key event can be recorded in * @readonly * @enum {KeyEventState} */ var KeyEventState = { unseen: 0, seen: 1, simulated: 2 }; /** * Creates and modifies KeyEvents * @class */ var KeyEventStateArrayManager = /*#__PURE__*/ function () { function KeyEventStateArrayManager() { _classCallCheck(this, KeyEventStateArrayManager); } _createClass(KeyEventStateArrayManager, null, [{ key: "newRecord", /** * Makes a new KeyEvent with one of the bits set to true * @param {KeyEventType=} keyEventType Index of bit to set to true * @param {KeyEventState} keyEventState The state to set the key event to * @returns {KeyEvent} New key event record with bit set to true */ value: function newRecord(keyEventType, keyEventState) { var record = [KeyEventState.unseen, KeyEventState.unseen, KeyEventState.unseen]; if (!isUndefined(keyEventType)) { for (var i = 0; i <= keyEventType; i++) { record[i] = keyEventState; } } return record; } /** * Sets a bit in the map to true * @param {KeyEvent} record Map to set a bit to true * @param {KeyEventType} index Index of bit to set * @param {KeyEventState} keyEventState The state to set the key event to */ }, { key: "setBit", value: function setBit(record, index, keyEventState) { record[index] = keyEventState; return record; } /** * Returns a new record with the same values as the one passed to it * @param {KeyEvent} original Record to copy * @returns {KeyEvent} Record with the same values as the original */ }, { key: "clone", value: function clone(original) { var record = this.newRecord(); for (var i = 0; i < original.length; i++) { record[i] = original[i]; } return record; } }]); return KeyEventStateArrayManager; }(); function isObject(target) { return !Array.isArray(target) && _typeof(target) === 'object' && target !== null; } function isEmpty(target) { if (isObject(target)) { return Object.keys(target).length === 0; } else { return !target ? true : target.length === 0; } } function size(collection) { return isObject(collection) ? Object.keys(collection).length : collection.length; } /** * Record of one or more keys pressed together, in a combination * @class */ var KeyCombination = /*#__PURE__*/ function () { /** * Creates a new KeyCombination instance * @param {Object.<ReactKeyName, Array.<KeyEventState[]>>} keys Dictionary * of keys * @returns {KeyCombination} */ function KeyCombination() { var keys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, KeyCombination); this._keys = keys; this._includesKeyUp = false; this._update(); } /******************************************************************************** * Getters *********************************************************************************/ /** * List of ids (serialized representations) for the keys involved in the combination * @returns {KeySequence[]} List of combination ids */ _createClass(KeyCombination, [{ key: "getIds", value: function getIds() { return this._ids; } /** * Dictionary mapping keys to their acceptable aliases. This includes "shifted" or * "alted" key characters. * @returns {Object.<ReactKeyName, ReactKeyName[]>} */ }, { key: "getKeyAliases", value: function getKeyAliases() { return this._keyAliases; } /** * A normalized version of the key, achieved by comparing it to the list of known * aliases for the keys in the combination * @param {ReactKeyName} keyName Name of the key to normalize * @returns {ReactKeyName} Normalized key name */ }, { key: "getNormalizedKeyName", value: function getNormalizedKeyName(keyName) { var keyState = this._keys[keyName]; if (keyState) { return keyName; } else { var keyAlias = this._keyAliases[keyName]; if (keyAlias) { return keyAlias; } else { return keyName; } } } /******************************************************************************** * Query attributes of entire combination *********************************************************************************/ /** * Number of keys involved in the combination * @returns {number} Number of keys */ }, { key: "getNumberOfKeys", value: function getNumberOfKeys() { return size(this._keys); } /** * Whether there are any keys in the combination * @returns {boolean} true if there is 1 or more keys involved in the combination, * else false. */ }, { key: "any", value: function any() { return Object.keys(this._getKeyStates()).length > 0; } /** * Whether any of the keys in the combination have been released * @returns {boolean} true if at least 1 key has been released in the combination */ }, { key: "isEnding", value: function isEnding() { return this._includesKeyUp; } /** * Whether there are any keys in the current combination still being pressed * @returns {boolean} True if all keys in the current combination are released */ }, { key: "hasEnded", value: function hasEnded() { return isEmpty(this.keysStillPressedDict()); } /******************************************************************************** * Adding & modifying key states *********************************************************************************/ /** * Add a new key to the combination (starting with a state of keydown) * @param {ReactKeyName} keyName Name of key * @param {KeyEventState} keyEventState State key is in * @returns {void} */ }, { key: "addKey", value: function addKey(keyName, keyEventState) { this._setKeyState(keyName, [KeyEventStateArrayManager.newRecord(), KeyEventStateArrayManager.newRecord(KeyEventType.keydown, keyEventState)]); } /** * Adds a key event to th