Skip to content
Closed
Changes from all 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
249 changes: 249 additions & 0 deletions src/components/overrides/Sidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,258 @@ const [product, module] = Astro.url.pathname.split("/").filter(Boolean);
</span>
</a>

<!-- Search Input -->
<div class="relative my-4">
<input
type="text"
id="sidebar-search"
placeholder="Search documentation..."
class="w-full py-2 px-2 text-sm border rounded-md transition-all duration-300 ease-in-out focus:outline-none focus:ring-2 sidebar-search-input"
style="border-color: var(--sl-color-hairline-light); background: var(--sl-color-bg-sidebar); color: var(--sl-color-text);"
/>
</div>

<!-- No Results Message -->
<div id="no-results-message" class="p-4 text-center rounded-md my-2 border" style="display: none; background-color: var(--sl-color-bg-sidebar); border-color: var(--sl-color-hairline-light);">
<p class="mb-1 font-semibold text-sm" style="color: var(--sl-color-text);">No results found</p>
<p class="m-0 text-xs" style="color: var(--sl-color-text-accent);">
Try a different search term, or
<button id="global-search-link" class="underline cursor-pointer" style="color: var(--sl-color-accent); background: none; border: none; padding: 0; font: inherit;">use our global search</button>.
</p>
</div>

<Default><slot /></Default>

<script>
/**
* Debounce delay for search input (milliseconds)
*/
const SEARCH_DEBOUNCE_DELAY = 150;

/**
* CSS selectors for sidebar elements
*/
const SELECTORS = {
sidebarContent: '.sidebar-content',
listItems: 'li',
detailsGroups: 'details',
links: 'a',
summaries: 'summary',
noResultsMessage: '#no-results-message'
} as const;


/**
* Initialize sidebar search functionality
*/
function initSidebarSearch(): void {
const searchInput = document.getElementById('sidebar-search') as HTMLInputElement;
if (!searchInput) {
console.warn('Sidebar search input not found');
return;
}

let debounceTimer: number;

// Handle search input with debouncing
searchInput.addEventListener('input', (event) => {
clearTimeout(debounceTimer);
debounceTimer = window.setTimeout(() => {
const target = event.target as HTMLInputElement;
const searchQuery = target.value.toLowerCase().trim();
filterSidebarItems(searchQuery);
}, SEARCH_DEBOUNCE_DELAY);
});

// Handle escape key to clear search
searchInput.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
searchInput.value = '';
filterSidebarItems('');
}
});
}


/**
* Check if an element's text content matches the search query
*/
function elementMatchesQuery(element: Element | null, query: string): boolean {
if (!element || !element.textContent) return false;
return element.textContent.toLowerCase().includes(query);
}

/**
* Show or hide a sidebar item
*/
function toggleItemVisibility(item: HTMLElement, isVisible: boolean): void {
item.style.display = isVisible ? '' : 'none';
}

/**
* Mark parent groups as visible and expand them
*/
function expandParentGroups(item: Element, visibleGroups: Set<Element>): void {
let currentParent = item.parentElement;

while (currentParent) {
if (currentParent.tagName === 'DETAILS') {
visibleGroups.add(currentParent);
(currentParent as HTMLDetailsElement).open = true;
}
currentParent = currentParent.parentElement;
}
}

/**
* Reset all sidebar items to visible state
*/
function showAllSidebarItems(sidebarContent: Element): void {
const allItems = sidebarContent.querySelectorAll(SELECTORS.listItems);
const allGroups = sidebarContent.querySelectorAll(SELECTORS.detailsGroups);

allItems.forEach(item => toggleItemVisibility(item as HTMLElement, true));
allGroups.forEach(group => toggleItemVisibility(group as HTMLElement, true));

// Hide no results message
const noResultsMessage = document.querySelector(SELECTORS.noResultsMessage) as HTMLElement;
if (noResultsMessage) {
noResultsMessage.style.display = 'none';
}
}

