/** * ComfyUI MagnifyGlass - Shared Utilities * * Common utility functions shared between magnify glass and info panel modules. */ import type { ComfyApp } from '../types/comfyui'; // Reference to ComfyUI's app - will be set at runtime // @ts-ignore import { app } from "/scripts/app.js"; /** * Debug logger interface */ export interface DebugLogger { log(...args: unknown[]): void; error(...args: unknown[]): void; } /** * Safely get a setting value from ComfyUI settings with fallback to default. * @param key - The setting key * @param fallback - Fallback value if setting not found * @returns The setting value or default */ export function getSettingValue(key: string, fallback: T): T { try { // We only pass the key to avoid the "Parameter defaultValue is deprecated" warning in ComfyUI core const value = app.ui.settings.getSettingValue(key); return value === undefined ? fallback : value as T; } catch (e) { console.warn(`ComfyUI Magnifying Glass: Could not get setting ${key}, using fallback ${fallback}. Error: ${e}`); return fallback; } } /** * Safely set a setting value in ComfyUI settings. * @param key - The setting key * @param value - The value to set */ export function setSettingValue(key: string, value: any): void { try { app.ui.settings.setSettingValue(key, value); } catch (e) { console.warn(`Failed to set setting ${key}:`, e); } } /** * Check if the user is currently typing in an input field. * Used for smart input detection to avoid triggering hotkeys while typing. * @returns True if user is typing in an input field */ export function isUserTyping(): boolean { const activeElement = document.activeElement as HTMLElement | null; if (!activeElement) return false; // Check if the active element is an input, textarea, or contenteditable const tagName = activeElement.tagName.toLowerCase(); if (tagName === 'input' || tagName === 'textarea') { return true; } // Check for contenteditable elements if (activeElement.contentEditable === 'true') { return true; } // Check if it's inside a form or has input-like classes if (activeElement.closest('form') || activeElement.classList.contains('cm-editor') || // CodeMirror editor activeElement.classList.contains('monaco-editor') || // Monaco editor activeElement.closest('.cm-editor') || activeElement.closest('.monaco-editor')) { return true; } return false; } /** * Find the LiteGraph canvas element in the DOM. * Tries multiple selectors for compatibility with different ComfyUI versions. * @returns The canvas element or null if not found */ export function findLiteGraphCanvas(): HTMLCanvasElement | null { // Try multiple selectors for compatibility const canvas = document.getElementById("graph-canvas") as HTMLCanvasElement | null; if (canvas) return canvas; const graphCanvas = document.querySelector("canvas.graphcanvas") as HTMLCanvasElement | null; if (graphCanvas) return graphCanvas; // Try canvas manager (dynamic access for ComfyUI compatibility) const appAny = app as any; if (appAny?.canvas_manager?.container) { const managerCanvas = appAny.canvas_manager.container.querySelector("canvas") as HTMLCanvasElement | null; if (managerCanvas) return managerCanvas; } // Try direct canvas reference if (appAny?.canvas?.graph_canvas) { return appAny.canvas.graph_canvas; } return null; } /** * Normalize a color value to include the # symbol for CSS. * @param color - The color value (with or without #) * @returns The normalized color with # prefix */ export function normalizeColor(color: string | null | undefined): string { if (!color) return color ?? ''; return color.startsWith('#') ? color : `#${color}`; } /** * Clamp a value between min and max bounds. * @param value - The value to clamp * @param min - Minimum bound * @param max - Maximum bound * @returns The clamped value */ export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } /** * Create a debug logger that only logs when debug mode is enabled. * @param prefix - Prefix for log messages * @param isDebugEnabled - Function that returns whether debug is enabled * @returns Logger object */ export function createDebugLogger( prefix: string, isDebugEnabled: () => boolean ): DebugLogger { return { log(...args: unknown[]): void { if (isDebugEnabled()) { console.log(`${prefix}:`, ...args); } }, error(...args: unknown[]): void { console.error(`${prefix} ERROR:`, ...args); } }; } /** * Escapes HTML special characters in a string to prevent XSS. * @param str - The string to escape * @returns Escaped string */ export function escapeHtml(str: unknown): string { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * Recursively sanitize an element by removing dangerous attributes. * This prevents XSS when cloning nodes that might contain malicious handlers or javascript: URIs. * @param element - The element to sanitize */ export function sanitizeElement(element: HTMLElement): void { if (!element || !element.attributes) return; const attributes = element.attributes; for (let i = attributes.length - 1; i >= 0; i--) { const attrName = attributes[i].name.toLowerCase(); const attrValue = attributes[i].value.toLowerCase().trim(); // 1. Remove event handlers (on*) if (attrName.startsWith('on')) { element.removeAttribute(attributes[i].name); continue; } // 2. Remove javascript: URIs in specific attributes if (['href', 'src', 'action', 'formaction'].includes(attrName)) { // Strip all whitespace and control characters to prevent bypasses like "java\tscript:" const normalizedValue = attrValue.replace(/[\s\x00-\x1F\x7F]/g, ''); if (normalizedValue.startsWith('javascript:')) { element.removeAttribute(attributes[i].name); } } } // Recursively sanitize children const children = element.children; for (let i = 0; i < children.length; i++) { sanitizeElement(children[i] as HTMLElement); } }