Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions src/components/overrides/Sidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,272 @@ const [product, module] = Astro.url.pathname.split("/").filter(Boolean);
</span>
</a>

<!-- Search Input -->
<div class="relative" style="margin: 1rem var(--sl-sidebar-item-padding-inline) 0.75rem;">
<input
type="text"
id="sidebar-search"
placeholder="Search documentation..."
class="w-full px-3 pr-8 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-orange-500 dark:focus:border-orange-500 transition-colors duration-200"
/>
<button
id="sidebar-search-clear"
class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-transparent border-none text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 cursor-pointer text-xl leading-none p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
style="display: none;"
>×</button>
</div>

<!-- No Results Message -->
<div
id="sidebar-no-results"
class="text-sm text-gray-500 dark:text-gray-400 text-center py-4 px-4 hidden"
style="margin: 0 var(--sl-sidebar-item-padding-inline);"
>
No results found. Try a different search term, or use our <button id="global-search-link" class="text-blue-500 dark:text-orange-500 underline hover:no-underline cursor-pointer bg-transparent border-none p-0 font-inherit">global search</button>.
</div>

<Default><slot /></Default>

<script>
function initSidebarSearch() {
const searchInput = document.getElementById('sidebar-search') as HTMLInputElement;
const clearButton = document.getElementById('sidebar-search-clear') as HTMLButtonElement;
const noResultsMessage = document.getElementById('sidebar-no-results') as HTMLElement;
const globalSearchLink = document.getElementById('global-search-link') as HTMLButtonElement;
const sidebarContent = document.querySelector('.sidebar-content');

if (!searchInput || !sidebarContent || !noResultsMessage || !globalSearchLink) return;

const originalState: Map<Element, boolean> = new Map();

// Store original state of details elements
function storeOriginalState() {
if (originalState.size === 0) {
const detailsElements = sidebarContent!.querySelectorAll('details');
detailsElements.forEach(details => {
originalState.set(details, details.open);
});
}
}


// Check if text matches all search terms (for multi-word searches)
function matchesAllTerms(text: string, searchTerms: string[]): boolean {
const lowerText = text.toLowerCase();
return searchTerms.every(term => lowerText.includes(term));
}

// Show only direct children of a folder (not recursive)
function showDirectChildren(details: HTMLDetailsElement) {
details.open = true;
const directList = details.querySelector(':scope > ul');
if (directList) {
const directChildren = directList.querySelectorAll(':scope > li');
directChildren.forEach(child => {
(child as HTMLElement).style.display = '';
});
}
}

// Show parent chain for a specific item
function showParentChain(element: Element) {
let parent = element.parentElement;
while (parent && parent !== sidebarContent) {
if (parent.tagName === 'LI') {
(parent as HTMLElement).style.display = '';
}
if (parent.tagName === 'DETAILS') {
(parent as HTMLDetailsElement).open = true;
}
parent = parent.parentElement;
}
}

// Filter sidebar items based on search query
function filterSidebarItems(query: string) {
const items = sidebarContent!.querySelectorAll('li');
const detailsElements = sidebarContent!.querySelectorAll('details');

if (!query.trim()) {
// Reset to original state
items.forEach(item => {
(item as HTMLElement).style.display = '';
});

// Restore original details state
detailsElements.forEach(details => {
const originalOpen = originalState.get(details);
if (originalOpen !== undefined) {
(details as HTMLDetailsElement).open = originalOpen;
}
});

// Hide no results message
noResultsMessage.classList.add('hidden');

clearButton.style.display = 'none';
return;
}

clearButton.style.display = 'block';

// Split search query into terms for more precise matching
const searchTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0);

// First, hide all items and close all details
items.forEach(item => {
(item as HTMLElement).style.display = 'none';
});
detailsElements.forEach(details => {
(details as HTMLDetailsElement).open = false;
});

// Track what we've matched to avoid duplicates
const matchedItems = new Set<Element>();

// 1. Check for folder/subfolder matches first (highest priority)
detailsElements.forEach(details => {
const summary = details.querySelector('summary');
if (summary) {
const summaryText = summary.textContent || '';

if (matchesAllTerms(summaryText, searchTerms)) {
// This is a folder match - show the folder and its direct children
const parentLi = details.closest('li');
if (parentLi && !matchedItems.has(parentLi)) {
(parentLi as HTMLElement).style.display = '';
showDirectChildren(details);
showParentChain(parentLi);
matchedItems.add(parentLi);
}
}
}
});

// 2. Check for specific page matches (show page + parent chain)
items.forEach(item => {
if (matchedItems.has(item)) return; // Skip if already matched as folder

const link = item.querySelector('a');
const summary = item.querySelector('summary');

// Skip if this is a folder (has summary) - those are handled above
if (summary) return;

if (link) {
const linkText = link.textContent || '';

if (matchesAllTerms(linkText, searchTerms)) {
// This is a specific page match - show page + parent chain
(item as HTMLElement).style.display = '';
showParentChain(item);
matchedItems.add(item);
}
}
});

// 3. Fallback: if no exact matches, show partial matches (less specific)
if (matchedItems.size === 0) {
items.forEach(item => {
const textContent = item.textContent?.toLowerCase() || '';
const link = item.querySelector('a');
const linkText = link?.textContent?.toLowerCase() || '';
const summary = item.querySelector('summary');
const summaryText = summary?.textContent?.toLowerCase() || '';

// Check if any search term is found (partial matching)
const hasPartialMatch = searchTerms.some(term =>
textContent.includes(term) || linkText.includes(term) || summaryText.includes(term)
);

if (hasPartialMatch) {
(item as HTMLElement).style.display = '';

// If it's a folder, show direct children only
if (summary) {
const details = item.querySelector('details');
if (details) {
showDirectChildren(details);
}
}

showParentChain(item);
matchedItems.add(item);
}
});
}

// Show/hide no results message based on matches
if (matchedItems.size === 0) {
noResultsMessage.classList.remove('hidden');
} else {
noResultsMessage.classList.add('hidden');
}
}

// Event listeners
searchInput.addEventListener('input', (e) => {
storeOriginalState();
const query = (e.target as HTMLInputElement).value;
filterSidebarItems(query);
});

clearButton.addEventListener('click', () => {
searchInput.value = '';
filterSidebarItems('');
searchInput.focus();
});

// Clear search on Escape key
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
filterSidebarItems('');
}
});

// Global search link click handler
globalSearchLink.addEventListener('click', () => {
const currentQuery = searchInput.value.trim();
if (currentQuery) {
// Try multiple selectors for DocSearch
const docSearchButton = document.querySelector('#docsearch button') as HTMLButtonElement ||
document.querySelector('.DocSearch-Button') as HTMLButtonElement ||
document.querySelector('[data-docsearch-button]') as HTMLButtonElement;

if (docSearchButton) {
// Click the DocSearch button to open the modal
docSearchButton.click();

// Wait for modal to open and set the search term
setTimeout(() => {
const searchInput = document.querySelector('.DocSearch-Input') as HTMLInputElement ||
document.querySelector('#docsearch-input') as HTMLInputElement ||
document.querySelector('[data-docsearch-input]') as HTMLInputElement;

if (searchInput) {
searchInput.value = currentQuery;
searchInput.focus();
// Trigger search
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}, 100);
}
}
});
}

// Initialize when DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSidebarSearch);
} else {
initSidebarSearch();
}

// Re-initialize on navigation (for SPA-like behavior)
document.addEventListener('astro:page-load', initSidebarSearch);
</script>

<style is:global>
:root {
.sidebar-content {
Expand Down
Loading