mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-23 22:22:11 -03:00
Implement centralized event management system with priority handling and state tracking
- Enhanced EventManager class to support priority-based event handling, conditional execution, and automatic cleanup. - Integrated event management into BulkManager for global keyboard shortcuts and marquee selection events. - Migrated mouse tracking and node selector events to UIHelpers for better coordination. - Established global event handlers for context menu interactions and modal state management. - Added comprehensive documentation for event management implementation and usage. - Implemented initialization logic for event management, including error handling and cleanup on page unload.
This commit is contained in:
@@ -9,16 +9,19 @@ export class EventManager {
|
||||
this.activeStates = {
|
||||
bulkMode: false,
|
||||
marqueeActive: false,
|
||||
modalOpen: false
|
||||
modalOpen: false,
|
||||
nodeSelectorActive: false
|
||||
};
|
||||
// Store references to cleanup functions
|
||||
this.cleanupFunctions = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event handler with priority
|
||||
* Register an event handler with priority and conditional execution
|
||||
* @param {string} eventType - The DOM event type (e.g., 'click', 'mousedown')
|
||||
* @param {string} source - Source identifier (e.g., 'bulkManager', 'contextMenu')
|
||||
* @param {Function} handler - Event handler function
|
||||
* @param {Object} options - Additional options including priority (higher number = higher priority)
|
||||
* @param {Object} options - Additional options including priority and conditions
|
||||
*/
|
||||
addHandler(eventType, source, handler, options = {}) {
|
||||
if (!this.handlers.has(eventType)) {
|
||||
@@ -28,15 +31,21 @@ export class EventManager {
|
||||
}
|
||||
|
||||
const handlerList = this.handlers.get(eventType);
|
||||
handlerList.push({
|
||||
const handlerEntry = {
|
||||
source,
|
||||
handler,
|
||||
priority: options.priority || 0,
|
||||
options
|
||||
});
|
||||
options,
|
||||
// Store cleanup function if provided
|
||||
cleanup: options.cleanup || null
|
||||
};
|
||||
|
||||
handlerList.push(handlerEntry);
|
||||
|
||||
// Sort by priority
|
||||
handlerList.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
return handlerEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,6 +55,17 @@ export class EventManager {
|
||||
if (!this.handlers.has(eventType)) return;
|
||||
|
||||
const handlerList = this.handlers.get(eventType);
|
||||
|
||||
// Find and cleanup handler before removing
|
||||
const handlerToRemove = handlerList.find(h => h.source === source);
|
||||
if (handlerToRemove && handlerToRemove.cleanup) {
|
||||
try {
|
||||
handlerToRemove.cleanup();
|
||||
} catch (error) {
|
||||
console.warn(`Error during cleanup for ${source}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const newList = handlerList.filter(h => h.source !== source);
|
||||
|
||||
if (newList.length === 0) {
|
||||
@@ -90,20 +110,91 @@ export class EventManager {
|
||||
if (options.onlyInBulkMode && !this.activeStates.bulkMode) continue;
|
||||
if (options.onlyWhenMarqueeActive && !this.activeStates.marqueeActive) continue;
|
||||
if (options.skipWhenModalOpen && this.activeStates.modalOpen) continue;
|
||||
if (options.skipWhenNodeSelectorActive && this.activeStates.nodeSelectorActive) continue;
|
||||
if (options.onlyWhenNodeSelectorActive && !this.activeStates.nodeSelectorActive) continue;
|
||||
|
||||
// Execute handler
|
||||
const result = handler(event);
|
||||
// Apply element-based filters
|
||||
if (options.targetSelector && !this._matchesSelector(event.target, options.targetSelector)) continue;
|
||||
if (options.excludeSelector && this._matchesSelector(event.target, options.excludeSelector)) continue;
|
||||
|
||||
// Stop propagation if handler returns true
|
||||
if (result === true) break;
|
||||
// Apply button filters
|
||||
if (options.button !== undefined && event.button !== options.button) continue;
|
||||
|
||||
try {
|
||||
// Execute handler
|
||||
const result = handler(event);
|
||||
|
||||
// Stop propagation if handler returns true
|
||||
if (result === true) break;
|
||||
} catch (error) {
|
||||
console.error(`Error in event handler for ${eventType}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if an element matches or is contained within an element matching the selector
|
||||
* This improves the robustness of the selector matching
|
||||
*/
|
||||
_matchesSelector(element, selector) {
|
||||
if (element.matches && element.matches(selector)) {
|
||||
return true;
|
||||
}
|
||||
if (element.closest && element.closest(selector)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update application state
|
||||
*/
|
||||
setState(state, value) {
|
||||
this.activeStates[state] = value;
|
||||
if (this.activeStates.hasOwnProperty(state)) {
|
||||
this.activeStates[state] = value;
|
||||
} else {
|
||||
console.warn(`Unknown state: ${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current application state
|
||||
*/
|
||||
getState(state) {
|
||||
return this.activeStates[state];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all handlers for a specific source
|
||||
*/
|
||||
removeAllHandlersForSource(source) {
|
||||
const eventTypes = Array.from(this.handlers.keys());
|
||||
eventTypes.forEach(eventType => {
|
||||
this.removeHandler(eventType, source);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all event listeners (useful for app teardown)
|
||||
*/
|
||||
cleanup() {
|
||||
const eventTypes = Array.from(this.handlers.keys());
|
||||
eventTypes.forEach(eventType => {
|
||||
const handlers = this.handlers.get(eventType);
|
||||
// Run cleanup functions
|
||||
handlers.forEach(h => {
|
||||
if (h.cleanup) {
|
||||
try {
|
||||
h.cleanup();
|
||||
} catch (error) {
|
||||
console.warn(`Error during cleanup for ${h.source}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.cleanupDOMListener(eventType);
|
||||
});
|
||||
this.handlers.clear();
|
||||
this.cleanupFunctions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
226
static/js/utils/eventManagementInit.js
Normal file
226
static/js/utils/eventManagementInit.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Event Management Initialization
|
||||
*
|
||||
* This module handles the initialization and coordination of the centralized
|
||||
* event management system across the application.
|
||||
*/
|
||||
|
||||
import { eventManager } from './EventManager.js';
|
||||
import { modalManager } from '../managers/ModalManager.js';
|
||||
import { state } from '../state/index.js';
|
||||
|
||||
/**
|
||||
* Initialize the centralized event management system
|
||||
*/
|
||||
export function initializeEventManagement() {
|
||||
console.log('Initializing centralized event management system...');
|
||||
|
||||
// Initialize modal state tracking
|
||||
initializeModalStateTracking();
|
||||
|
||||
// Set up global error handling for event handlers
|
||||
setupGlobalEventErrorHandling();
|
||||
|
||||
// Set up cleanup on page unload
|
||||
setupPageUnloadCleanup();
|
||||
|
||||
// Register global event handlers that need coordination
|
||||
registerContextMenuEvents();
|
||||
registerGlobalClickHandlers();
|
||||
|
||||
console.log('Event management system initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize modal state tracking with the event manager
|
||||
*/
|
||||
function initializeModalStateTracking() {
|
||||
// Override modalManager methods to update event manager state
|
||||
const originalShowModal = modalManager.showModal.bind(modalManager);
|
||||
const originalCloseModal = modalManager.closeModal.bind(modalManager);
|
||||
const originalIsAnyModalOpen = modalManager.isAnyModalOpen.bind(modalManager);
|
||||
|
||||
modalManager.showModal = function(...args) {
|
||||
const result = originalShowModal(...args);
|
||||
eventManager.setState('modalOpen', this.isAnyModalOpen());
|
||||
return result;
|
||||
};
|
||||
|
||||
modalManager.closeModal = function(...args) {
|
||||
const result = originalCloseModal(...args);
|
||||
eventManager.setState('modalOpen', this.isAnyModalOpen());
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up global error handling for event handlers
|
||||
*/
|
||||
function setupGlobalEventErrorHandling() {
|
||||
// Override the handleEvent method to add better error handling
|
||||
const originalHandleEvent = eventManager.handleEvent.bind(eventManager);
|
||||
|
||||
eventManager.handleEvent = function(eventType, event) {
|
||||
try {
|
||||
return originalHandleEvent(eventType, event);
|
||||
} catch (error) {
|
||||
console.error(`Critical error in event management for ${eventType}:`, error);
|
||||
// Don't let event handling errors crash the app
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up cleanup when the page is unloaded
|
||||
*/
|
||||
function setupPageUnloadCleanup() {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
console.log('Cleaning up event management system...');
|
||||
eventManager.cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register context menu related events with proper priority
|
||||
*/
|
||||
function registerContextMenuEvents() {
|
||||
eventManager.addHandler('contextmenu', 'contextMenu-coordination', (e) => {
|
||||
const card = e.target.closest('.model-card');
|
||||
if (!card) {
|
||||
// Hide all menus if not right-clicking on a card
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
return false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// Hide all menus first
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
|
||||
// Determine which menu to show based on bulk mode and selection state
|
||||
if (state.bulkMode && card.classList.contains('selected')) {
|
||||
// Show bulk menu for selected cards in bulk mode
|
||||
window.bulkManager?.bulkContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||
} else if (!state.bulkMode) {
|
||||
// Show regular menu when not in bulk mode
|
||||
window.pageContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||
}
|
||||
// Don't show any menu for unselected cards in bulk mode
|
||||
|
||||
return true; // Stop propagation
|
||||
}, {
|
||||
priority: 200, // Higher priority than bulk manager events
|
||||
skipWhenModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register global click handlers for context menu hiding
|
||||
*/
|
||||
function registerGlobalClickHandlers() {
|
||||
eventManager.addHandler('click', 'contextMenu-hide', (e) => {
|
||||
// Hide context menus when clicking elsewhere
|
||||
if (!e.target.closest('.context-menu')) {
|
||||
window.pageContextMenu?.hideMenu();
|
||||
window.bulkManager?.bulkContextMenu?.hideMenu();
|
||||
}
|
||||
return false; // Allow other handlers to process
|
||||
}, {
|
||||
priority: 50,
|
||||
skipWhenModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register common application-wide event handlers
|
||||
*/
|
||||
export function registerGlobalEventHandlers() {
|
||||
// Escape key handler for closing modals/panels
|
||||
eventManager.addHandler('keydown', 'global-escape', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
// Check if any modal is open and close it
|
||||
if (eventManager.getState('modalOpen')) {
|
||||
modalManager.closeCurrentModal();
|
||||
return true; // Stop propagation
|
||||
}
|
||||
|
||||
// Check if node selector is active and close it
|
||||
if (eventManager.getState('nodeSelectorActive')) {
|
||||
// The node selector should handle its own escape key
|
||||
return false; // Continue with other handlers
|
||||
}
|
||||
}
|
||||
return false; // Continue with other handlers
|
||||
}, {
|
||||
priority: 250 // Very high priority for escape handling
|
||||
});
|
||||
|
||||
// Global focus management
|
||||
eventManager.addHandler('focusin', 'global-focus', (e) => {
|
||||
// Track focus for accessibility and keyboard navigation
|
||||
window.lastFocusedElement = e.target;
|
||||
}, {
|
||||
priority: 10 // Low priority for tracking
|
||||
});
|
||||
|
||||
// Global click tracking for analytics (if needed)
|
||||
eventManager.addHandler('click', 'global-analytics', (e) => {
|
||||
// Track clicks for usage analytics
|
||||
// This runs last and doesn't interfere with other handlers
|
||||
trackUserInteraction(e);
|
||||
}, {
|
||||
priority: 1 // Lowest priority
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Example analytics tracking function
|
||||
*/
|
||||
function trackUserInteraction(event) {
|
||||
// Implement analytics tracking here
|
||||
// This is just a placeholder
|
||||
if (window.analytics && typeof window.analytics.track === 'function') {
|
||||
const element = event.target;
|
||||
const elementInfo = {
|
||||
tag: element.tagName.toLowerCase(),
|
||||
class: element.className,
|
||||
id: element.id,
|
||||
text: element.textContent?.substring(0, 50)
|
||||
};
|
||||
|
||||
window.analytics.track('ui_interaction', elementInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to check if event management is properly initialized
|
||||
*/
|
||||
export function isEventManagementInitialized() {
|
||||
return eventManager && typeof eventManager.addHandler === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event management statistics for debugging
|
||||
*/
|
||||
export function getEventManagementStats() {
|
||||
const stats = {
|
||||
totalEventTypes: eventManager.handlers.size,
|
||||
totalHandlers: 0,
|
||||
handlersBySource: {},
|
||||
currentStates: { ...eventManager.activeStates }
|
||||
};
|
||||
|
||||
eventManager.handlers.forEach((handlers, eventType) => {
|
||||
stats.totalHandlers += handlers.length;
|
||||
handlers.forEach(handler => {
|
||||
if (!stats.handlersBySource[handler.source]) {
|
||||
stats.handlersBySource[handler.source] = 0;
|
||||
}
|
||||
stats.handlersBySource[handler.source]++;
|
||||
});
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { translate } from './i18nHelpers.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { getStorageItem, setStorageItem } from './storageHelpers.js';
|
||||
import { NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
|
||||
import { eventManager } from './EventManager.js';
|
||||
|
||||
/**
|
||||
* Utility function to copy text to clipboard with fallback for older browsers
|
||||
@@ -528,12 +529,15 @@ function showNodeSelector(nodes, loraSyntax, replaceMode, syntaxType) {
|
||||
selector.style.display = 'block';
|
||||
nodeSelectorState.isActive = true;
|
||||
|
||||
// Setup event listeners with proper cleanup
|
||||
// Update event manager state
|
||||
eventManager.setState('nodeSelectorActive', true);
|
||||
|
||||
// Setup event listeners with proper cleanup through event manager
|
||||
setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, syntaxType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for node selector
|
||||
* Setup event listeners for node selector using event manager
|
||||
* @param {HTMLElement} selector - The selector element
|
||||
* @param {Object} nodes - Registry nodes data
|
||||
* @param {string} loraSyntax - The LoRA syntax to send
|
||||
@@ -544,17 +548,21 @@ function setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, synta
|
||||
// Clean up any existing event listeners
|
||||
cleanupNodeSelectorEvents();
|
||||
|
||||
// Handle clicks outside to close
|
||||
nodeSelectorState.clickHandler = (e) => {
|
||||
// Register click outside handler with event manager
|
||||
eventManager.addHandler('click', 'nodeSelector-outside', (e) => {
|
||||
if (!selector.contains(e.target)) {
|
||||
hideNodeSelector();
|
||||
return true; // Stop propagation
|
||||
}
|
||||
};
|
||||
}, {
|
||||
priority: 200, // High priority to handle before other click handlers
|
||||
onlyWhenNodeSelectorActive: true
|
||||
});
|
||||
|
||||
// Handle node selection
|
||||
nodeSelectorState.selectorClickHandler = async (e) => {
|
||||
// Register node selection handler with event manager
|
||||
eventManager.addHandler('click', 'nodeSelector-selection', async (e) => {
|
||||
const nodeItem = e.target.closest('.node-item');
|
||||
if (!nodeItem) return;
|
||||
if (!nodeItem) return false; // Continue with other handlers
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -571,33 +579,25 @@ function setupNodeSelectorEvents(selector, nodes, loraSyntax, replaceMode, synta
|
||||
}
|
||||
|
||||
hideNodeSelector();
|
||||
};
|
||||
|
||||
// Add event listeners with a small delay to prevent immediate triggering
|
||||
setTimeout(() => {
|
||||
if (nodeSelectorState.isActive) {
|
||||
document.addEventListener('click', nodeSelectorState.clickHandler);
|
||||
selector.addEventListener('click', nodeSelectorState.selectorClickHandler);
|
||||
}
|
||||
}, 100);
|
||||
return true; // Stop propagation
|
||||
}, {
|
||||
priority: 150, // High priority but lower than outside click
|
||||
targetSelector: '#nodeSelector',
|
||||
onlyWhenNodeSelectorActive: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up node selector event listeners
|
||||
*/
|
||||
function cleanupNodeSelectorEvents() {
|
||||
if (nodeSelectorState.clickHandler) {
|
||||
document.removeEventListener('click', nodeSelectorState.clickHandler);
|
||||
nodeSelectorState.clickHandler = null;
|
||||
}
|
||||
// Remove event handlers from event manager
|
||||
eventManager.removeHandler('click', 'nodeSelector-outside');
|
||||
eventManager.removeHandler('click', 'nodeSelector-selection');
|
||||
|
||||
if (nodeSelectorState.selectorClickHandler) {
|
||||
const selector = document.getElementById('nodeSelector');
|
||||
if (selector) {
|
||||
selector.removeEventListener('click', nodeSelectorState.selectorClickHandler);
|
||||
}
|
||||
nodeSelectorState.selectorClickHandler = null;
|
||||
}
|
||||
// Clear legacy references
|
||||
nodeSelectorState.clickHandler = null;
|
||||
nodeSelectorState.selectorClickHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -613,6 +613,9 @@ function hideNodeSelector() {
|
||||
// Clean up event listeners
|
||||
cleanupNodeSelectorEvents();
|
||||
nodeSelectorState.isActive = false;
|
||||
|
||||
// Update event manager state
|
||||
eventManager.setState('nodeSelectorActive', false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -651,11 +654,21 @@ function positionNearMouse(element) {
|
||||
element.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
// Track mouse position for node selector positioning
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
window.lastMouseX = e.clientX;
|
||||
window.lastMouseY = e.clientY;
|
||||
});
|
||||
/**
|
||||
* Initialize mouse tracking for positioning elements
|
||||
*/
|
||||
export function initializeMouseTracking() {
|
||||
// Register mouse tracking with event manager
|
||||
eventManager.addHandler('mousemove', 'uiHelpers-mouseTracking', (e) => {
|
||||
window.lastMouseX = e.clientX;
|
||||
window.lastMouseY = e.clientY;
|
||||
}, {
|
||||
priority: 10 // Low priority since this is just tracking
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize mouse tracking when module loads
|
||||
initializeMouseTracking();
|
||||
|
||||
/**
|
||||
* Opens the example images folder for a specific model
|
||||
|
||||
Reference in New Issue
Block a user