mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-30 22:02:03 -08:00
fix(core): Fix keyboard shortcuts for non-ansi layouts (#12672)
This commit is contained in:
parent
395f2ad0dc
commit
4c8193fedc
|
@ -218,8 +218,9 @@ const keyMap = computed(() => ({
|
|||
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
|
||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||
'shift_+|+|=': async () => await onZoomIn(),
|
||||
'shift+_|-|_': async () => await onZoomOut(),
|
||||
// Support both key and code for zooming in and out
|
||||
'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(),
|
||||
'shift+_|-|_|shift_Minus|Minus': async () => await onZoomOut(),
|
||||
0: async () => await onResetZoom(),
|
||||
1: async () => await onFitView(),
|
||||
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
|
||||
|
|
|
@ -136,4 +136,31 @@ describe('useKeybindings', () => {
|
|||
document.dispatchEvent(eventB);
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should prefer the 'key' over 'code' for dvorak to work correctly", () => {
|
||||
const cHandler = vi.fn();
|
||||
const iHandler = vi.fn();
|
||||
const keymap = ref({
|
||||
'ctrl+c': cHandler,
|
||||
'ctrl+i': iHandler,
|
||||
});
|
||||
|
||||
useKeybindings(keymap);
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'c', code: 'KeyI', ctrlKey: true });
|
||||
document.dispatchEvent(event);
|
||||
expect(cHandler).toHaveBeenCalled();
|
||||
expect(iHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fallback to 'code' for non-ansi layouts", () => {
|
||||
const handler = vi.fn();
|
||||
const keymap = ref({ 'ctrl+c': handler });
|
||||
|
||||
useKeybindings(keymap);
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'ב', code: 'KeyC', ctrlKey: true });
|
||||
document.dispatchEvent(event);
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,20 @@ import { computed, unref } from 'vue';
|
|||
|
||||
type KeyMap = Record<string, (event: KeyboardEvent) => void>;
|
||||
|
||||
/**
|
||||
* Binds a `keydown` event to `document` and calls the approriate
|
||||
* handlers based on the given `keymap`. The keymap is a map from
|
||||
* shortcut strings to handlers. The shortcut strings can contain
|
||||
* multiple shortcuts separated by `|`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* 'ctrl+a': () => console.log('ctrl+a'),
|
||||
* 'ctrl+b|ctrl+c': () => console.log('ctrl+b or ctrl+c'),
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const useKeybindings = (
|
||||
keymap: Ref<KeyMap>,
|
||||
options?: {
|
||||
|
@ -29,12 +43,10 @@ export const useKeybindings = (
|
|||
|
||||
const normalizedKeymap = computed(() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(keymap.value)
|
||||
.map(([shortcut, handler]) => {
|
||||
const shortcuts = shortcut.split('|');
|
||||
return shortcuts.map((s) => [normalizeShortcutString(s), handler]);
|
||||
})
|
||||
.flat(),
|
||||
Object.entries(keymap.value).flatMap(([shortcut, handler]) => {
|
||||
const shortcuts = shortcut.split('|');
|
||||
return shortcuts.map((s) => [normalizeShortcutString(s), handler]);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -62,10 +74,36 @@ export const useKeybindings = (
|
|||
return shortcutPartsToString(shortcut.split(new RegExp(`[${splitCharsRegEx}]`)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a keyboard event code to a key string.
|
||||
*
|
||||
* @example
|
||||
* keyboardEventCodeToKey('Digit0') -> '0'
|
||||
* keyboardEventCodeToKey('KeyA') -> 'a'
|
||||
*/
|
||||
function keyboardEventCodeToKey(code: string) {
|
||||
if (code.startsWith('Digit')) {
|
||||
return code.replace('Digit', '').toLowerCase();
|
||||
} else if (code.startsWith('Key')) {
|
||||
return code.replace('Key', '').toLowerCase();
|
||||
}
|
||||
|
||||
return code.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a keyboard event to a shortcut string for both
|
||||
* `key` and `code`.
|
||||
*
|
||||
* @example
|
||||
* keyboardEventToShortcutString({ key: 'a', code: 'KeyA', ctrlKey: true })
|
||||
* // --> { byKey: 'ctrl+a', byCode: 'ctrl+a' }
|
||||
*/
|
||||
function toShortcutString(event: KeyboardEvent) {
|
||||
const { shiftKey, altKey } = event;
|
||||
const ctrlKey = isCtrlKeyPressed(event);
|
||||
const keys = [event.key];
|
||||
const codes = [keyboardEventCodeToKey(event.code)];
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (shiftKey) {
|
||||
|
@ -80,15 +118,22 @@ export const useKeybindings = (
|
|||
modifiers.push('alt');
|
||||
}
|
||||
|
||||
return shortcutPartsToString([...modifiers, ...keys]);
|
||||
return {
|
||||
byKey: shortcutPartsToString([...modifiers, ...keys]),
|
||||
byCode: shortcutPartsToString([...modifiers, ...codes]),
|
||||
};
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (ignoreKeyPresses.value || isDisabled.value) return;
|
||||
|
||||
const shortcutString = toShortcutString(event);
|
||||
const { byKey, byCode } = toShortcutString(event);
|
||||
|
||||
const handler = normalizedKeymap.value[shortcutString];
|
||||
// Prefer `byKey` over `byCode` so that:
|
||||
// - ANSI layouts work correctly
|
||||
// - Dvorak works correctly
|
||||
// - Non-ansi layouts work correctly
|
||||
const handler = normalizedKeymap.value[byKey] ?? normalizedKeymap.value[byCode];
|
||||
|
||||
if (handler) {
|
||||
event.preventDefault();
|
||||
|
|
Loading…
Reference in a new issue