// 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;
}
}
}