feat(keyboard): enhance keyboard shortcut handling by synchronizing modifier keys on keydown and keyup events; improve shortcut matching logic for better compatibility and layout independence
This commit is contained in:
@@ -31,31 +31,45 @@ class KeyboardShortcuts {
|
|||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
const key = e.key.toLowerCase();
|
const key = e.key.toLowerCase();
|
||||||
this.activeKeys.add(key);
|
|
||||||
|
// Update active keys from modifiers to ensure they are in sync
|
||||||
|
if (e.ctrlKey) this.activeKeys.add("control");
|
||||||
|
else if (!this.isRecording) this.activeKeys.delete("control");
|
||||||
|
|
||||||
|
if (e.altKey) this.activeKeys.add("alt");
|
||||||
|
else if (!this.isRecording) this.activeKeys.delete("alt");
|
||||||
|
|
||||||
|
if (e.shiftKey) this.activeKeys.add("shift");
|
||||||
|
else if (!this.isRecording) this.activeKeys.delete("shift");
|
||||||
|
|
||||||
|
if (e.metaKey) this.activeKeys.add("meta");
|
||||||
|
else if (!this.isRecording) this.activeKeys.delete("meta");
|
||||||
|
|
||||||
|
if (!["control", "alt", "shift", "meta"].includes(key)) {
|
||||||
|
this.activeKeys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isRecording) {
|
if (this.isRecording) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.onRecordCallback) {
|
if (this.onRecordCallback) {
|
||||||
const keys = Array.from(this.activeKeys);
|
this.onRecordCallback(Array.from(this.activeKeys));
|
||||||
this.onRecordCallback(keys);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for matches
|
// Check for matches
|
||||||
for (const shortcut of this.shortcuts) {
|
for (const shortcut of this.shortcuts) {
|
||||||
if (this.matches(shortcut.keys)) {
|
if (this.matches(shortcut.keys, e)) {
|
||||||
// Don't trigger if user is typing in an input, unless it's a global shortcut
|
// Don't trigger if user is typing in an input, unless it's a global shortcut
|
||||||
if (
|
const isInput = ["INPUT", "TEXTAREA"].includes(document.activeElement.tagName);
|
||||||
["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) &&
|
const hasModifier = shortcut.keys.some((k) => ["control", "alt", "meta"].includes(k));
|
||||||
!shortcut.keys.includes("control") &&
|
|
||||||
!shortcut.keys.includes("alt") &&
|
if (isInput && !hasModifier) {
|
||||||
!shortcut.keys.includes("meta")
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
this.executeAction(shortcut.action);
|
this.executeAction(shortcut.action);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -65,23 +79,41 @@ class KeyboardShortcuts {
|
|||||||
handleKeyUp(e) {
|
handleKeyUp(e) {
|
||||||
const key = e.key.toLowerCase();
|
const key = e.key.toLowerCase();
|
||||||
this.activeKeys.delete(key);
|
this.activeKeys.delete(key);
|
||||||
|
|
||||||
|
// Sync modifiers on keyup
|
||||||
|
if (!e.ctrlKey) this.activeKeys.delete("control");
|
||||||
|
if (!e.altKey) this.activeKeys.delete("alt");
|
||||||
|
if (!e.shiftKey) this.activeKeys.delete("shift");
|
||||||
|
if (!e.metaKey) this.activeKeys.delete("meta");
|
||||||
}
|
}
|
||||||
|
|
||||||
matches(shortcutKeys) {
|
matches(shortcutKeys, e) {
|
||||||
if (shortcutKeys.length === 0) return false;
|
if (!shortcutKeys || shortcutKeys.length === 0) return false;
|
||||||
|
|
||||||
// Map common keys
|
// Check modifiers using event properties (most reliable in browsers)
|
||||||
const mappedActiveKeys = Array.from(this.activeKeys).map((k) => {
|
const hasControl = shortcutKeys.includes("control");
|
||||||
if (k === "control") return "control";
|
const hasAlt = shortcutKeys.includes("alt");
|
||||||
if (k === "alt") return "alt";
|
const hasShift = shortcutKeys.includes("shift");
|
||||||
if (k === "shift") return "shift";
|
|
||||||
if (k === "meta") return "meta";
|
|
||||||
return k;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shortcutKeys.length !== mappedActiveKeys.length) return false;
|
// Allow Cmd (Meta) to act as Control on Mac for improved compatibility
|
||||||
|
const ctrlMatch = hasControl ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey;
|
||||||
|
const altMatch = hasAlt ? e.altKey : !e.altKey;
|
||||||
|
const shiftMatch = hasShift ? e.shiftKey : !e.shiftKey;
|
||||||
|
|
||||||
return shortcutKeys.every((k) => mappedActiveKeys.includes(k));
|
if (!ctrlMatch || !altMatch || !shiftMatch) return false;
|
||||||
|
|
||||||
|
// Find the non-modifier key in the shortcut
|
||||||
|
const mainKey = shortcutKeys.find((k) => !["control", "alt", "shift", "meta"].includes(k));
|
||||||
|
if (!mainKey) return true; // Modifier-only shortcut (rare but possible)
|
||||||
|
|
||||||
|
const pressedKey = e.key.toLowerCase();
|
||||||
|
if (pressedKey === mainKey.toLowerCase()) return true;
|
||||||
|
|
||||||
|
// Layout independence: check e.code as well (handles Alt+key layout changes)
|
||||||
|
if (e.code === `Digit${mainKey}`) return true;
|
||||||
|
if (e.code === `Key${mainKey.toUpperCase()}`) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction(action) {
|
executeAction(action) {
|
||||||
|
|||||||
Reference in New Issue
Block a user