react-hotkeys
Version:
A declarative library for handling hotkeys and focus within a React application
305 lines (268 loc) • 11.4 kB
JavaScript
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 _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 _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; }
import Configuration from '../config/Configuration';
import size from '../../utils/collection/size';
import keyupIsHiddenByCmd from '../../helpers/resolving-handlers/keyupIsHiddenByCmd';
/**
* Object containing all information necessary to match a handler to a history of
* key combinations
* @typedef {Object} MatchingActionConfig
* @property {NormalizedKeySequenceId} prefix - String describing the sequence of key
* combinations, before the final key combination (an empty string for
* sequences that are a single key combination)
* @property {number} sequenceLength - Number of combinations involved in the
* sequence
* @property {KeyCombinationString} id - Serialized description of the key combinations
* that make up the sequence
* @property {Object.<KeyName, Boolean>} keyDictionary - Dictionary of key names involved
* in the last key combination of the sequence
* @property {KeyEventType} keyEventType - Record index for key event that
* the matcher should match on
* @property {number} size - Number of keys involved in the final key combination
* @property {EventMatchDictionary} events - Dictionary of EventMatches
*/
/**
* A dictionary mapping key event types to event matches
* @typedef {Object.<KeyEventType, EventMatch>} EventMatchDictionary
*/
/**
* Object containing information to call a handler if an event type matches a
* key event
* @typedef {Object} EventMatch
* @property {ActionName} actionName - Name of the action
* @property {Function} handler - Handler to call if event type matches
*/
/**
* Matches a KeyCombination to a list of pre-registered ActionConfiguration and their
* corresponding handler functions
* @class
*/
var KeyCombinationMatcher =
/*#__PURE__*/
function () {
/**
* Returns a new instance of KeyCombinationMatcher
* @returns {KeyCombinationMatcher}
*/
function KeyCombinationMatcher() {
_classCallCheck(this, KeyCombinationMatcher);
this._actionConfigs = {};
this._order = null;
}
/**
* Adds a new ActionConfiguration and handler to those that can be used to match a
* KeyCombination
* @param {ActionConfiguration} actionConfig
* @param {Function} handler Function to call if match is selected
* @returns {void}
*/
_createClass(KeyCombinationMatcher, [{
key: "addMatch",
value: function addMatch(actionConfig, handler) {
if (this._includesMatcherForCombination(actionConfig.id)) {
var keyEventType = actionConfig.keyEventType,
actionName = actionConfig.actionName,
id = actionConfig.id;
this._addHandlerToActionConfig(id, {
keyEventType: keyEventType,
actionName: actionName,
handler: handler
});
} else {
this._addNewActionConfig(actionConfig, handler);
}
}
/**
* Finds a MatchingActionConfig for a KeyCombination, ReactKeyName and
* KeyEventType
* @param {KeyCombination} keyCombination Record of key combinations
* to use in the match
* @param {ReactKeyName} keyName Name of the key to use in the match
* @param {KeyEventType} keyEventType The type of key event to use in the match
* @returns {MatchingActionConfig|null} A MatchingActionOptions that matches the
* KeyCombination, ReactKeyName and KeyEventType
*/
}, {
key: "findMatch",
value: function findMatch(keyCombination, keyName, keyEventType) {
if (!this._order) {
this._setOrder();
}
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = this._order[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var combinationId = _step.value;
var actionOptions = this._actionConfigs[combinationId];
if (this._matchesActionConfig(keyCombination, keyName, keyEventType, actionOptions)) {
return actionOptions;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
return null;
}
/********************************************************************************
* Presentation
********************************************************************************/
/**
* A plain JavaScript representation of the KeyCombinationMatcher, useful for
* serialization or debugging
* @returns {Object} Serialized representation of the key combination matcher
*/
}, {
key: "toJSON",
value: function toJSON() {
return {
actionConfigs: this._actionConfigs,
order: this._order
};
}
/********************************************************************************
* Private methods
********************************************************************************/
}, {
key: "_matchesActionConfig",
value: function _matchesActionConfig(keyCombination, keyName, keyEventType, actionOptions) {
if (!canBeMatched(keyCombination, actionOptions)) {
return false;
}
var combinationHasHandlerForEventType = actionOptions.events[keyEventType];
if (!combinationHasHandlerForEventType) {
/**
* If the combination does not have any actions bound to the key event we are
* currently processing, we skip checking if it matches the current keys being
* pressed.
*/
return false;
}
var keyCompletesCombination = false;
var combinationMatchesKeysPressed = Object.keys(actionOptions.keyDictionary).every(function (candidateKeyName) {
if (keyCombination.isEventTriggered(candidateKeyName, keyEventType)) {
if (keyName && keyName === keyCombination.getNormalizedKeyName(candidateKeyName)) {
keyCompletesCombination = !keyCombination.wasEventPreviouslyTriggered(candidateKeyName, keyEventType);
}
return true;
} else {
return false;
}
});
return combinationMatchesKeysPressed && keyCompletesCombination;
}
}, {
key: "_setOrder",
value: function _setOrder() {
/**
* The first time the component that is currently handling the key event has
* its handlers searched for a match, order the combinations based on their
* size so that they may be applied in the correct priority order
*/
var combinationsPartitionedBySize = Object.values(this._actionConfigs).reduce(function (memo, _ref) {
var id = _ref.id,
size = _ref.size;
if (!memo[size]) {
memo[size] = [];
}
memo[size].push(id);
return memo;
}, {});
this._order = Object.keys(combinationsPartitionedBySize).sort(function (a, b) {
return b - a;
}).reduce(function (memo, key) {
return memo.concat(combinationsPartitionedBySize[key]);
}, []);
}
}, {
key: "_addNewActionConfig",
value: function _addNewActionConfig(combinationSchema, handler) {
var prefix = combinationSchema.prefix,
sequenceLength = combinationSchema.sequenceLength,
id = combinationSchema.id,
keyDictionary = combinationSchema.keyDictionary,
size = combinationSchema.size,
keyEventType = combinationSchema.keyEventType,
actionName = combinationSchema.actionName;
this._setCombinationMatcher(id, {
prefix: prefix,
sequenceLength: sequenceLength,
id: id,
keyDictionary: keyDictionary,
size: size,
events: {}
});
this._addHandlerToActionConfig(id, {
keyEventType: keyEventType,
actionName: actionName,
handler: handler
});
}
}, {
key: "_addHandlerToActionConfig",
value: function _addHandlerToActionConfig(id, _ref2) {
var keyEventType = _ref2.keyEventType,
actionName = _ref2.actionName,
handler = _ref2.handler;
var combination = this._getCombinationMatcher(id);
this._setCombinationMatcher(id, _objectSpread({}, combination, {
events: _objectSpread({}, combination.events, _defineProperty({}, keyEventType, {
actionName: actionName,
handler: handler
}))
}));
}
}, {
key: "_setCombinationMatcher",
value: function _setCombinationMatcher(id, combinationMatcher) {
this._actionConfigs[id] = combinationMatcher;
}
}, {
key: "_getCombinationMatcher",
value: function _getCombinationMatcher(id) {
return this._actionConfigs[id];
}
}, {
key: "_includesMatcherForCombination",
value: function _includesMatcherForCombination(id) {
return !!this._getCombinationMatcher(id);
}
}]);
return KeyCombinationMatcher;
}();
function canBeMatched(keyCombination, combinationMatcher) {
var combinationKeysNo = size(combinationMatcher.keyDictionary);
if (Configuration.option('allowCombinationSubmatches') || keyUpIsBeingHidden(keyCombination)) {
return keyCombination.getNumberOfKeys() >= combinationKeysNo;
} else {
/**
* If sub-matches are not allow, the number of keys in the key state and the
* number of keys in the combination we are attempting to match, must be
* exactly the same
*/
return keyCombination.getNumberOfKeys() === combinationKeysNo;
}
}
function keyUpIsBeingHidden(keyCombination) {
if (keyCombination.isKeyStillPressed('Meta')) {
return keyCombination.some(function (keyName) {
return keyupIsHiddenByCmd(keyName);
});
}
return false;
}
export default KeyCombinationMatcher;