// Search Bar Component with Autocomplete export class SearchBar { constructor(onSearch, onSelect) { this.onSearch = onSearch; this.onSelect = onSelect; this.searchInput = null; this.suggestionsContainer = null; this.allItems = []; this.filteredItems = []; this.selectedIndex = -1; this.debounceTimer = null; this.isOpen = false; this.init(); } async init() { await this.loadAllItems(); this.createSearchBar(); } async loadAllItems() { try { const response = await fetch('/modules'); const data = await response.json(); this.allItems = []; // Add builtins if (data.builtins && data.builtins.length > 0) { data.builtins.forEach(item => { this.allItems.push({ name: item.name, fullName: item.full_name || `builtins.${item.name}`, type: 'builtin', display: item.name }); }); } // Add modules if (data.modules && data.modules.length > 0) { data.modules.forEach(item => { this.allItems.push({ name: item.name, fullName: item.name, type: 'module', display: item.name }); }); } } catch (error) { console.error('Error loading items:', error); this.allItems = []; } } createSearchBar() { const searchForm = document.querySelector('.search-form'); if (!searchForm) return; // Create search input const searchWrapper = document.createElement('div'); searchWrapper.className = 'search-bar-wrapper'; const searchIcon = document.createElement('span'); searchIcon.className = 'search-icon'; searchIcon.innerHTML = '🔍'; searchIcon.setAttribute('aria-hidden', 'true'); this.searchInput = document.createElement('input'); this.searchInput.type = 'text'; this.searchInput.className = 'search-bar-input'; this.searchInput.placeholder = 'Search Python objects, modules, builtins...'; this.searchInput.autocomplete = 'off'; this.searchInput.setAttribute('aria-label', 'Search Python documentation'); // Create suggestions dropdown this.suggestionsContainer = document.createElement('div'); this.suggestionsContainer.className = 'search-suggestions'; this.suggestionsContainer.id = 'searchSuggestions'; searchWrapper.appendChild(searchIcon); searchWrapper.appendChild(this.searchInput); searchWrapper.appendChild(this.suggestionsContainer); // Replace the old input group const oldInputGroup = searchForm.querySelector('.input-group'); if (oldInputGroup) { oldInputGroup.replaceWith(searchWrapper); } else { searchForm.insertBefore(searchWrapper, searchForm.firstChild); } this.setupEventListeners(); } setupEventListeners() { // Debounced search on input this.searchInput.addEventListener('input', (e) => { clearTimeout(this.debounceTimer); const query = e.target.value.trim(); if (query.length === 0) { this.hideSuggestions(); return; } this.debounceTimer = setTimeout(() => { this.filterItems(query); this.showSuggestions(); }, 150); }); // Handle keyboard navigation this.searchInput.addEventListener('keydown', (e) => { if (!this.isOpen || this.filteredItems.length === 0) { if (e.key === 'Enter' && this.searchInput.value.trim()) { this.performSearch(this.searchInput.value.trim()); } return; } switch (e.key) { case 'ArrowDown': e.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredItems.length - 1); this.updateSelection(); break; case 'ArrowUp': e.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, -1); this.updateSelection(); break; case 'Enter': e.preventDefault(); if (this.selectedIndex >= 0 && this.selectedIndex < this.filteredItems.length) { this.selectItem(this.filteredItems[this.selectedIndex]); } else if (this.searchInput.value.trim()) { this.performSearch(this.searchInput.value.trim()); } break; case 'Escape': this.hideSuggestions(); break; } }); // Close suggestions when clicking outside document.addEventListener('click', (e) => { if (!this.searchInput.contains(e.target) && !this.suggestionsContainer.contains(e.target)) { this.hideSuggestions(); } }); // Auto-search on blur if there's a value and user typed something let hasTyped = false; this.searchInput.addEventListener('input', () => { hasTyped = true; }); this.searchInput.addEventListener('blur', () => { // Delay to allow click events on suggestions setTimeout(() => { const query = this.searchInput.value.trim(); if (query && hasTyped && !this.isOpen) { this.performSearch(query); } hasTyped = false; }, 200); }); } filterItems(query) { const lowerQuery = query.toLowerCase(); this.filteredItems = this.allItems .filter(item => { const nameMatch = item.name.toLowerCase().includes(lowerQuery); const fullNameMatch = item.fullName.toLowerCase().includes(lowerQuery); return nameMatch || fullNameMatch; }) .slice(0, 10); // Limit to 10 suggestions this.selectedIndex = -1; } showSuggestions() { if (this.filteredItems.length === 0) { this.hideSuggestions(); return; } this.isOpen = true; this.suggestionsContainer.innerHTML = ''; this.suggestionsContainer.classList.add('active'); this.filteredItems.forEach((item, index) => { const suggestion = document.createElement('div'); suggestion.className = 'search-suggestion'; suggestion.dataset.index = index; const icon = item.type === 'builtin' ? '⚡' : '📦'; suggestion.innerHTML = ` ${icon} ${this.highlightMatch(item.name, this.searchInput.value)} ${item.type} `; suggestion.addEventListener('click', () => { this.selectItem(item); }); suggestion.addEventListener('mouseenter', () => { this.selectedIndex = index; this.updateSelection(); }); this.suggestionsContainer.appendChild(suggestion); }); } highlightMatch(text, query) { if (!query) return text; const lowerText = text.toLowerCase(); const lowerQuery = query.toLowerCase(); const index = lowerText.indexOf(lowerQuery); if (index === -1) return text; const before = text.substring(0, index); const match = text.substring(index, index + query.length); const after = text.substring(index + query.length); return `${before}${match}${after}`; } updateSelection() { const suggestions = this.suggestionsContainer.querySelectorAll('.search-suggestion'); suggestions.forEach((suggestion, index) => { if (index === this.selectedIndex) { suggestion.classList.add('selected'); suggestion.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } else { suggestion.classList.remove('selected'); } }); } selectItem(item) { this.searchInput.value = item.fullName; this.hideSuggestions(); if (this.onSelect) { this.onSelect(item.fullName); } this.performSearch(item.fullName); } performSearch(query) { if (!query || query.trim().length === 0) return; if (this.onSearch) { this.onSearch(query.trim()); } } hideSuggestions() { this.isOpen = false; this.selectedIndex = -1; this.suggestionsContainer.classList.remove('active'); this.suggestionsContainer.innerHTML = ''; } setValue(value) { if (this.searchInput) { this.searchInput.value = value; } } }