204 lines
6.5 KiB
TypeScript
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, "&")
|
|
.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);
|
|
}
|
|
}
|