You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hestiacp/web/js/src/shortcuts.js

463 lines
12 KiB

import {
moveFocusLeft,
moveFocusRight,
moveFocusDown,
moveFocusUp,
enterFocused,
executeShortcut,
} from './navigation';
import { createConfirmationDialog } from './helpers';
/**
* Shortcuts
* @typedef {{ key: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} KeyCombination
* @typedef {{ code: string, altKey?: boolean, ctrlKey?: boolean, metaKey?: boolean, shiftKey?: boolean }} CodeCombination
* @typedef {{ combination: KeyCombination, event: 'keydown' | 'keyup', callback: (evt: KeyboardEvent) => void, target: EventTarget }} RegisteredShortcut
* @typedef {{ type?: 'keydown' | 'keyup', propagate?: boolean, disabledInInput?: boolean, target?: EventTarget }} ShortcutOptions
*/
export default function handleShortcuts() {
Alpine.store('shortcuts', {
/**
* @type RegisteredShortcut[]
*/
registeredShortcuts: [],
/**
* @param {KeyCombination | CodeCombination} combination
* A combination using a `code` representing a physical key on the keyboard or a `key`
* representing the character generated by pressing the key. Modifiers can be added using the
* `altKey`, `ctrlKey`, `metaKey` or `shiftKey` parameters.
* @param {(evt: KeyboardEvent) => void} callback
* The callback function that will be called when the correct combination is pressed.
* @param {ShortcutOptions?} options
* An object of options, containing the event `type`, whether it will `propagate`, the `target`
* element, and whether it's `disabledInInput`.
* @returns {this} The `Shortcuts` object.
*/
register(combination, callback, options) {
/** @type ShortcutOptions */
const defaultOptions = {
type: 'keydown',
propagate: false,
disabledInInput: false,
target: document,
};
options = { ...defaultOptions, ...options };
/**
* @param {KeyboardEvent} evt
*/
const func = (evt) => {
if (options.disabledInInput) {
// Don't enable shortcut keys in input, textarea, select fields
const element = evt.target.nodeType === 3 ? evt.target.parentNode : evt.target;
if (['input', 'textarea', 'selectbox'].includes(element.tagName.toLowerCase())) {
return;
}
}
const validations = [
combination.code
? combination.code == evt.code
: combination.key.toLowerCase() == evt.key.toLowerCase(),
(combination.altKey && evt.altKey) || (!combination.altKey && !evt.altKey),
(combination.ctrlKey && evt.ctrlKey) || (!combination.ctrlKey && !evt.ctrlKey),
(combination.metaKey && evt.metaKey) || (!combination.metaKey && !evt.metaKey),
(combination.shiftKey && evt.shiftKey) || (!combination.shiftKey && !evt.shiftKey),
];
const valid = validations.filter(Boolean);
if (valid.length === validations.length) {
callback(evt);
if (!options.propagate) {
evt.stopPropagation();
evt.preventDefault();
}
}
};
this.registeredShortcuts.push({
combination,
callback: func,
target: options.target,
event: options.type,
});
options.target.addEventListener(options.type, func);
return this;
},
/**
* @param {KeyCombination | CodeCombination} combination
* A combination using a `code` representing a physical key on the keyboard or a `key`
* representing the character generated by pressing the key. Modifiers can be added using the
* `altKey`, `ctrlKey`, `metaKey` or `shiftKey` parameters.
* @returns {this} The `Shortcuts` object.
*/
unregister(combination) {
const shortcut = this.registeredShortcuts.find(
(shortcut) => JSON.stringify(shortcut.combination) == JSON.stringify(combination)
);
if (!shortcut) {
return;
}
this.registeredShortcuts = this.registeredShortcuts.filter(
(shortcut) => JSON.stringify(shortcut.combination) != JSON.stringify(combination)
);
shortcut.target.removeEventListener(shortcut.event, shortcut.callback, false);
return this;
},
});
Alpine.store('shortcuts')
.register(
{ key: 'A' },
(_evt) => {
const createButton = document.querySelector('a.js-button-create');
if (!createButton) {
return;
}
location.href = createButton.href;
},
{ disabledInInput: true }
)
.register(
{ key: 'A', ctrlKey: true, shiftKey: true },
(_evt) => {
const checked = document.querySelector('.js-unit-checkbox:eq(0)').checked;
document
.querySelectorAll('.js-unit')
.forEach((el) => el.classList.toggle('selected'), !checked);
document.querySelectorAll('.js-unit-checkbox').forEach((el) => (el.checked = !checked));
},
{ disabledInInput: true }
)
.register({ code: 'Enter', ctrlKey: true }, (_evt) => {
document.querySelector('#main-form').submit();
})
.register({ code: 'Backspace', ctrlKey: true }, (_evt) => {
const redirect = document.querySelector('a.js-button-back').href;
if (!redirect) {
return;
}
if (Alpine.store('form').dirty && redirect) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: redirect,
});
} else if (redirect) {
location.href = redirect;
}
})
.register(
{ key: 'F' },
(_evt) => {
const searchBox = document.querySelector('.js-search-input');
if (searchBox) {
searchBox.focus();
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit1' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(1) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit2' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(2) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit3' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(3) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit4' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(4) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit5' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(5) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit6' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(6) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Digit7' },
(_evt) => {
const target = document.querySelector('.main-menu .main-menu-item:nth-of-type(7) a');
if (!target) {
return;
}
if (Alpine.store('form').dirty) {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: target.href,
});
} else {
location.href = target.href;
}
},
{ disabledInInput: true }
)
.register(
{ key: 'H' },
(_evt) => {
const shortcutsDialog = document.querySelector('.shortcuts');
if (shortcutsDialog.open) {
shortcutsDialog.close();
} else {
shortcutsDialog.showModal();
}
},
{ disabledInInput: true }
)
.register({ code: 'Escape' }, (_evt) => {
const openDialog = document.querySelector('dialog[open]');
if (openDialog) {
openDialog.close();
}
document.querySelectorAll('input, checkbox, textarea, select').forEach((el) => el.blur());
})
.register(
{ code: 'ArrowLeft' },
(_evt) => {
moveFocusLeft();
},
{ disabledInInput: true }
)
.register(
{ code: 'ArrowRight' },
(_evt) => {
moveFocusRight();
},
{ disabledInInput: true }
)
.register(
{ code: 'ArrowDown' },
(_evt) => {
moveFocusDown();
},
{ disabledInInput: true }
)
.register(
{ code: 'ArrowUp' },
(_evt) => {
moveFocusUp();
},
{ disabledInInput: true }
)
.register(
{ key: 'L' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-l');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ key: 'S' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-s');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ key: 'W' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-w');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ key: 'D' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-d');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ key: 'R' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-r');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ key: 'N' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-n');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ key: 'U' },
(_evt) => {
const element = document.querySelector('.js-units-container .js-unit.focus .shortcut-u');
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Delete' },
(_evt) => {
const element = document.querySelector(
'.js-units-container .js-unit.focus .shortcut-delete'
);
if (element) {
executeShortcut(element);
}
},
{ disabledInInput: true }
)
.register(
{ code: 'Enter' },
(evt) => {
if (evt.target.tagName === 'INPUT' && evt.target.form.id === 'main-form') {
evt.target.form.submit();
}
if (Alpine.store('form').dirty) {
if (document.querySelector('dialog[open]')) {
const dialog = document.querySelector('dialog[open]');
dialog.querySelector('button[type="submit"]').click();
} else {
createConfirmationDialog({
message: Alpine.store('globals').CONFIRM_LEAVE_PAGE,
targetUrl: document.querySelector('.main-menu-item.focus a').href,
});
}
} else if (document.querySelector('dialog[open]')) {
const dialog = document.querySelector('dialog[open]');
dialog.querySelector('button[type="submit"]').click();
} else {
const element = document.querySelector(
'.js-units-container .js-unit.focus .shortcut-enter'
);
if (element) {
executeShortcut(element);
} else {
enterFocused();
}
}
},
{ propagate: true }
);
}