Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/panel/controllers/panelHotkeysController.js
T

245 lines
8.2 KiB
JavaScript

import {
getHotkeysState,
isHotkeysSuspended,
setHotkeysScope,
setHotkeysSuspendedFlag,
} from "./hotkeysState.js";
import { getActiveGridContainer } from "../panelRuntimeRefs.js";
export function setHotkeysSuspended(suspended) {
setHotkeysSuspendedFlag(suspended);
}
export function createPanelHotkeysController({
onTriggerScan,
getScanContext,
onToggleDetails,
onToggleFloatingViewer,
onFocusSearch,
onClearSearch,
allowListKeys,
} = {}) {
let panelFocused = false;
let panelHovered = false;
let boundEl = null;
const allowed =
allowListKeys instanceof Set
? allowListKeys
: new Set([
" ",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"PageUp",
"PageDown",
"Home",
"End",
"Enter",
"Escape",
"Tab",
]);
const isPanelActive = () => panelFocused || panelHovered;
const isPanelHotkeyContext = (event) => {
if (isPanelActive()) return true;
try {
const target = event?.target;
if (boundEl && target && typeof boundEl.contains === "function") {
if (boundEl === target || boundEl.contains(target)) return true;
}
} catch (e) {
console.debug?.(e);
}
try {
return Boolean(boundEl?.isConnected) && document.activeElement === document.body;
} catch (e) {
console.debug?.(e);
return false;
}
};
const isGridHotkeyContext = (event) => {
try {
const targetGrid = event?.target?.closest?.(".mjr-grid");
if (targetGrid) return true;
} catch (e) {
console.debug?.(e);
}
try {
const activeGrid = getActiveGridContainer();
if (!activeGrid) return false;
if (document.activeElement === document.body) return true;
return activeGrid.contains?.(document.activeElement) || false;
} catch (e) {
console.debug?.(e);
return false;
}
};
const handlers = {
focusin: () => {
panelFocused = true;
},
focusout: () => {
panelFocused = false;
},
mouseenter: () => {
panelHovered = true;
},
mouseleave: () => {
panelHovered = false;
},
keydown: (event) => {
// Check if hotkeys are suspended (e.g. when dialogs/popovers are open)
if (isHotkeysSuspended()) return;
// Check if viewer has hotkey priority
if (getHotkeysState().scope === "viewer") return;
// Check if panel is active before processing hotkeys
// This ensures hotkeys only work when the panel is hovered or focused
if (!isPanelHotkeyContext(event)) return;
const lower = event.key?.toLowerCase?.() || "";
const isTypingTarget =
event.target?.isContentEditable ||
event.target?.closest?.("input, textarea, select, [contenteditable='true']");
// Ctrl/Cmd+S triggers an index scan for the current scope, without letting the
// surrounding ComfyUI UI capture the shortcut (e.g. workflow save).
if (lower === "s" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
try {
const ctx = typeof getScanContext === "function" ? getScanContext() : null;
onTriggerScan?.(ctx);
} catch (e) {
console.debug?.(e);
}
return;
}
// Ctrl/Cmd+F or Ctrl/Cmd+K focuses the search box (matching docs/SHORTCUTS & HOTKEYS guides).
if ((lower === "f" || lower === "k") && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
try {
onFocusSearch?.();
} catch (e) {
console.debug?.(e);
}
return;
}
// Ctrl/Cmd+H clears the search box.
if (lower === "h" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
try {
onClearSearch?.();
} catch (e) {
console.debug?.(e);
}
return;
}
// D toggles the Details sidebar for the current selection.
if (lower === "d" && !event.ctrlKey && !event.metaKey && !event.altKey) {
if (isTypingTarget) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
try {
onToggleDetails?.();
} catch (e) {
console.debug?.(e);
}
return;
}
// V and Ctrl/Cmd+V toggle the floating viewer for the current panel selection.
// Keep Alt+V and Ctrl/Cmd+Shift+V untouched for other app/browser behaviors.
if (
lower === "v" &&
!event.altKey &&
!(event.shiftKey && (event.ctrlKey || event.metaKey))
) {
if (isTypingTarget) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
try {
onToggleFloatingViewer?.();
} catch (e) {
console.debug?.(e);
}
return;
}
// For navigation keys, prevent ComfyUI global handlers from stealing focus/scroll.
// Only intercept if panel is actually active.
// Never intercept while typing in inputs (e.g. tags editor in the sidebar).
if (allowed.has(event.key) && isPanelHotkeyContext(event)) {
if (isTypingTarget) return;
if (isGridHotkeyContext(event)) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
}
},
};
const bind = (el) => {
if (!el || boundEl === el) return;
if (boundEl) dispose();
boundEl = el;
try {
boundEl.addEventListener("focusin", handlers.focusin);
boundEl.addEventListener("focusout", handlers.focusout);
boundEl.addEventListener("mouseenter", handlers.mouseenter);
boundEl.addEventListener("mouseleave", handlers.mouseleave);
} catch (e) {
console.debug?.(e);
}
// Use global listener with capture phase for keydown to ensure it works regardless of focus
try {
window.addEventListener("keydown", handlers.keydown, { capture: true });
} catch (e) {
console.debug?.(e);
}
setHotkeysScope("panel");
};
const dispose = () => {
if (!boundEl) return;
try {
boundEl.removeEventListener("focusin", handlers.focusin);
boundEl.removeEventListener("focusout", handlers.focusout);
boundEl.removeEventListener("mouseenter", handlers.mouseenter);
boundEl.removeEventListener("mouseleave", handlers.mouseleave);
} catch (e) {
console.debug?.(e);
}
// Remove global listener for keydown
try {
window.removeEventListener("keydown", handlers.keydown, { capture: true });
} catch (e) {
console.debug?.(e);
}
if (getHotkeysState().scope === "panel") {
setHotkeysScope(null);
}
boundEl = null;
};
return { bind, dispose };
}