/** * Search Modal Alpine.js Component * * Provides package selection, search mode switching, and results display * for the search modal. */ document.addEventListener('alpine:init', () => { /** * Search Modal Component * * Usage: * * ... * */ Alpine.data('searchModal', () => ({ // Package selector state selectedPackages: [], // Search state searchMode: 'text', searchModeLabel: 'Text', query: '', // Results state results: null, isSearching: false, error: null, // Initialize on modal open init() { // Load reviewed packages by default this.loadInitialSelection(); // Watch for modal open to focus searchbar this.$watch('$el.open', (open) => { if (open) { setTimeout(() => { this.$refs.searchbar.focus(); }, 320); } }); }, loadInitialSelection() { // Select all reviewed packages by default const menuItems = this.$refs.packageDropdown.querySelectorAll('li'); for (const item of menuItems) { // Stop at 'Unreviewed Packages' section if (item.classList.contains('menu-title') && item.textContent.trim() === 'Unreviewed Packages') { break; } const packageOption = item.querySelector('.package-option'); if (packageOption) { this.selectedPackages.push({ url: packageOption.dataset.packageUrl, name: packageOption.dataset.packageName }); } } }, togglePackage(url, name) { const index = this.selectedPackages.findIndex(pkg => pkg.url === url); if (index !== -1) { this.selectedPackages.splice(index, 1); } else { this.selectedPackages.push({ url, name }); } }, removePackage(url) { const index = this.selectedPackages.findIndex(pkg => pkg.url === url); if (index !== -1) { this.selectedPackages.splice(index, 1); } }, isPackageSelected(url) { return this.selectedPackages.some(pkg => pkg.url === url); }, setSearchMode(mode, label) { this.searchMode = mode; this.searchModeLabel = label; this.$refs.modeDropdown.hidePopover(); }, async performSearch(serverBase) { if (!this.query.trim()) { return; } if (this.selectedPackages.length < 1) { this.results = { error: 'no_packages' }; return; } const params = new URLSearchParams(); this.selectedPackages.forEach(pkg => params.append('packages', pkg.url)); params.append('search', this.query.trim()); params.append('mode', this.searchModeLabel.toLowerCase()); this.isSearching = true; this.results = null; this.error = null; try { const response = await fetch(`${serverBase}/search?${params.toString()}`, { method: 'GET', headers: { 'Accept': 'application/json' } }); if (!response.ok) { throw new Error('Search request failed'); } this.results = await response.json(); } catch (err) { console.error('Search error:', err); this.error = 'Search failed. Please try again.'; } finally { this.isSearching = false; } }, hasResults() { if (!this.results || this.results.error) return false; const categories = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways']; return categories.some(cat => this.results[cat] && this.results[cat].length > 0); }, reset() { this.query = ''; this.results = null; this.error = null; this.isSearching = false; } })); });