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:
Will Miao
2025-09-05 16:56:26 +08:00
parent 92ac487128
commit 95e2ff5f1e
10 changed files with 1056 additions and 212 deletions

View File

@@ -8,7 +8,6 @@ import { LoraContextMenu } from './LoraContextMenu.js';
import { RecipeContextMenu } from './RecipeContextMenu.js';
import { CheckpointContextMenu } from './CheckpointContextMenu.js';
import { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
import { state } from '../../state/index.js';
// Factory method to create page-specific context menu instances
export function createPageContextMenu(pageType) {
@@ -24,34 +23,4 @@ export function createPageContextMenu(pageType) {
default:
return null;
}
}
// Initialize context menu coordination for pages that support it
export function initializeContextMenuCoordination(pageContextMenu, bulkContextMenu) {
// Centralized context menu event handler
document.addEventListener('contextmenu', (e) => {
const card = e.target.closest('.model-card');
if (!card) {
// Hide all menus if not right-clicking on a card
pageContextMenu?.hideMenu();
bulkContextMenu?.hideMenu();
return;
}
e.preventDefault();
// Hide all menus first
pageContextMenu?.hideMenu();
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
bulkContextMenu?.showMenu(e.clientX, e.clientY, card);
} else if (!state.bulkMode) {
// Show regular menu when not in bulk mode
pageContextMenu?.showMenu(e.clientX, e.clientY, card);
}
// Don't show any menu for unselected cards in bulk mode
});
}

View File

