Files
Will Miao 91f955a277 fix(context-menu): keep only one context menu open at a time
The sidebar menus (folder tree, view options) and the page menus (card,
bulk selection, page-global) are independent components that only knew
how to close themselves, so right-clicking a folder left an open card
menu next to the folder menu, and right-clicking a card left the folder
menu behind.

Add a coordinator where every menu registers a closer; opening a menu
first closes all the others. BaseContextMenu takes part automatically,
the sidebar registers its two menus, and the global contextmenu handler
now uses the coordinator instead of hiding three specific menus by hand
(which also covers right-clicks outside the content area).
2026-10-31 11:27:48 +08:00

251 lines
8.2 KiB
JavaScript

/**
* 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';
import { closeContextMenus } from './contextMenuCoordinator.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
registerGlobalEventHandlers();
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');
const pageContent = e.target.closest('.page-content');
if (!pageContent) {
// Right-click outside the content area opens no menu, but it must
// not leave one open from another region either.
closeContextMenus();
return false;
}
if (card) {
e.preventDefault();
// Context menus are mutually exclusive, including the sidebar ones.
// The menu shown below closes the rest through the same coordinator;
// this call covers the foreground case where nothing opens.
closeContextMenus();
// 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);
}
} else {
e.preventDefault();
closeContextMenus();
window.globalContextMenuInstance?.showMenu(e.clientX, e.clientY, null);
}
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();
window.globalContextMenuInstance?.hideMenu();
}
return false; // Allow other handlers to process
}, {
priority: 50,
skipWhenModalOpen: true
});
}
/**
* Register common application-wide event handlers
*/
export function registerGlobalEventHandlers() {
eventManager.removeHandler('keydown', 'global-escape');
eventManager.removeHandler('focusin', 'global-focus');
eventManager.removeHandler('click', 'global-analytics');
// 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
}
if (
window.filterManager?.filterPanel
&& !window.filterManager.filterPanel.classList.contains('hidden')
) {
window.filterManager.closeFilterPanel();
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;
}