/* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import './HotKeysPreferences.styl'; import { allowedKeys, disallowedCombinations, specialKeys, } from './hotKeysConfig.js'; import isEqual from 'lodash.isequal'; const getKeysPressedArray = keyDownEvent => { const keysPressedArray = []; const { ctrlKey, altKey, shiftKey } = keyDownEvent; if (ctrlKey && !altKey) { keysPressedArray.push('ctrl'); } if (shiftKey && !altKey) { keysPressedArray.push('shift'); } if (altKey && !ctrlKey) { keysPressedArray.push('alt'); } return keysPressedArray; }; const findConflictingCommand = ( originalHotKeys, currentCommandName, currentHotKeys ) => { let firstConflictingCommand = undefined; for (const commandName in originalHotKeys) { const toolHotKeys = originalHotKeys[commandName].keys; if ( isEqual(toolHotKeys, currentHotKeys) && commandName !== currentCommandName ) { firstConflictingCommand = originalHotKeys[commandName]; break; } } return firstConflictingCommand; }; /** * Splits given keysObj into arrays. Each array item will be a representation of column * @param {obj} keysObj objects to be splitted into columns * @param {number} columnSize How many rows per column */ const getHotKeysArrayColumns = (keysObj = {}, columnSize) => { if (isNaN(columnSize)) { return keysObj; } const keys = Object.keys(keysObj); const keysValues = Object.values(keysObj); const keysLength = keys.length; // Columns from left should be bigger; let currentColumn = 0; const dividedKeys = []; for ( let it = 0; it < keysLength; it++, it % columnSize === 0 ? currentColumn++ : currentColumn ) { if (!dividedKeys[currentColumn]) { dividedKeys[currentColumn] = []; } dividedKeys[currentColumn][keys[it]] = keysValues[it]; } return dividedKeys; }; const NO_FIELD_ERROR_MESSAGE = undefined; const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+'); const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+'); const inputValidators = ( commandName, inputValue, pressedKeys, lastPressedKey, originalHotKeys ) => { let hasError = false; let errorMessage = NO_FIELD_ERROR_MESSAGE; const modifierValidator = ({ lastPressedKey }) => { // Check if it has a valid modifier const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey); if (isModifier) { hasError = true; errorMessage = "It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"; return { hasError, errorMessage, }; } }; const emptyValidator = ({ inputValue }) => { if (!inputValue) { hasError = true; errorMessage = "Field can't be empty."; return { hasError, errorMessage, }; } }; const conflictingValidator = ({ commandName, pressedKeys, originalHotKeys, }) => { const conflictingCommand = findConflictingCommand( originalHotKeys, commandName, pressedKeys ); if (conflictingCommand) { hasError = true; errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`; return { hasError, errorMessage, }; } }; const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => { const modifierCommand = formatPressedKeys( pressedKeys.slice(0, pressedKeys.length - 1) ); const disallowedCombination = disallowedCombinations[modifierCommand]; const hasDisallowedCombinations = disallowedCombination ? disallowedCombination.includes(lastPressedKey) : false; if (hasDisallowedCombinations) { hasError = true; errorMessage = `"${inputValue}" shortcut combination is not allowed`; return { hasError, errorMessage, }; } }; const validators = [ emptyValidator, modifierValidator, conflictingValidator, disallowedValidator, ]; for (const validator of validators) { const validation = validator({ commandName, inputValue, pressedKeys, lastPressedKey, originalHotKeys, }); if (validation && validation.hasError) { return validation; } } // validation has passed successfully return { hasError, errorMessage, }; }; /** * HotKeysPreferencesRow * Renders row for hotkey preference * It stores current state and whenever it changes, component messages parent of new value (through function callback) * @param {object} props component props * @param {string} props.commandName command name associated to given row * @param {string[]} props.hotkeys keys associated to given command * @param {object} props.originalHotKeys original hotkeys values * @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes * @param {function} props.onFailureChanged Callback Function in case any error on row */ function HotKeyPreferencesRow({ commandName, hotkeys, label, originalHotKeys, tabError, onSuccessChanged, onFailureChanged, }) { const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys)); const [error, setError] = useState(false); const [fieldErrorMessage, setFieldErrorMessage] = useState( NO_FIELD_ERROR_MESSAGE ); // reset error count if tab has no errors useEffect(() => { if (!tabError) { setError(false); setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE); setInputValue(formatPressedKeys(hotkeys)); } }, [tabError]); // update state values if props changes useEffect(() => { setInputValue(formatPressedKeys(hotkeys)); }, [hotkeys]); const updateInputText = (keyDownEvent, displayPressedKey = false) => { const pressedKeys = getKeysPressedArray(keyDownEvent); if (displayPressedKey) { const specialKeyName = specialKeys[keyDownEvent.which]; const keyName = specialKeyName || keyDownEvent.key || String.fromCharCode(keyDownEvent.keyCode); // ensure lowerCase pressedKeys.push(keyName.toLowerCase()); } setInputValue(formatPressedKeys(pressedKeys)); }; // validate input value const validateInput = () => { const pressedKeys = unFormatPressedKeys(inputValue); const lastPressedKey = pressedKeys[pressedKeys.length - 1]; const { hasError = false, errorMessage = NO_FIELD_ERROR_MESSAGE, } = inputValidators( commandName, inputValue, pressedKeys, lastPressedKey, originalHotKeys ); if (hasError) { setInputValue(''); } else { onSuccessChanged([inputValue]); } if (hasError !== error) { setError(hasError); } setFieldErrorMessage(errorMessage); }; useEffect(() => { onFailureChanged(error); }, [error]); const onInputKeyDown = event => { // Prevent ESC key from propagating and closing the modal if (event.key === 'Escape') { event.stopPropagation(); } updateInputText(event, allowedKeys.includes(event.keyCode)); event.preventDefault(); }; return (
| Function | Shortcut |
|---|