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)),
|
ctrl_c: emitWithSelectedNodes((ids) => emit('copy:nodes', ids)),
|
||||||
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
|
enter: emitWithLastSelectedNode((id) => onSetNodeActive(id)),
|
||||||
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
ctrl_a: () => addSelectedNodes(graphNodes.value),
|
||||||
'shift_+|+|=': async () => await onZoomIn(),
|
// Support both key and code for zooming in and out
|
||||||
'shift+_|-|_': async () => await onZoomOut(),
|
'shift_+|+|=|shift_Equal|Equal': async () => await onZoomIn(),
|
||||||
|
'shift+_|-|_|shift_Minus|Minus': async () => await onZoomOut(),
|
||||||
0: async () => await onResetZoom(),
|
0: async () => await onResetZoom(),
|
||||||
1: async () => await onFitView(),
|
1: async () => await onFitView(),
|
||||||
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
|
ArrowUp: emitWithLastSelectedNode(selectUpperSiblingNode),
|
||||||
|
|
|
@ -136,4 +136,31 @@ describe('useKeybindings', () => {
|
||||||
document.dispatchEvent(eventB);
|
document.dispatchEvent(eventB);
|
||||||
expect(handler).toHaveBeenCalledTimes(2);
|
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>;
|
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 = (
|
export const useKeybindings = (
|
||||||
keymap: Ref<KeyMap>,
|
keymap: Ref<KeyMap>,
|
||||||
options?: {
|
options?: {
|
||||||
|
@ -29,12 +43,10 @@ export const useKeybindings = (
|
||||||
|
|
||||||
const normalizedKeymap = computed(() =>
|
const normalizedKeymap = computed(() =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(keymap.value)
|
Object.entries(keymap.value).flatMap(([shortcut, handler]) => {
|
||||||
.map(([shortcut, handler]) => {
|
const shortcuts = shortcut.split('|');
|
||||||
const shortcuts = shortcut.split('|');
|
return shortcuts.map((s) => [normalizeShortcutString(s), handler]);
|
||||||
return shortcuts.map((s) => [normalizeShortcutString(s), handler]);
|
}),
|
||||||
})
|
|
||||||
.flat(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -62,10 +74,36 @@ export const useKeybindings = (
|
||||||
return shortcutPartsToString(shortcut.split(new RegExp(`[${splitCharsRegEx}]`)));
|
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) {
|
function toShortcutString(event: KeyboardEvent) {
|
||||||
const { shiftKey, altKey } = event;
|
const { shiftKey, altKey } = event;
|
||||||
const ctrlKey = isCtrlKeyPressed(event);
|
const ctrlKey = isCtrlKeyPressed(event);
|
||||||
const keys = [event.key];
|
const keys = [event.key];
|
||||||
|
const codes = [keyboardEventCodeToKey(event.code)];
|
||||||
const modifiers: string[] = [];
|
const modifiers: string[] = [];
|
||||||
|
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
|
@ -80,15 +118,22 @@ export const useKeybindings = (
|
||||||
modifiers.push('alt');
|
modifiers.push('alt');
|
||||||
}
|
}
|
||||||
|
|
||||||
return shortcutPartsToString([...modifiers, ...keys]);
|
return {
|
||||||
|
byKey: shortcutPartsToString([...modifiers, ...keys]),
|
||||||
|
byCode: shortcutPartsToString([...modifiers, ...codes]),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(event: KeyboardEvent) {
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
if (ignoreKeyPresses.value || isDisabled.value) return;
|
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) {
|
if (handler) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
Loading…
Reference in a new issue