Files
AEmotionStudio-ComfyUI-Magn…/src/shared/utils.ts
T

204 lines
6.5 KiB
TypeScript

/**
* 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<T>(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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* 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);
}
}