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

290 lines
10 KiB
TypeScript

import {
getHotkeysState,
isHotkeysSuspended,
setHotkeysScope,
setHotkeysSuspendedFlag,
} from "./hotkeysState.js";
import { getActiveGridContainer } from "../panelRuntimeRefs.js";
export function setHotkeysSuspended(suspended: boolean): void {
setHotkeysSuspendedFlag(suspended);
}
export function createPanelHotkeysController({
onTriggerScan,
getScanContext,
onToggleDetails,
onToggleFloatingViewer,
onFocusSearch,
onClearSearch,
allowListKeys,
}: Record<string, any> = {}): Record<string, any> {
let panelFocused = false;
let panelHovered = false;
let boundEl: any = 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: any) => {
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 {
const activeElement = document.activeElement;
return Boolean(
boundEl?.isConnected &&
activeElement &&
activeElement !== document.body &&
boundEl.contains?.(activeElement)
);
} catch (e) {
console.debug?.(e);
return false;
}
};
const isGridHotkeyContext = (event: any) => {
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: any) => {
// 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;
// V toggles the floating viewer - processed before the panel-active guard so it
// still fires after subfolder navigation when hover/focus state may have reset
// (e.g. the focused folder card was removed from the DOM after reload).
if (
event.key?.toLowerCase?.() === "v" &&
!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
) {
const vTyping =
event.target?.isContentEditable ||
event.target?.closest?.("input, textarea, select, [contenteditable='true']");
if (!vTyping && boundEl?.isConnected && isPanelHotkeyContext(event)) {
// Block only when focus is inside an input/textarea that lives outside this panel.
let blockedByOutsideInput = false;
try {
const ae = document.activeElement;
if (ae && ae !== document.body && !boundEl.contains?.(ae)) {
blockedByOutsideInput =
ae.matches?.("input, textarea, select, [contenteditable='true']") ||
(ae as HTMLElement).isContentEditable;
}
} catch (e) {
console.debug?.(e);
}
if (!blockedByOutsideInput) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
try {
onToggleFloatingViewer?.();
} catch (e) {
console.debug?.(e);
}
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 toggles the floating viewer for the current panel selection.
// Keep modified V shortcuts untouched for app/browser behaviors such as paste.
if (
lower === "v" &&
!event.ctrlKey &&
!event.metaKey &&
!event.altKey &&
!event.shiftKey
) {
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: any) => {
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 };
}