/** * Docs Search Enhancement Module * Provides faceted search, filters, history, and keyboard navigation * CSP Compliant - No inline scripts or event handlers */ (function() { 'use strict'; // Configuration const CONFIG = { DEBOUNCE_DELAY: 300, MAX_SEARCH_HISTORY: 10, SEARCH_HISTORY_KEY: 'tractatus_search_history', MIN_QUERY_LENGTH: 2 }; // State let searchTimeout = null; let currentFilters = { query: '', quadrant: '', persistence: '', audience: '' }; let searchHistory = []; let selectedResultIndex = -1; let searchResults = []; // DOM Elements const elements = { searchInput: null, quadrantFilter: null, persistenceFilter: null, audienceFilter: null, clearFiltersBtn: null, searchTipsBtn: null, searchTipsModal: null, searchTipsCloseBtn: null, searchResultsPanel: null, searchResultsList: null, closeSearchResults: null, searchResultsSummary: null, searchResultsCount: null, searchHistoryContainer: null, searchHistory: null, searchModal: null, openSearchModalBtn: null, searchModalCloseBtn: null, searchResultsModal: null, searchResultsListModal: null }; /** * Initialize the search enhancement module */ function init() { // Get DOM elements elements.searchInput = document.getElementById('docs-search-input'); elements.quadrantFilter = document.getElementById('filter-quadrant'); elements.persistenceFilter = document.getElementById('filter-persistence'); elements.audienceFilter = document.getElementById('filter-audience'); elements.clearFiltersBtn = document.getElementById('clear-filters-btn'); elements.searchTipsBtn = document.getElementById('search-tips-btn'); elements.searchTipsModal = document.getElementById('search-tips-modal'); elements.searchTipsCloseBtn = document.getElementById('search-tips-close-btn'); elements.searchResultsPanel = document.getElementById('search-results-panel'); elements.searchResultsList = document.getElementById('search-results-list'); elements.closeSearchResults = document.getElementById('close-search-results'); elements.searchResultsSummary = document.getElementById('search-results-summary'); elements.searchResultsCount = document.getElementById('search-results-count'); elements.searchHistoryContainer = document.getElementById('search-history-container'); elements.searchHistory = document.getElementById('search-history'); elements.searchModal = document.getElementById('search-modal'); elements.openSearchModalBtn = document.getElementById('open-search-modal-btn'); elements.searchModalCloseBtn = document.getElementById('search-modal-close-btn'); elements.searchResultsModal = document.getElementById('search-results-modal'); elements.searchResultsListModal = document.getElementById('search-results-list-modal'); // Check if essential elements exist if (!elements.searchInput || !elements.searchModal) { console.warn('Search elements not found - search enhancement disabled'); return; } // Load search history from localStorage loadSearchHistory(); // Attach event listeners attachEventListeners(); // Display search history if available renderSearchHistory(); } /** * Attach event listeners (CSP compliant - no inline handlers) */ function attachEventListeners() { // Search modal open/close if (elements.openSearchModalBtn) { elements.openSearchModalBtn.addEventListener('click', openSearchModal); } if (elements.searchModalCloseBtn) { elements.searchModalCloseBtn.addEventListener('click', closeSearchModal); } if (elements.searchModal) { elements.searchModal.addEventListener('click', function(e) { if (e.target === elements.searchModal) { closeSearchModal(); } }); } // Search input - debounced if (elements.searchInput) { elements.searchInput.addEventListener('input', handleSearchInput); elements.searchInput.addEventListener('keydown', handleSearchKeydown); } // Filter dropdowns if (elements.quadrantFilter) { elements.quadrantFilter.addEventListener('change', handleFilterChange); } if (elements.persistenceFilter) { elements.persistenceFilter.addEventListener('change', handleFilterChange); } if (elements.audienceFilter) { elements.audienceFilter.addEventListener('change', handleFilterChange); } // Clear filters button if (elements.clearFiltersBtn) { elements.clearFiltersBtn.addEventListener('click', clearFilters); } // Search tips button if (elements.searchTipsBtn) { elements.searchTipsBtn.addEventListener('click', openSearchTipsModal); } if (elements.searchTipsCloseBtn) { elements.searchTipsCloseBtn.addEventListener('click', closeSearchTipsModal); } if (elements.searchTipsModal) { elements.searchTipsModal.addEventListener('click', function(e) { if (e.target === elements.searchTipsModal) { closeSearchTipsModal(); } }); } // Close search results if (elements.closeSearchResults) { elements.closeSearchResults.addEventListener('click', closeSearchResultsPanel); } // Global keyboard shortcuts document.addEventListener('keydown', handleGlobalKeydown); // Escape key to close modals document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeSearchTipsModal(); closeSearchModal(); } }); } /** * Handle search input with debounce */ function handleSearchInput(e) { const query = e.target.value.trim(); // Clear previous timeout if (searchTimeout) { clearTimeout(searchTimeout); } // Debounce search searchTimeout = setTimeout(() => { currentFilters.query = query; performSearch(); }, CONFIG.DEBOUNCE_DELAY); } /** * Handle keyboard navigation in search input */ function handleSearchKeydown(e) { if (e.key === 'Enter') { e.preventDefault(); const query = e.target.value.trim(); if (query) { currentFilters.query = query; performSearch(); } } else if (e.key === 'Escape') { closeSearchResultsPanel(); closeSearchModal(); e.target.blur(); } else if (e.key === 'ArrowDown') { e.preventDefault(); navigateResults('down'); } else if (e.key === 'ArrowUp') { e.preventDefault(); navigateResults('up'); } } /** * Handle filter changes */ function handleFilterChange() { currentFilters.quadrant = elements.quadrantFilter ? elements.quadrantFilter.value : ''; currentFilters.persistence = elements.persistenceFilter ? elements.persistenceFilter.value : ''; currentFilters.audience = elements.audienceFilter ? elements.audienceFilter.value : ''; performSearch(); } /** * Clear all filters */ function clearFilters() { if (elements.searchInput) elements.searchInput.value = ''; if (elements.quadrantFilter) elements.quadrantFilter.value = ''; if (elements.persistenceFilter) elements.persistenceFilter.value = ''; if (elements.audienceFilter) elements.audienceFilter.value = ''; currentFilters = { query: '', quadrant: '', persistence: '', audience: '' }; closeSearchResultsPanel(); } /** * Perform search with current filters */ async function performSearch() { const { query, quadrant, persistence, audience } = currentFilters; // If no query and no filters, don't search if (!query && !quadrant && !persistence && !audience) { closeSearchResultsPanel(); return; } // Build query params const params = new URLSearchParams(); if (query) params.append('q', query); if (quadrant) params.append('quadrant', quadrant); if (persistence) params.append('persistence', persistence); if (audience) params.append('audience', audience); try { const startTime = performance.now(); const response = await fetch(`/api/documents/search?${params.toString()}`); const endTime = performance.now(); const duration = Math.round(endTime - startTime); const data = await response.json(); if (data.success) { searchResults = data.documents || []; renderSearchResults(data, duration); // Save to search history if query exists if (query) { addToSearchHistory(query); } } else { showError('Search failed. Please try again.'); } } catch (error) { console.error('Search error:', error); showError('Search failed. Please check your connection.'); } } /** * Render search results */ function renderSearchResults(data, duration) { // Use modal list if available, otherwise fall back to panel list const targetList = elements.searchResultsListModal || elements.searchResultsList; const targetContainer = elements.searchResultsModal || elements.searchResultsPanel; if (!targetList) return; const { documents, count, total, filters } = data; // Show results container if (targetContainer) { targetContainer.classList.remove('hidden'); } // Update summary if (elements.searchResultsSummary && elements.searchResultsCount) { elements.searchResultsSummary.classList.remove('hidden'); let summaryText = `Found ${total} document${total !== 1 ? 's' : ''}`; if (duration) { summaryText += ` (${duration}ms)`; } const activeFilters = []; if (filters.quadrant) activeFilters.push(`Quadrant: ${filters.quadrant}`); if (filters.persistence) activeFilters.push(`Persistence: ${filters.persistence}`); if (filters.audience) activeFilters.push(`Audience: ${filters.audience}`); if (activeFilters.length > 0) { summaryText += ` • Filters: ${activeFilters.join(', ')}`; } elements.searchResultsCount.textContent = summaryText; } // Render results if (documents.length === 0) { targetList.innerHTML = `
No documents found
Try adjusting your search terms or filters
${message}