@@ -70,7 +70,6 @@ export class PageControls {
async initSidebarManager() {
try {
await this.sidebarManager.initialize(this);
console.log('SidebarManager initialized');
} catch (error) {
console.error('Failed to initialize SidebarManager:', error);
}

View File

@@ -9,48 +9,46 @@ import { MODEL_TYPES } from '../../api/apiConfig.js';
import { getModelApiClient } from '../../api/modelApiFactory.js';
import { showDeleteModal } from '../../utils/modalUtils.js';
import { translate } from '../../utils/i18nHelpers.js';
import { eventManager } from '../../utils/EventManager.js';
// Add global event delegation handlers
// Add global event delegation handlers using event manager
export function setupModelCardEventDelegation(modelType) {
const gridElement = document.getElementById('modelGrid');
if (!gridElement) return;
// Remove any existing handler first
eventManager.removeHandler('click', 'modelCard-delegation');
// Remove any existing event listener to prevent duplication
gridElement.removeEventListener('click', gridElement._handleModelCardEvent);
// Create event handler with modelType context
const handleModelCardEvent = (event) => handleModelCardEvent_internal(event, modelType);
// Add the event delegation handler
gridElement.addEventListener('click', handleModelCardEvent);
// Store reference to the handler for cleanup
gridElement._handleModelCardEvent = handleModelCardEvent;
// Register model card event delegation with event manager
eventManager.addHandler('click', 'modelCard-delegation', (event) => {
return handleModelCardEvent_internal(event, modelType);
}, {
priority: 60, // Medium priority for model card interactions
targetSelector: '#modelGrid',
skipWhenModalOpen: false // Allow model card interactions even when modals are open (for some actions)
});
}
// Event delegation handler for all model card events
function handleModelCardEvent_internal(event, modelType) {
// Find the closest card element
const card = event.target.closest('.model-card');
if (!card) return;
if (!card) return false; // Continue with other handlers
// Handle specific elements within the card
if (event.target.closest('.toggle-blur-btn')) {
event.stopPropagation();
toggleBlurContent(card);
return;
return true; // Stop propagation
}
if (event.target.closest('.show-content-btn')) {
event.stopPropagation();
showBlurredContent(card);
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-star')) {
event.stopPropagation();
toggleFavorite(card);
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-globe')) {
@@ -58,41 +56,42 @@ function handleModelCardEvent_internal(event, modelType) {
if (card.dataset.from_civitai === 'true') {
openCivitai(card.dataset.filepath);
}
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-paper-plane')) {
event.stopPropagation();
handleSendToWorkflow(card, event.shiftKey, modelType);
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-copy')) {
event.stopPropagation();
handleCopyAction(card, modelType);
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-trash')) {
event.stopPropagation();
showDeleteModal(card.dataset.filepath);
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-image')) {
event.stopPropagation();
getModelApiClient().replaceModelPreview(card.dataset.filepath);
return;
return true; // Stop propagation
}
if (event.target.closest('.fa-folder-open')) {
event.stopPropagation();
handleExampleImagesAccess(card, modelType);
return;
return true; // Stop propagation
}
// If no specific element was clicked, handle the card click (show modal or toggle selection)
handleCardClick(card, modelType);
return false; // Continue with other handlers (e.g., bulk selection)
}
// Helper functions for event handling

View File

@@ -16,14 +16,13 @@ import { migrateStorageItems } from './utils/storageHelpers.js';
import { i18n } from './i18n/index.js';
import { onboardingManager } from './managers/OnboardingManager.js';
import { BulkContextMenu } from './components/ContextMenu/BulkContextMenu.js';
import { createPageContextMenu, initializeContextMenuCoordination } from './components/ContextMenu/index.js';
import { createPageContextMenu } from './components/ContextMenu/index.js';
import { initializeEventManagement } from './utils/eventManagementInit.js';
// Core application class
export class AppCore {
constructor() {
this.initialized = false;
this.pageContextMenu = null;
this.bulkContextMenu = null;
}
// Initialize core functionality
@@ -70,6 +69,8 @@ export class AppCore {
const cardInfoDisplay = state.global.settings.cardInfoDisplay || 'always';
document.body.classList.toggle('hover-reveal', cardInfoDisplay === 'hover');
initializeEventManagement();
// Mark as initialized
this.initialized = true;
@@ -107,15 +108,7 @@ export class AppCore {
// Initialize context menus for the current page
initializeContextMenus(pageType) {
// Create page-specific context menu
this.pageContextMenu = createPageContextMenu(pageType);
// Get bulk context menu from bulkManager
this.bulkContextMenu = bulkManager.bulkContextMenu;
// Initialize context menu coordination
if (this.pageContextMenu || this.bulkContextMenu) {
initializeContextMenuCoordination(this.pageContextMenu, this.bulkContextMenu);
}
window.pageContextMenu = createPageContextMenu(pageType);
}
}

View File

@@ -5,6 +5,7 @@ import { modalManager } from './ModalManager.js';
import { getModelApiClient } from '../api/modelApiFactory.js';
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
import { eventManager } from '../utils/EventManager.js';
export class BulkManager {
constructor() {
@@ -14,10 +15,16 @@ export class BulkManager {
// Marquee selection properties
this.isMarqueeActive = false;
this.isDragging = false;
this.marqueeStart = { x: 0, y: 0 };
this.marqueeElement = null;
this.initialSelectedModels = new Set();
// Drag detection properties
this.dragThreshold = 5; // Pixels to move before considering it a drag
this.mouseDownTime = 0;
this.mouseDownPosition = { x: 0, y: 0 };
// Model type specific action configurations
this.actionConfig = {
[MODEL_TYPES.LORA]: {
@@ -48,50 +55,156 @@ export class BulkManager {
}
initialize() {
this.setupEventListeners();
this.setupGlobalKeyboardListeners();
this.setupMarqueeSelection();
// Register with event manager for coordinated event handling
this.registerEventHandlers();
// Initialize bulk mode state in event manager
eventManager.setState('bulkMode', state.bulkMode || false);
}
setBulkContextMenu(bulkContextMenu) {
this.bulkContextMenu = bulkContextMenu;
}
setupEventListeners() {
// Only setup bulk mode toggle button listener now
// Context menu actions are handled by BulkContextMenu
/**
* Register all event handlers with the centralized event manager
*/
registerEventHandlers() {
// Register keyboard shortcuts with high priority
eventManager.addHandler('keydown', 'bulkManager-keyboard', (e) => {
return this.handleGlobalKeyboard(e);
}, {
priority: 100,
skipWhenModalOpen: true
});
// Register marquee selection events
eventManager.addHandler('mousedown', 'bulkManager-marquee-start', (e) => {
return this.handleMarqueeStart(e);
}, {
priority: 80,
skipWhenModalOpen: true,
targetSelector: '.page-content',
excludeSelector: '.model-card, button, input, folder-sidebar, .breadcrumb-item, #path-part, .context-menu',
button: 0 // Left mouse button only
});
eventManager.addHandler('mousemove', 'bulkManager-marquee-move', (e) => {
if (this.isMarqueeActive) {
this.updateMarqueeSelection(e);
} else if (this.mouseDownTime && !this.isDragging) {
// Check if we've moved enough to consider it a drag
const dx = e.clientX - this.mouseDownPosition.x;
const dy = e.clientY - this.mouseDownPosition.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance >= this.dragThreshold) {
this.isDragging = true;
this.startMarqueeSelection(e, true);
}
}
}, {
priority: 90,
skipWhenModalOpen: true
});
eventManager.addHandler('mouseup', 'bulkManager-marquee-end', (e) => {
if (this.isMarqueeActive) {
this.endMarqueeSelection(e);
return true; // Stop propagation
}
// Reset drag detection if we had a mousedown but didn't drag
if (this.mouseDownTime) {
this.mouseDownTime = 0;
return false; // Allow other handlers to process the click
}
}, {
priority: 90
});
eventManager.addHandler('contextmenu', 'bulkManager-marquee-prevent', (e) => {
if (this.isMarqueeActive) {
e.preventDefault();
return true; // Stop propagation
}
}, {
priority: 100
});
// Modified: Clear selection and exit bulk mode on left-click page-content blank area
// Lower priority to avoid interfering with context menu interactions
eventManager.addHandler('mousedown', 'bulkManager-clear-on-blank', (e) => {
// Only handle left mouse button
if (e.button !== 0) return false;
// Only if in bulk mode and there are selected models
if (state.bulkMode && state.selectedModels && state.selectedModels.size > 0) {
// Check if click is on blank area (not on a model card or excluded elements)
// Also exclude context menu elements to prevent interference
this.clearSelection();
this.toggleBulkMode();
// Prevent further handling
return true;
}
return false;
}, {
priority: 70, // Lower priority to let context menu events process first
onlyInBulkMode: true,
targetSelector: '.page-content',
excludeSelector: '.model-card, button, input, folder-sidebar, .breadcrumb-item, #path-part, .context-menu, .context-menu *',
button: 0 // Left mouse button only
});
}
setupGlobalKeyboardListeners() {
document.addEventListener('keydown', (e) => {
if (modalManager.isAnyModalOpen()) {
return;
}
/**
* Clean up event handlers
*/
cleanup() {
eventManager.removeAllHandlersForSource('bulkManager-keyboard');
eventManager.removeAllHandlersForSource('bulkManager-marquee-start');
eventManager.removeAllHandlersForSource('bulkManager-marquee-move');
eventManager.removeAllHandlersForSource('bulkManager-marquee-end');
eventManager.removeAllHandlersForSource('bulkManager-marquee-prevent');
eventManager.removeAllHandlersForSource('bulkManager-clear-on-blank');
}
const searchInput = document.getElementById('searchInput');
if (searchInput && document.activeElement === searchInput) {
return;
}
/**
* Handle global keyboard events through the event manager
*/
handleGlobalKeyboard(e) {
// Skip if modal is open (handled by event manager conditions)
// Skip if search input is focused
const searchInput = document.getElementById('searchInput');
if (searchInput && document.activeElement === searchInput) {
return false; // Don't handle, allow default behavior
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') {
e.preventDefault();
if (!state.bulkMode) {
this.toggleBulkMode();
setTimeout(() => this.selectAllVisibleModels(), 50);
} else {
this.selectAllVisibleModels();
}
} else if (e.key === 'Escape' && state.bulkMode) {
this.toggleBulkMode();
} else if (e.key.toLowerCase() === 'b') {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') {
e.preventDefault();
if (!state.bulkMode) {
this.toggleBulkMode();
setTimeout(() => this.selectAllVisibleModels(), 50);
} else {
this.selectAllVisibleModels();
}
});
return true; // Stop propagation
} else if (e.key === 'Escape' && state.bulkMode) {
this.toggleBulkMode();
return true; // Stop propagation
} else if (e.key.toLowerCase() === 'b') {
this.toggleBulkMode();
return true; // Stop propagation
}
return false; // Continue with other handlers
}
toggleBulkMode() {
state.bulkMode = !state.bulkMode;
// Update event manager state
eventManager.setState('bulkMode', state.bulkMode);
this.bulkBtn.classList.toggle('active', state.bulkMode);
updateCardsForBulkMode(state.bulkMode);
@@ -133,8 +246,6 @@ export class BulkManager {
metadataCache.set(filepath, {
fileName: card.dataset.file_name,
usageTips: card.dataset.usage_tips,
previewUrl: this.getCardPreviewUrl(card),
isVideo: this.isCardPreviewVideo(card),
modelName: card.dataset.name
});
}
@@ -162,16 +273,6 @@ export class BulkManager {
return pageState.metadataCache;
}
}
getCardPreviewUrl(card) {
const img = card.querySelector('img');
const video = card.querySelector('video source');
return img ? img.src : (video ? video.src : '/loras_static/images/no-preview.png');
}
isCardPreviewVideo(card) {
return card.querySelector('video') !== null;
}
applySelectionState() {
if (!state.bulkMode) return;
@@ -185,8 +286,6 @@ export class BulkManager {
metadataCache.set(filepath, {
fileName: card.dataset.file_name,
usageTips: card.dataset.usage_tips,
previewUrl: this.getCardPreviewUrl(card),
isVideo: this.isCardPreviewVideo(card),
modelName: card.dataset.name
});
} else {
@@ -354,8 +453,6 @@ export class BulkManager {
metadataCache.set(item.file_path, {
fileName: item.file_name,
usageTips: item.usage_tips || '{}',
previewUrl: item.preview_url || '/loras_static/images/no-preview.png',
isVideo: item.is_video || false,
modelName: item.name || item.file_name
});
}
@@ -399,8 +496,6 @@ export class BulkManager {
...metadata,
fileName: card.dataset.file_name,
usageTips: card.dataset.usage_tips,
previewUrl: this.getCardPreviewUrl(card),
isVideo: this.isCardPreviewVideo(card),
modelName: card.dataset.name
});
}
@@ -866,64 +961,33 @@ export class BulkManager {
}
/**
* Setup marquee selection functionality
* Handle marquee start through event manager
*/
setupMarqueeSelection() {
const container = document.querySelector('.models-container') || document.body;
handleMarqueeStart(e) {
// Store mousedown info for potential drag detection
this.mouseDownTime = Date.now();
this.mouseDownPosition = { x: e.clientX, y: e.clientY };
this.isDragging = false;
container.addEventListener('mousedown', (e) => {
// Disable marquee if any modal is open
if (modalManager.isAnyModalOpen()) {
return;
}
// Only start marquee selection on left click in empty areas
if (e.button !== 0 || e.target.closest('.model-card') || e.target.closest('button') || e.target.closest('input')) {
return;
}
// Prevent text selection during marquee
e.preventDefault();
this.startMarqueeSelection(e);
});
document.addEventListener('mousemove', (e) => {
// Disable marquee update if any modal is open
if (modalManager.isAnyModalOpen()) {
return;
}
if (this.isMarqueeActive) {
this.updateMarqueeSelection(e);
}
});
document.addEventListener('mouseup', (e) => {
if (this.isMarqueeActive) {
this.endMarqueeSelection(e);
}
});
// Prevent context menu during marquee selection
document.addEventListener('contextmenu', (e) => {
if (this.isMarqueeActive) {
e.preventDefault();
}
});
// Don't start marquee yet - wait to see if user is dragging
return false;
}
/**
* Start marquee selection
* @param {MouseEvent} e - Mouse event
* @param {boolean} isDragging - Whether this is triggered from a drag operation
*/
startMarqueeSelection(e) {
startMarqueeSelection(e, isDragging = false) {
// Store initial mouse position
this.marqueeStart.x = e.clientX;
this.marqueeStart.y = e.clientY;
this.marqueeStart.x = this.mouseDownPosition.x;
this.marqueeStart.y = this.mouseDownPosition.y;
// Store initial selection state
this.initialSelectedModels = new Set(state.selectedModels);
// Enter bulk mode if not already active
if (!state.bulkMode) {
// Enter bulk mode if not already active and we're actually dragging
if (isDragging && !state.bulkMode) {
this.toggleBulkMode();
}
@@ -932,6 +996,9 @@ export class BulkManager {
this.isMarqueeActive = true;
// Update event manager state
eventManager.setState('marqueeActive', true);
// Add visual feedback class to body
document.body.classList.add('marquee-selecting');
}
@@ -1010,8 +1077,6 @@ export class BulkManager {
metadataCache.set(filepath, {
fileName: card.dataset.file_name,
usageTips: card.dataset.usage_tips,
previewUrl: this.getCardPreviewUrl(card),
isVideo: this.isCardPreviewVideo(card),
modelName: card.dataset.name
});
}
@@ -1035,7 +1100,13 @@ export class BulkManager {
* End marquee selection
*/
endMarqueeSelection(e) {
// First, mark as inactive to prevent double processing
this.isMarqueeActive = false;
this.isDragging = false;
this.mouseDownTime = 0;
// Update event manager state
eventManager.setState('marqueeActive', false);
// Remove marquee element
if (this.marqueeElement) {
@@ -1046,14 +1117,14 @@ export class BulkManager {
// Remove visual feedback class
document.body.classList.remove('marquee-selecting');
// Show toast with selection count if any items were selected
// Get selection count
const selectionCount = state.selectedModels.size;
if (selectionCount > 0) {
const currentConfig = MODEL_CONFIG[state.currentPageType];
showToast('toast.models.marqueeSelectionComplete', {
count: selectionCount,
type: currentConfig.displayName.toLowerCase()
}, 'success');
// If no models were selected, exit bulk mode
if (selectionCount === 0) {
if (state.bulkMode) {
this.toggleBulkMode();
}
}
// Clear initial selection state

View File

@@ -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();
}
}

View 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;
}

View File

@@ -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