mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-18 05:39:50 +08:00
616 lines
19 KiB
JavaScript
616 lines
19 KiB
JavaScript
/**
|
|
* SearchInterface Module
|
|
* Handles the creation and management of the search UI
|
|
*/
|
|
|
|
class SearchInterface {
|
|
constructor(options) {
|
|
this.options = options;
|
|
this.isVisible = false;
|
|
this.modal = null;
|
|
this.input = null;
|
|
this.resultsContainer = null;
|
|
this.statsContainer = null;
|
|
}
|
|
|
|
/**
|
|
* Create the search interface elements
|
|
*/
|
|
create() {
|
|
// Check if we're on the search page
|
|
if (this.isSearchPage()) {
|
|
this.enhanceSearchPage();
|
|
} else {
|
|
// On other pages, create the modal for search functionality
|
|
this.createModal();
|
|
this.enhanceSearchButton();
|
|
}
|
|
console.log('✅ Search interface created');
|
|
}
|
|
|
|
/**
|
|
* Check if we're on the search page
|
|
*/
|
|
isSearchPage() {
|
|
return window.location.pathname.includes('/search') ||
|
|
window.location.pathname.includes('/search.html') ||
|
|
window.location.pathname.endsWith('search/') ||
|
|
document.querySelector('#search-results') !== null ||
|
|
document.querySelector('.search-page') !== null ||
|
|
document.querySelector('form[action*="search"]') !== null ||
|
|
document.title.toLowerCase().includes('search') ||
|
|
document.querySelector('h1')?.textContent.toLowerCase().includes('search');
|
|
}
|
|
|
|
/**
|
|
* Enhance the existing search page using the template structure
|
|
*/
|
|
enhanceSearchPage() {
|
|
console.log('🔍 Enhancing search page using existing template...');
|
|
console.log('📄 Page URL:', window.location.href);
|
|
console.log('📋 Page title:', document.title);
|
|
|
|
// Use the template's existing elements
|
|
this.input = document.querySelector('#enhanced-search-page-input');
|
|
this.resultsContainer = document.querySelector('#enhanced-search-page-results');
|
|
|
|
console.log('🔎 Template search input found:', !!this.input);
|
|
console.log('📦 Template results container found:', !!this.resultsContainer);
|
|
|
|
if (this.input && this.resultsContainer) {
|
|
console.log('✅ Using existing template structure - no additional setup needed');
|
|
// The template's JavaScript will handle everything
|
|
return;
|
|
}
|
|
|
|
// Fallback for non-template pages
|
|
console.log('⚠️ Template elements not found, falling back to generic search page detection');
|
|
this.fallbackToGenericSearchPage();
|
|
}
|
|
|
|
/**
|
|
* Fallback for pages that don't use the template
|
|
*/
|
|
fallbackToGenericSearchPage() {
|
|
// Find existing search elements on generic pages
|
|
this.input = document.querySelector('#searchbox input[type="text"]') ||
|
|
document.querySelector('input[name="q"]') ||
|
|
document.querySelector('.search input[type="text"]');
|
|
|
|
// Find or create results container
|
|
this.resultsContainer = document.querySelector('#search-results') ||
|
|
document.querySelector('.search-results') ||
|
|
this.createResultsContainer();
|
|
|
|
// Create stats container
|
|
this.statsContainer = this.createStatsContainer();
|
|
|
|
// Hide default Sphinx search results if they exist
|
|
this.hideDefaultResults();
|
|
|
|
// Initialize with empty state
|
|
this.showEmptyState();
|
|
|
|
console.log('✅ Generic search page enhanced');
|
|
}
|
|
|
|
/**
|
|
* Create results container if it doesn't exist
|
|
*/
|
|
createResultsContainer() {
|
|
const container = document.createElement('div');
|
|
container.id = 'enhanced-search-results';
|
|
container.className = 'enhanced-search-results';
|
|
|
|
// Add basic styling to ensure proper positioning
|
|
container.style.cssText = `
|
|
width: 100%;
|
|
max-width: none;
|
|
margin: 1rem 0;
|
|
clear: both;
|
|
position: relative;
|
|
z-index: 1;
|
|
`;
|
|
|
|
// Find the best place to insert it within the main content area
|
|
const insertLocation = this.findBestInsertLocation();
|
|
|
|
if (insertLocation.parent && insertLocation.method === 'append') {
|
|
insertLocation.parent.appendChild(container);
|
|
console.log(`✅ Results container added to: ${insertLocation.parent.className || insertLocation.parent.tagName}`);
|
|
} else if (insertLocation.parent && insertLocation.method === 'after') {
|
|
insertLocation.parent.insertAdjacentElement('afterend', container);
|
|
console.log(`✅ Results container added after: ${insertLocation.parent.className || insertLocation.parent.tagName}`);
|
|
} else {
|
|
// Last resort - create a wrapper in main content
|
|
this.createInMainContent(container);
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Find the best location to insert search results
|
|
*/
|
|
findBestInsertLocation() {
|
|
// Try to find existing search-related elements first
|
|
let searchResults = document.querySelector('.search-results, #search-results');
|
|
if (searchResults) {
|
|
return { parent: searchResults, method: 'append' };
|
|
}
|
|
|
|
// Look for search form and place results after it
|
|
let searchForm = document.querySelector('#searchbox, .search form, form[action*="search"]');
|
|
if (searchForm) {
|
|
return { parent: searchForm, method: 'after' };
|
|
}
|
|
|
|
// Look for main content containers (common Sphinx/theme classes)
|
|
const mainSelectors = [
|
|
'.document .body',
|
|
'.document .documentwrapper',
|
|
'.content',
|
|
'.main-content',
|
|
'.page-content',
|
|
'main',
|
|
'.container .row .col',
|
|
'.rst-content',
|
|
'.body-content'
|
|
];
|
|
|
|
for (const selector of mainSelectors) {
|
|
const element = document.querySelector(selector);
|
|
if (element) {
|
|
return { parent: element, method: 'append' };
|
|
}
|
|
}
|
|
|
|
// Try to find any container that's not the body
|
|
const anyContainer = document.querySelector('.container, .wrapper, .page, #content');
|
|
if (anyContainer) {
|
|
return { parent: anyContainer, method: 'append' };
|
|
}
|
|
|
|
return { parent: null, method: null };
|
|
}
|
|
|
|
/**
|
|
* Create container in main content as last resort
|
|
*/
|
|
createInMainContent(container) {
|
|
// Create a wrapper section
|
|
const wrapper = document.createElement('section');
|
|
wrapper.className = 'search-page-content';
|
|
wrapper.style.cssText = `
|
|
max-width: 800px;
|
|
margin: 2rem auto;
|
|
padding: 0 1rem;
|
|
`;
|
|
|
|
// Add a title
|
|
const title = document.createElement('h1');
|
|
title.textContent = 'Search Results';
|
|
title.style.cssText = 'margin-bottom: 1rem;';
|
|
wrapper.appendChild(title);
|
|
|
|
// Add the container
|
|
wrapper.appendChild(container);
|
|
|
|
// Insert into body, but with proper styling
|
|
document.body.appendChild(wrapper);
|
|
|
|
console.log('⚠️ Created search results in body with wrapper - consider improving page structure');
|
|
}
|
|
|
|
/**
|
|
* Create stats container
|
|
*/
|
|
createStatsContainer() {
|
|
const container = document.createElement('div');
|
|
container.className = 'enhanced-search-stats';
|
|
container.style.cssText = 'margin: 1rem 0; font-size: 0.9rem; color: #666;';
|
|
|
|
// Insert before results
|
|
if (this.resultsContainer && this.resultsContainer.parentNode) {
|
|
this.resultsContainer.parentNode.insertBefore(container, this.resultsContainer);
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Hide default Sphinx search results
|
|
*/
|
|
hideDefaultResults() {
|
|
// Hide default search results that Sphinx might show
|
|
const defaultResults = document.querySelectorAll(
|
|
'.search-summary, .search li, #search-results .search, .searchresults'
|
|
);
|
|
defaultResults.forEach(el => {
|
|
el.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create the main search modal (legacy - kept for compatibility)
|
|
*/
|
|
createModal() {
|
|
// Enhanced search modal
|
|
const modal = document.createElement('div');
|
|
modal.id = 'enhanced-search-modal';
|
|
modal.className = 'enhanced-search-modal';
|
|
modal.innerHTML = `
|
|
<div class="enhanced-search-backdrop"></div>
|
|
<div class="enhanced-search-container">
|
|
<div class="enhanced-search-header">
|
|
<div class="enhanced-search-input-wrapper">
|
|
<i class="fa-solid fa-magnifying-glass search-icon"></i>
|
|
<input
|
|
type="text"
|
|
id="enhanced-search-input"
|
|
class="enhanced-search-input"
|
|
placeholder="${this.options.placeholder}"
|
|
autofocus
|
|
>
|
|
<button class="enhanced-search-close" title="Close search">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
<div class="enhanced-search-stats"></div>
|
|
</div>
|
|
<div class="enhanced-search-results"></div>
|
|
<div class="enhanced-search-footer">
|
|
<div class="enhanced-search-shortcuts">
|
|
<span><kbd>↵</kbd> Open</span>
|
|
<span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
|
|
<span><kbd>Esc</kbd> Close</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// Cache references
|
|
this.modal = modal;
|
|
this.input = modal.querySelector('#enhanced-search-input');
|
|
this.resultsContainer = modal.querySelector('.enhanced-search-results');
|
|
this.statsContainer = modal.querySelector('.enhanced-search-stats');
|
|
|
|
// Add event handlers for closing the modal
|
|
const closeButton = modal.querySelector('.enhanced-search-close');
|
|
const backdrop = modal.querySelector('.enhanced-search-backdrop');
|
|
|
|
if (closeButton) {
|
|
closeButton.addEventListener('click', () => this.hideModal());
|
|
}
|
|
|
|
if (backdrop) {
|
|
backdrop.addEventListener('click', () => this.hideModal());
|
|
}
|
|
|
|
// Hide modal by default
|
|
modal.style.display = 'none';
|
|
|
|
// Initialize with empty state
|
|
this.showEmptyState();
|
|
}
|
|
|
|
/**
|
|
* Replace or enhance existing search button to show modal
|
|
*/
|
|
enhanceSearchButton() {
|
|
// Find existing search button/form
|
|
const searchForm = document.querySelector('#searchbox form') ||
|
|
document.querySelector('.search form') ||
|
|
document.querySelector('form[action*="search"]');
|
|
|
|
if (searchForm) {
|
|
// Prevent form submission and show modal instead
|
|
searchForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.showModal();
|
|
});
|
|
console.log('✅ Search form enhanced to show modal');
|
|
}
|
|
|
|
// Find search button specifically and enhance it
|
|
const existingButton = document.querySelector('.search-button-field, .search-button__button');
|
|
if (existingButton) {
|
|
existingButton.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.showModal();
|
|
});
|
|
console.log('✅ Search button enhanced to show modal');
|
|
}
|
|
|
|
// Also look for search input fields and enhance them
|
|
const searchInput = document.querySelector('#searchbox input[type="text"]') ||
|
|
document.querySelector('.search input[type="text"]');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('focus', () => {
|
|
this.showModal();
|
|
});
|
|
console.log('✅ Search input enhanced to show modal on focus');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show the search interface (focus input or show modal)
|
|
*/
|
|
show() {
|
|
if (this.modal) {
|
|
this.showModal();
|
|
} else if (this.input) {
|
|
this.input.focus();
|
|
this.input.select();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide the search interface (hide modal or blur input)
|
|
*/
|
|
hide() {
|
|
if (this.modal) {
|
|
this.hideModal();
|
|
} else if (this.input) {
|
|
this.input.blur();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show the modal
|
|
*/
|
|
showModal() {
|
|
if (this.modal) {
|
|
this.modal.style.display = 'flex';
|
|
this.modal.classList.add('visible');
|
|
this.isVisible = true;
|
|
// Focus the input after a brief delay to ensure modal is visible
|
|
setTimeout(() => {
|
|
if (this.input) {
|
|
this.input.focus();
|
|
this.input.select();
|
|
}
|
|
}, 100);
|
|
console.log('🔍 Search modal shown');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide the modal
|
|
*/
|
|
hideModal() {
|
|
if (this.modal) {
|
|
this.modal.classList.remove('visible');
|
|
this.isVisible = false;
|
|
// Hide after animation completes
|
|
setTimeout(() => {
|
|
if (this.modal) {
|
|
this.modal.style.display = 'none';
|
|
}
|
|
}, 200);
|
|
// Clear any search results
|
|
this.showEmptyState();
|
|
console.log('🔍 Search modal hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the search input element
|
|
*/
|
|
getInput() {
|
|
return this.input;
|
|
}
|
|
|
|
/**
|
|
* Get the results container
|
|
*/
|
|
getResultsContainer() {
|
|
return this.resultsContainer;
|
|
}
|
|
|
|
/**
|
|
* Get the stats container
|
|
*/
|
|
getStatsContainer() {
|
|
return this.statsContainer;
|
|
}
|
|
|
|
/**
|
|
* Get the modal element
|
|
*/
|
|
getModal() {
|
|
return this.modal;
|
|
}
|
|
|
|
/**
|
|
* Check if modal is visible
|
|
*/
|
|
isModalVisible() {
|
|
return this.isVisible && this.modal && this.modal.style.display !== 'none';
|
|
}
|
|
|
|
/**
|
|
* Show empty state in results
|
|
*/
|
|
showEmptyState() {
|
|
if (this.resultsContainer) {
|
|
this.resultsContainer.innerHTML = `
|
|
<div class="search-empty-state">
|
|
<i class="fa-solid fa-magnifying-glass"></i>
|
|
<p>Start typing to search documentation...</p>
|
|
<div class="search-tips">
|
|
<strong>Search tips:</strong>
|
|
<ul>
|
|
<li>Use specific terms for better results</li>
|
|
<li>Try different keywords if you don't find what you're looking for</li>
|
|
<li>Search includes titles, content, headings, and tags</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show no results state
|
|
*/
|
|
showNoResults(query) {
|
|
if (this.resultsContainer) {
|
|
this.resultsContainer.innerHTML = `
|
|
<div class="search-no-results">
|
|
<i class="fa-solid fa-search-minus"></i>
|
|
<p>No results found for "<strong>${this.escapeHtml(query)}</strong>"</p>
|
|
<div class="search-suggestions">
|
|
<strong>Try:</strong>
|
|
<ul>
|
|
<li>Checking for typos</li>
|
|
<li>Using different or more general terms</li>
|
|
<li>Using fewer keywords</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show error state
|
|
*/
|
|
showError(message = 'Search temporarily unavailable') {
|
|
if (this.resultsContainer) {
|
|
this.resultsContainer.innerHTML = `
|
|
<div class="search-error">
|
|
<i class="fa-solid fa-exclamation-triangle"></i>
|
|
<p>${this.escapeHtml(message)}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update search statistics
|
|
*/
|
|
updateStats(query, count) {
|
|
if (this.statsContainer) {
|
|
if (count > 0) {
|
|
this.statsContainer.innerHTML = `${count} result${count !== 1 ? 's' : ''} for "${this.escapeHtml(query)}"`;
|
|
} else {
|
|
this.statsContainer.innerHTML = `No results for "${this.escapeHtml(query)}"`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear search statistics
|
|
*/
|
|
clearStats() {
|
|
if (this.statsContainer) {
|
|
this.statsContainer.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current search query
|
|
*/
|
|
getQuery() {
|
|
return this.input ? this.input.value.trim() : '';
|
|
}
|
|
|
|
/**
|
|
* Set search query
|
|
*/
|
|
setQuery(query) {
|
|
if (this.input) {
|
|
this.input.value = query;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear search query
|
|
*/
|
|
clearQuery() {
|
|
if (this.input) {
|
|
this.input.value = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Focus the search input
|
|
*/
|
|
focusInput() {
|
|
if (this.input) {
|
|
this.input.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get close button for event binding
|
|
*/
|
|
getCloseButton() {
|
|
return this.modal ? this.modal.querySelector('.enhanced-search-close') : null;
|
|
}
|
|
|
|
/**
|
|
* Get backdrop for event binding
|
|
*/
|
|
getBackdrop() {
|
|
return this.modal ? this.modal.querySelector('.enhanced-search-backdrop') : null;
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* Add CSS class to modal
|
|
*/
|
|
addModalClass(className) {
|
|
if (this.modal) {
|
|
this.modal.classList.add(className);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove CSS class from modal
|
|
*/
|
|
removeModalClass(className) {
|
|
if (this.modal) {
|
|
this.modal.classList.remove(className);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if modal has class
|
|
*/
|
|
hasModalClass(className) {
|
|
return this.modal ? this.modal.classList.contains(className) : false;
|
|
}
|
|
|
|
/**
|
|
* Destroy the search interface
|
|
*/
|
|
destroy() {
|
|
if (this.modal) {
|
|
this.modal.remove();
|
|
this.modal = null;
|
|
this.input = null;
|
|
this.resultsContainer = null;
|
|
this.statsContainer = null;
|
|
}
|
|
this.isVisible = false;
|
|
}
|
|
}
|
|
|
|
// Make SearchInterface available globally
|
|
window.SearchInterface = SearchInterface;
|