Skip to content

Commit 7ec0a8b

Browse files
committed
Better logic for search box.
1 parent c16c6dd commit 7ec0a8b

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

src/components/overrides/Sidebar.astro

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,294 @@ const [product, module] = Astro.url.pathname.split("/").filter(Boolean);
1616
</span>
1717
</a>
1818

19+
<!-- Search Input -->
20+
<div class="relative" style="margin: 1rem var(--sl-sidebar-item-padding-inline) 0.75rem;">
21+
<input
22+
type="text"
23+
id="sidebar-search"
24+
placeholder="Search documentation..."
25+
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"
26+
/>
27+
<button
28+
id="sidebar-search-clear"
29+
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"
30+
style="display: none;"
31+
>×</button>
32+
</div>
33+
34+
<!-- No Results Message -->
35+
<div
36+
id="sidebar-no-results"
37+
class="text-sm text-gray-500 dark:text-gray-400 text-center py-4 px-4 hidden"
38+
style="margin: 0 var(--sl-sidebar-item-padding-inline);"
39+
>
40+
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>.
41+
</div>
42+
1943
<Default><slot /></Default>
2044

45+
<script>
46+
function initSidebarSearch() {
47+
const searchInput = document.getElementById('sidebar-search') as HTMLInputElement;
48+
const clearButton = document.getElementById('sidebar-search-clear') as HTMLButtonElement;
49+
const noResultsMessage = document.getElementById('sidebar-no-results') as HTMLElement;
50+
const globalSearchLink = document.getElementById('global-search-link') as HTMLButtonElement;
51+
const sidebarContent = document.querySelector('.sidebar-content');
52+
53+
if (!searchInput || !sidebarContent || !noResultsMessage || !globalSearchLink) return;
54+
55+
let originalState: Map<Element, boolean> = new Map();
56+
let isSearchActive = false;
57+
58+
// Store original state of details elements
59+
function storeOriginalState() {
60+
if (originalState.size === 0) {
61+
const detailsElements = sidebarContent!.querySelectorAll('details');
62+
detailsElements.forEach(details => {
63+
originalState.set(details, details.open);
64+
});
65+
}
66+
}
67+
68+
// Show all contents within a folder recursively
69+
function showAllContentsInFolder(element: Element) {
70+
// Show the element itself
71+
(element as HTMLElement).style.display = '';
72+
73+
// If it's a details element, open it
74+
if (element.tagName === 'DETAILS') {
75+
(element as HTMLDetailsElement).open = true;
76+
}
77+
78+
// Recursively show all children
79+
const children = element.children;
80+
for (let i = 0; i < children.length; i++) {
81+
const child = children[i];
82+
if (child.tagName === 'LI' || child.tagName === 'DETAILS' || child.tagName === 'UL') {
83+
showAllContentsInFolder(child);
84+
}
85+
}
86+
}
87+
88+
// Check if text matches all search terms (for multi-word searches)
89+
function matchesAllTerms(text: string, searchTerms: string[]): boolean {
90+
const lowerText = text.toLowerCase();
91+
return searchTerms.every(term => lowerText.includes(term));
92+
}
93+
94+
// Show only direct children of a folder (not recursive)
95+
function showDirectChildren(details: HTMLDetailsElement) {
96+
details.open = true;
97+
const directList = details.querySelector(':scope > ul');
98+
if (directList) {
99+
const directChildren = directList.querySelectorAll(':scope > li');
100+
directChildren.forEach(child => {
101+
(child as HTMLElement).style.display = '';
102+
});
103+
}
104+
}
105+
106+
// Show parent chain for a specific item
107+
function showParentChain(element: Element) {
108+
let parent = element.parentElement;
109+
while (parent && parent !== sidebarContent) {
110+
if (parent.tagName === 'LI') {
111+
(parent as HTMLElement).style.display = '';
112+
}
113+
if (parent.tagName === 'DETAILS') {
114+
(parent as HTMLDetailsElement).open = true;
115+
}
116+
parent = parent.parentElement;
117+
}
118+
}
119+
120+
// Filter sidebar items based on search query
121+
function filterSidebarItems(query: string) {
122+
const items = sidebarContent!.querySelectorAll('li');
123+
const detailsElements = sidebarContent!.querySelectorAll('details');
124+
125+
if (!query.trim()) {
126+
// Reset to original state
127+
items.forEach(item => {
128+
(item as HTMLElement).style.display = '';
129+
});
130+
131+
// Restore original details state
132+
detailsElements.forEach(details => {
133+
const originalOpen = originalState.get(details);
134+
if (originalOpen !== undefined) {
135+
(details as HTMLDetailsElement).open = originalOpen;
136+
}
137+
});
138+
139+
// Hide no results message
140+
noResultsMessage.classList.add('hidden');
141+
142+
isSearchActive = false;
143+
clearButton.style.display = 'none';
144+
return;
145+
}
146+
147+
isSearchActive = true;
148+
clearButton.style.display = 'block';
149+
150+
// Split search query into terms for more precise matching
151+
const searchTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0);
152+
153+
// First, hide all items and close all details
154+
items.forEach(item => {
155+
(item as HTMLElement).style.display = 'none';
156+
});
157+
detailsElements.forEach(details => {
158+
(details as HTMLDetailsElement).open = false;
159+
});
160+
161+
// Track what we've matched to avoid duplicates
162+
const matchedItems = new Set<Element>();
163+
164+
// 1. Check for folder/subfolder matches first (highest priority)
165+
detailsElements.forEach(details => {
166+
const summary = details.querySelector('summary');
167+
if (summary) {
168+
const summaryText = summary.textContent || '';
169+
170+
if (matchesAllTerms(summaryText, searchTerms)) {
171+
// This is a folder match - show the folder and its direct children
172+
const parentLi = details.closest('li');
173+
if (parentLi && !matchedItems.has(parentLi)) {
174+
(parentLi as HTMLElement).style.display = '';
175+
showDirectChildren(details);
176+
showParentChain(parentLi);
177+
matchedItems.add(parentLi);
178+
}
179+
}
180+
}
181+
});
182+
183+
// 2. Check for specific page matches (show page + parent chain)
184+
items.forEach(item => {
185+
if (matchedItems.has(item)) return; // Skip if already matched as folder
186+
187+
const link = item.querySelector('a');
188+
const summary = item.querySelector('summary');
189+
190+
// Skip if this is a folder (has summary) - those are handled above
191+
if (summary) return;
192+
193+
if (link) {
194+
const linkText = link.textContent || '';
195+
196+
if (matchesAllTerms(linkText, searchTerms)) {
197+
// This is a specific page match - show page + parent chain
198+
(item as HTMLElement).style.display = '';
199+
showParentChain(item);
200+
matchedItems.add(item);
201+
}
202+
}
203+
});
204+
205+
// 3. Fallback: if no exact matches, show partial matches (less specific)
206+
if (matchedItems.size === 0) {
207+
items.forEach(item => {
208+
const textContent = item.textContent?.toLowerCase() || '';
209+
const link = item.querySelector('a');
210+
const linkText = link?.textContent?.toLowerCase() || '';
211+
const summary = item.querySelector('summary');
212+
const summaryText = summary?.textContent?.toLowerCase() || '';
213+
214+
// Check if any search term is found (partial matching)
215+
const hasPartialMatch = searchTerms.some(term =>
216+
textContent.includes(term) || linkText.includes(term) || summaryText.includes(term)
217+
);
218+
219+
if (hasPartialMatch) {
220+
(item as HTMLElement).style.display = '';
221+
222+
// If it's a folder, show direct children only
223+
if (summary) {
224+
const details = item.querySelector('details');
225+
if (details) {
226+
showDirectChildren(details);
227+
}
228+
}
229+
230+
showParentChain(item);
231+
matchedItems.add(item);
232+
}
233+
});
234+
}
235+
236+
// Show/hide no results message based on matches
237+
if (matchedItems.size === 0) {
238+
noResultsMessage.classList.remove('hidden');
239+
} else {
240+
noResultsMessage.classList.add('hidden');
241+
}
242+
}
243+
244+
// Event listeners
245+
searchInput.addEventListener('input', (e) => {
246+
storeOriginalState();
247+
const query = (e.target as HTMLInputElement).value;
248+
filterSidebarItems(query);
249+
});
250+
251+
clearButton.addEventListener('click', () => {
252+
searchInput.value = '';
253+
filterSidebarItems('');
254+
searchInput.focus();
255+
});
256+
257+
// Clear search on Escape key
258+
searchInput.addEventListener('keydown', (e) => {
259+
if (e.key === 'Escape') {
260+
searchInput.value = '';
261+
filterSidebarItems('');
262+
}
263+
});
264+
265+
// Global search link click handler
266+
globalSearchLink.addEventListener('click', () => {
267+
const currentQuery = searchInput.value.trim();
268+
if (currentQuery) {
269+
// Try multiple selectors for DocSearch
270+
const docSearchButton = document.querySelector('#docsearch button') as HTMLButtonElement ||
271+
document.querySelector('.DocSearch-Button') as HTMLButtonElement ||
272+
document.querySelector('[data-docsearch-button]') as HTMLButtonElement;
273+
274+
if (docSearchButton) {
275+
// Click the DocSearch button to open the modal
276+
docSearchButton.click();
277+
278+
// Wait for modal to open and set the search term
279+
setTimeout(() => {
280+
const searchInput = document.querySelector('.DocSearch-Input') as HTMLInputElement ||
281+
document.querySelector('#docsearch-input') as HTMLInputElement ||
282+
document.querySelector('[data-docsearch-input]') as HTMLInputElement;
283+
284+
if (searchInput) {
285+
searchInput.value = currentQuery;
286+
searchInput.focus();
287+
// Trigger search
288+
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
289+
}
290+
}, 100);
291+
}
292+
}
293+
});
294+
}
295+
296+
// Initialize when DOM is loaded
297+
if (document.readyState === 'loading') {
298+
document.addEventListener('DOMContentLoaded', initSidebarSearch);
299+
} else {
300+
initSidebarSearch();
301+
}
302+
303+
// Re-initialize on navigation (for SPA-like behavior)
304+
document.addEventListener('astro:page-load', initSidebarSearch);
305+
</script>
306+
21307
<style is:global>
22308
:root {
23309
.sidebar-content {

0 commit comments

Comments
 (0)