* Split Settings::Authentication from SiteConfig * Move specs * Sort fields * Update settings usages * Update recaptcha usages * Add data update script * Update spec * Rename SiteConfigParams concern * Fixes, new route, new controller * Controller and service refactoring * More controller and service updates * Spec updates * More spec fixes * Move file * Fix FeedbackMessagesController * Update admin/configs_spec * Fix remaining specs in admin/configs_spec * Fix configs API * Formatting * Clean up old service object * Various fixes * Update DUS * Add model argument to admin_config_label * Fix key name * Fix specs * Add distinct request caches for settings classes * Fix e2e tests * Fix remaining system spec * Make DUS idempotent * Move routes block * Cleanup * Switch to ActiveSupport::CurrentAttributes * Pinned rails-settings-cached * Update e2e test * Update lib/data_update_scripts/20210316091354_move_authentication_settings.rb Co-authored-by: rhymes <rhymes@hey.com> * Add guard to DUS * Temporarily re-add two SiteConfig fields * Fix config show view Co-authored-by: rhymes <rhymes@hey.com>
187 lines
5 KiB
JavaScript
187 lines
5 KiB
JavaScript
import { useState, useEffect } from 'preact/hooks';
|
|
import PropTypes from 'prop-types';
|
|
|
|
/**
|
|
* Checker that return true if element is a form element
|
|
*
|
|
* @param {node} element to be checked
|
|
*
|
|
* @returns {boolean} isFormField
|
|
*/
|
|
function isFormField(element) {
|
|
if (element instanceof HTMLElement === false) return false;
|
|
|
|
const name = element.nodeName.toLowerCase();
|
|
const type = (element.getAttribute('type') || '').toLowerCase();
|
|
return (
|
|
name === 'select' ||
|
|
name === 'textarea' ||
|
|
(name === 'input' &&
|
|
type !== 'submit' &&
|
|
type !== 'reset' &&
|
|
type !== 'checkbox' &&
|
|
type !== 'radio') ||
|
|
element.isContentEditable
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Function to handle converting key presses to callback functions
|
|
*
|
|
* @param {KeyboardEvent} e Keyboard event
|
|
* @param {String} keys special keys formatted in a string
|
|
* @param {Array} chain array of past keys
|
|
* @param {Object} shortcuts object containing callback functions
|
|
*
|
|
* @returns {Array} New chain
|
|
*/
|
|
const callShortcut = (e, keys, chain, shortcuts) => {
|
|
const shortcut =
|
|
chain && chain.length > 0
|
|
? shortcuts[`${chain.join('~')}~${e.code}`]
|
|
: shortcuts[`${keys}${e.code}`] ||
|
|
shortcuts[`${keys}${e.key.toLowerCase()}`];
|
|
|
|
// if a valid shortcut is found call it and reset the chain
|
|
if (shortcut) {
|
|
shortcut(e);
|
|
return [];
|
|
}
|
|
|
|
// if we have keys don't add to the chain
|
|
if (keys || e.key === 'Shift') {
|
|
return [];
|
|
}
|
|
|
|
return [...chain, e.code];
|
|
};
|
|
|
|
// Default options to be used if null
|
|
const defaultOptions = {
|
|
timeout: 0, // The default is zero as we want no delays between keystrokes by default.
|
|
};
|
|
|
|
/**
|
|
* hook that can be added to a component to listen
|
|
* for keyboard presses
|
|
*
|
|
* @example
|
|
* const shortcuts = {
|
|
* 'ctrl+alt+KeyG': (e) => {
|
|
* e.preventDefault();
|
|
* alert('Control Alt G has been pressed');
|
|
* },
|
|
* 'KeyG~KeyH': (e) => {
|
|
* e.preventDefault();
|
|
* alert('G has been pressed quickly followed by H');
|
|
* },
|
|
* '?': (e) => {
|
|
* setIsHelpVisible(true);
|
|
* }
|
|
* }
|
|
*
|
|
* useKeyboardShortcuts(shortcuts, someElementOrWindowObject, {timeout: 1500});
|
|
*
|
|
* @param {object} shortcuts List of keyboard shortcuts/event
|
|
* @param {EventTarget} [eventTarget=window] An event target.
|
|
* @param {object} [options = {}] An object for extra options
|
|
*
|
|
*/
|
|
export function useKeyboardShortcuts(
|
|
shortcuts,
|
|
eventTarget = window,
|
|
options = {},
|
|
) {
|
|
const [storedShortcuts] = useState(shortcuts);
|
|
const [keyChain, setKeyChain] = useState([]);
|
|
const [mergedOptions, setMergedOptions] = useState({
|
|
...defaultOptions,
|
|
...options,
|
|
});
|
|
|
|
// update mergedOptions if options prop changes
|
|
useEffect(() => {
|
|
const newOptions = {};
|
|
if (typeof options.timeout === 'number')
|
|
newOptions.timeout = options.timeout;
|
|
setMergedOptions({ ...defaultOptions, ...newOptions });
|
|
}, [options.timeout]);
|
|
|
|
// clear key chain after timeout is reached
|
|
useEffect(() => {
|
|
if (keyChain.length <= 0) return;
|
|
|
|
const timeout = window.setTimeout(() => {
|
|
clearTimeout(timeout);
|
|
setKeyChain([]);
|
|
}, mergedOptions.timeout);
|
|
|
|
return () => clearTimeout(timeout);
|
|
}, [keyChain.length, mergedOptions.timeout]);
|
|
|
|
// set up event listeners
|
|
useEffect(() => {
|
|
if (!storedShortcuts || Object.keys(storedShortcuts).length === 0) return;
|
|
|
|
const keyEvent = (e) => {
|
|
if (e.defaultPrevented) return;
|
|
|
|
// Get special keys
|
|
const keys = `${e.ctrlKey || e.metaKey ? 'ctrl+' : ''}${
|
|
e.altKey ? 'alt+' : ''
|
|
}${(e.ctrlKey || e.metaKey || e.altKey) && e.shiftKey ? 'shift+' : ''}`;
|
|
|
|
// If no special keys, except shift, are pressed and focus is inside a field return
|
|
if (e.target instanceof Node && isFormField(e.target) && !keys) return;
|
|
|
|
const newChain = callShortcut(e, keys, keyChain, storedShortcuts);
|
|
|
|
// update keychain with latest chain
|
|
setKeyChain(newChain);
|
|
};
|
|
|
|
eventTarget.addEventListener('keydown', keyEvent);
|
|
|
|
return () => eventTarget.removeEventListener('keydown', keyEvent);
|
|
}, [keyChain, storedShortcuts, eventTarget]);
|
|
}
|
|
|
|
/**
|
|
* A component that can be added to a component to listen
|
|
* for keyboard presses using the useKeyboardShortcuts hook
|
|
*
|
|
* @example
|
|
* const shortcuts = {
|
|
* 'ctrl+alt+KeyG': (e) => {
|
|
* e.preventDefault();
|
|
* alert('Control Alt G has been pressed')
|
|
* }
|
|
* }
|
|
*
|
|
* <KeyboardShortcuts shortcuts={shortcuts} />
|
|
* <KeyboardShortcuts shortcuts={shortcuts} eventTarget={ref.current} />
|
|
*
|
|
* @param {object} shortcuts List of keyboard shortcuts/event
|
|
* @param {EventTarget} [eventTarget=window] An event target.
|
|
* @param {object} [options = {}] An object for extra options
|
|
*
|
|
*/
|
|
export function KeyboardShortcuts({ shortcuts, eventTarget, options }) {
|
|
useKeyboardShortcuts(shortcuts, eventTarget, options);
|
|
|
|
return null;
|
|
}
|
|
|
|
KeyboardShortcuts.propTypes = {
|
|
shortcuts: PropTypes.object.isRequired,
|
|
options: PropTypes.shape({
|
|
timeout: PropTypes.number,
|
|
}),
|
|
eventTarget: PropTypes.instanceOf(Element),
|
|
};
|
|
|
|
KeyboardShortcuts.defaultProps = {
|
|
shortcuts: {},
|
|
options: {},
|
|
eventTarget: window,
|
|
};
|