/**
* Check if a sidebar item matches the search query
*/
function itemMatchesSearch(item: Element, query: string): boolean {
// Check direct link text and nested links
const allLinks = item.querySelectorAll(SELECTORS.links);
for (const link of allLinks) {
if (elementMatchesQuery(link, query)) {
return true;
}
}

// Check group/folder names
const summary = item.querySelector(SELECTORS.summaries);
return elementMatchesQuery(summary, query);
}

/**
* Show all contents within a matching folder (including nested folders)
*/
function showAllContentsInFolder(folder: Element, visibleGroups: Set<Element>): void {
// Show all list items within this folder
const allItemsInFolder = folder.querySelectorAll(SELECTORS.listItems);
allItemsInFolder.forEach(item => {
toggleItemVisibility(item as HTMLElement, true);
});

// Show and expand all nested folders within this folder
const nestedGroups = folder.querySelectorAll(SELECTORS.detailsGroups);
nestedGroups.forEach(nestedGroup => {
visibleGroups.add(nestedGroup);
toggleItemVisibility(nestedGroup as HTMLElement, true);
(nestedGroup as HTMLDetailsElement).open = true;
});
}

/**
* Filter sidebar items based on search query
*/
function filterSidebarItems(query: string): void {
const sidebarContent = document.querySelector(SELECTORS.sidebarContent);
if (!sidebarContent) {
console.warn('Sidebar content not found');
return;
}

// If no query, show all items
if (!query) {
showAllSidebarItems(sidebarContent);
return;
}

const allItems = sidebarContent.querySelectorAll(SELECTORS.listItems);
const allGroups = sidebarContent.querySelectorAll(SELECTORS.detailsGroups);
const visibleGroups = new Set<Element>();
const itemsInMatchingFolders = new Set<Element>();

// First pass: identify matching folders and their contents
allGroups.forEach(group => {
const summary = group.querySelector(SELECTORS.summaries);
if (elementMatchesQuery(summary, query)) {
visibleGroups.add(group);
(group as HTMLDetailsElement).open = true;
showAllContentsInFolder(group, visibleGroups);
group.querySelectorAll(SELECTORS.listItems).forEach(item => itemsInMatchingFolders.add(item));
}
});

// Process each sidebar item
allItems.forEach(item => {
const shouldShowItem = itemsInMatchingFolders.has(item) || itemMatchesSearch(item, query);
toggleItemVisibility(item as HTMLElement, shouldShowItem);

if (shouldShowItem) {
expandParentGroups(item, visibleGroups);
}
});

// Handle groups visibility
allGroups.forEach(group => {
if (visibleGroups.has(group)) {
toggleItemVisibility(group as HTMLElement, true);
} else {
const hasVisibleChildren = group.querySelectorAll('li:not([style*="display: none"])').length > 0;
toggleItemVisibility(group as HTMLElement, hasVisibleChildren);
}
});

// Check if any items are visible and show/hide no results message
const visibleItems = sidebarContent.querySelectorAll('li:not([style*="display: none"])');
const noResultsMessage = document.querySelector(SELECTORS.noResultsMessage) as HTMLElement;
if (noResultsMessage) {
noResultsMessage.style.display = visibleItems.length === 0 ? 'block' : 'none';

// Set up global search link click handler
if (visibleItems.length === 0) {
const globalSearchLink = noResultsMessage.querySelector('#global-search-link') as HTMLButtonElement;
if (globalSearchLink) {
globalSearchLink.onclick = () => {
// Trigger DocSearch modal and pass the current query
const docSearchButton = document.querySelector('#docsearch button') as HTMLButtonElement;
if (docSearchButton) {
docSearchButton.click();
// Wait for modal to open and set the search query
setTimeout(() => {
const docSearchInput = document.querySelector('#docsearch-input') as HTMLInputElement;
if (docSearchInput) {
docSearchInput.value = query;
docSearchInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}, 100);
}
};
}
}
}
}

// Initialize search functionality when DOM is ready
document.addEventListener('DOMContentLoaded', initSidebarSearch);

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

<style is:global>
.sidebar-search-input:focus {
border-color: var(--sl-color-accent) !important;
--tw-ring-color: var(--sl-color-accent);
}


:root {
.sidebar-content {
--sl-color-hairline-light: #cacaca !important;
Expand Down
Loading