/** * EventHandler Module * Handles keyboard shortcuts and event management for the search interface */ class EventHandler { constructor(enhancedSearch) { this.enhancedSearch = enhancedSearch; this.searchInterface = enhancedSearch.searchInterface; this.resultRenderer = enhancedSearch.resultRenderer; this.searchEngine = enhancedSearch.searchEngine; this.utils = enhancedSearch.utils; // Track bound event listeners for cleanup this.boundListeners = new Map(); // Debounced search function this.debouncedSearch = this.utils.debounce(this.handleSearch.bind(this), 200); } /** * Bind all event listeners */ bindEvents() { this.bindInputEvents(); this.bindModalEvents(); this.bindGlobalEvents(); console.log('✅ Event handlers bound'); } /** * Bind input-related events */ bindInputEvents() { const input = this.searchInterface.getInput(); if (!input) return; // Search input const inputHandler = (e) => this.debouncedSearch(e); input.addEventListener('input', inputHandler); this.boundListeners.set('input', inputHandler); // Keyboard navigation const keydownHandler = (e) => this.handleKeyDown(e); input.addEventListener('keydown', keydownHandler); this.boundListeners.set('keydown', keydownHandler); } /** * Bind page-specific events (replaces modal events) */ bindModalEvents() { // Check if we're on the search page if (!this.searchInterface.isSearchPage()) { return; } // Get query parameter if we're on search page const urlParams = new URLSearchParams(window.location.search); const query = urlParams.get('q'); if (query) { // Perform search immediately with the query from URL setTimeout(() => { const input = this.searchInterface.getInput(); if (input) { input.value = query; this.handleSearch({ target: input }); } }, 100); } } /** * Bind global keyboard shortcuts */ bindGlobalEvents() { const globalKeyHandler = (e) => { // Ctrl+K or Cmd+K to focus search input if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); // Focus the search input if we're on the search page const searchInput = this.searchInterface.getInput(); if (searchInput) { searchInput.focus(); } else { // If not on search page, redirect to search page window.location.href = 'search.html'; } return; } }; document.addEventListener('keydown', globalKeyHandler); this.boundListeners.set('global', globalKeyHandler); } /** * Handle search input */ async handleSearch(event) { const query = event.target.value.trim(); const resultsContainer = this.searchInterface.getResultsContainer(); if (query.length < this.enhancedSearch.options.minQueryLength) { this.searchInterface.showEmptyState(); this.searchInterface.clearStats(); return; } try { // Show loading state this.resultRenderer.renderLoading(resultsContainer); // Perform search const results = this.searchEngine.search(query, this.enhancedSearch.options.maxResults); const count = results.length; // Render results this.resultRenderer.render(results, query, resultsContainer); // Update stats this.searchInterface.updateStats(query, count); // Emit search event for AI Assistant extension if available this.emitSearchEvent(query, results, count); } catch (error) { console.error('Search error:', error); this.resultRenderer.renderError(resultsContainer, 'Search temporarily unavailable'); this.searchInterface.clearStats(); } } /** * Handle keyboard navigation */ handleKeyDown(event) { const resultsContainer = this.searchInterface.getResultsContainer(); switch (event.key) { case 'ArrowDown': event.preventDefault(); this.resultRenderer.selectNext(resultsContainer); break; case 'ArrowUp': event.preventDefault(); this.resultRenderer.selectPrevious(resultsContainer); break; case 'Enter': event.preventDefault(); this.resultRenderer.activateSelected(resultsContainer); break; case 'Escape': event.preventDefault(); this.enhancedSearch.hide(); break; } } /** * Emit search event for other extensions */ emitSearchEvent(query, results, count) { if (window.AIAssistant && window.aiAssistantInstance) { const searchEvent = new CustomEvent('enhanced-search-results', { detail: { query, results, count } }); document.dispatchEvent(searchEvent); } } /** * Handle window resize */ handleResize() { // Adjust modal positioning if needed const modal = this.searchInterface.getModal(); if (modal && this.searchInterface.isModalVisible()) { // Could add responsive adjustments here } } /** * Handle focus management */ handleFocus(event) { // Trap focus within modal when visible if (this.searchInterface.isModalVisible()) { const modal = this.searchInterface.getModal(); const focusableElements = modal.querySelectorAll( 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; if (event.key === 'Tab') { if (event.shiftKey) { // Shift + Tab if (document.activeElement === firstFocusable) { event.preventDefault(); lastFocusable.focus(); } } else { // Tab if (document.activeElement === lastFocusable) { event.preventDefault(); firstFocusable.focus(); } } } } } /** * Bind additional event listeners */ bindAdditionalEvents() { // Window resize const resizeHandler = this.utils.debounce(() => this.handleResize(), 100); window.addEventListener('resize', resizeHandler); this.boundListeners.set('resize', resizeHandler); // Focus trap const focusHandler = (e) => this.handleFocus(e); document.addEventListener('keydown', focusHandler); this.boundListeners.set('focus', focusHandler); } /** * Unbind all event listeners */ unbindEvents() { // Remove input events const input = this.searchInterface.getInput(); if (input && this.boundListeners.has('input')) { input.removeEventListener('input', this.boundListeners.get('input')); input.removeEventListener('keydown', this.boundListeners.get('keydown')); } // Remove modal events const closeBtn = this.searchInterface.getCloseButton(); if (closeBtn && this.boundListeners.has('close')) { closeBtn.removeEventListener('click', this.boundListeners.get('close')); } const backdrop = this.searchInterface.getBackdrop(); if (backdrop && this.boundListeners.has('backdrop')) { backdrop.removeEventListener('click', this.boundListeners.get('backdrop')); } // Remove global events if (this.boundListeners.has('global')) { document.removeEventListener('keydown', this.boundListeners.get('global')); } if (this.boundListeners.has('resize')) { window.removeEventListener('resize', this.boundListeners.get('resize')); } if (this.boundListeners.has('focus')) { document.removeEventListener('keydown', this.boundListeners.get('focus')); } // Clear listeners map this.boundListeners.clear(); console.log('✅ Event handlers unbound'); } /** * Get event handler statistics */ getStatistics() { return { boundListeners: this.boundListeners.size, modalVisible: this.searchInterface.isModalVisible(), hasInput: !!this.searchInterface.getInput(), hasModal: !!this.searchInterface.getModal() }; } /** * Check if events are properly bound */ isReady() { return this.boundListeners.size > 0 && this.searchInterface.getInput() !== null && this.searchInterface.getModal() !== null; } } // Make EventHandler available globally window.EventHandler = EventHandler;