` : '';
return `
${img}
${hit.title}
${hit.description || ''}
`;
}
function sourceIndex(indexName, panelId) {
return instantsearch.widgets.index({ indexName }).addWidgets([
instantsearch.widgets.configure({ hitsPerPage: resultsPerTab }),
instantsearch.widgets.hits({ container: '#' + panelId + ' .hits', templates: { item: hitTemplate } }),
makeStatsWidget(panelId),
]);
}
search.addWidgets([
sourceIndex('percona_main_site', 'results1'),
sourceIndex('percona_docs_pages', 'results2'),
sourceIndex('percona_blog_articles', 'results3'),
sourceIndex('percona_community_blog_pages', 'results4'),
]);
search.on('render', () => {
clearActive();
scheduleReposition();
// new query: reset the "user picked a tab" pin and scroll lists to the top
const q = (search.helper ? search.helper.state.query : '').trim();
if (q !== lastQuery) {
lastQuery = q;
userChoseTab = false;
popupEl.querySelectorAll('.results').forEach(r => { r.scrollTop = 0; });
}
if (q !== '') maybeAutoSwitch();
});
search.start();
function getInput() {
return container.querySelector('.ais-SearchBox-input');
}
function openPopup() {
updateDropPositions();
popupEl.classList.add('open');
const input = getInput();
if (input) input.setAttribute('aria-expanded', 'true');
}
function closePopup() {
popupEl.classList.remove('open');
clearActive();
const input = getInput();
if (input) input.setAttribute('aria-expanded', 'false');
}
function setupCombobox() {
const input = getInput();
if (!input) return;
input.setAttribute('role', 'combobox');
input.setAttribute('aria-expanded', 'false');
input.setAttribute('aria-controls', 'search-popup');
input.setAttribute('aria-haspopup', 'listbox');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-label', 'Search percona.com');
}
setupCombobox();
function visibleHitLinks() {
const activePanel = popupEl.querySelector('.results.active');
return activePanel ? Array.from(activePanel.querySelectorAll('.hit-link')) : [];
}
function clearActive() {
activeIndex = -1;
popupEl.querySelectorAll('.hit-link.is-active').forEach(el => {
el.classList.remove('is-active');
el.removeAttribute('aria-selected');
});
const input = getInput();
if (input) input.removeAttribute('aria-activedescendant');
}
function setActive(index, opts = {}) {
const { scroll = true } = opts;
const links = visibleHitLinks();
if (!links.length) return;
activeIndex = (index + links.length) % links.length;
links.forEach((el, i) => {
const on = i === activeIndex;
el.classList.toggle('is-active', on);
if (on) {
if (!el.id) el.id = 'hit-option-' + i;
el.setAttribute('aria-selected', 'true');
// keyboard nav keeps the highlighted row inside the scroll area;
// hovering must not scroll, or the list jumps under the pointer
if (scroll) el.scrollIntoView({ block: 'nearest' });
const input = getInput();
if (input) input.setAttribute('aria-activedescendant', el.id);
} else {
el.removeAttribute('aria-selected');
}
});
}
document.querySelectorAll('.search-popup .tab').forEach(tab => {
tab.addEventListener('click', () => activateTab(tab, { byUser: true }));
tab.addEventListener('keydown', (e) => {
const tabsArr = Array.from(document.querySelectorAll('.search-popup .tab'));
const i = tabsArr.indexOf(tab);
let next = null;
if (e.key === 'ArrowRight') next = tabsArr[(i + 1) % tabsArr.length];
if (e.key === 'ArrowLeft') next = tabsArr[(i - 1 + tabsArr.length) % tabsArr.length];
if (next) {
e.preventDefault();
next.focus();
activateTab(next, { byUser: true });
}
});
});
function activateTab(tab, opts = {}) {
if (opts.byUser) userChoseTab = true;
document.querySelectorAll('.search-popup .tab').forEach(t => {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
document.querySelectorAll('.search-popup .results').forEach(r => r.classList.remove('active'));
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
tab.setAttribute('tabindex', '0');
const panel = document.getElementById(tab.dataset.target);
panel.classList.add('active');
panel.scrollTop = 0;
clearActive();
updateFooter();
}
function expandSearch() {
updateDropPositions();
container.classList.add('expanded');
const delay = isDropdownMode() ? 0 : 260;
setTimeout(() => {
// the wrap is laid out by now, so measure it for real — measuring too
// early estimated 56px and let the popup slide under the input bar,
// hiding the tab row and the first hit
updateDropPositions();
const input = getInput();
if (input) {
input.focus();
if (input.value.trim() !== '') openPopup();
}
}, delay);
}
// focusIcon: only when the user deliberately dismissed the search (close
// button, Escape, icon toggle). On an outside click focus belongs wherever
// they clicked, so don't yank it back to the header.
function collapseSearch(opts = {}) {
const { focusIcon = true } = opts;
const input = getInput();
if (input) {
input.value = '';
search.helper.setQuery('').search();
}
closePopup();
container.classList.remove('expanded');
if (focusIcon) iconBtn.focus();
}
iconBtn.addEventListener('click', () => {
if (isDropdownMode() && container.classList.contains('expanded')) {
collapseSearch();
} else {
expandSearch();
}
});
collapseBtn.addEventListener('click', () => collapseSearch());
container.addEventListener('keydown', (e) => {
const input = getInput();
if (!input || e.target !== input) return;
if (e.key === 'Enter') {
e.preventDefault();
const links = visibleHitLinks();
if (popupEl.classList.contains('open') && activeIndex >= 0 && links[activeIndex]) {
links[activeIndex].click();
} else {
const q = input.value.trim();
if (q !== '') goToSearchPage(q);
}
return;
}
if (!popupEl.classList.contains('open')) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
setActive(activeIndex + 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActive(activeIndex - 1);
} else if (e.key === 'Home' && activeIndex >= 0) {
e.preventDefault();
setActive(0);
} else if (e.key === 'End' && activeIndex >= 0) {
e.preventDefault();
setActive(visibleHitLinks().length - 1);
}
});
popupEl.addEventListener('mousemove', (e) => {
const link = e.target.closest('.hit-link');
if (!link) {
if (activeIndex !== -1) clearActive();
return;
}
const links = visibleHitLinks();
const i = links.indexOf(link);
if (i !== -1 && i !== activeIndex) setActive(i, { scroll: false });
});
popupEl.addEventListener('mouseleave', () => {
clearActive();
});
document.addEventListener('focusin', (e) => {
if (container.contains(e.target) && e.target.matches('.ais-SearchBox-input')) {
if (e.target.value.trim() !== '') openPopup();
}
});
/* Outside-click close. composedPath() is captured at dispatch time, so it
still works if a re-render detaches the clicked node before the event
bubbles here; the pointerdown flag is a fallback for older browsers. */
let pressedInside = false;
document.addEventListener('pointerdown', (e) => {
pressedInside = container.contains(e.target);
}, true);
document.addEventListener('click', (e) => {
const path = typeof e.composedPath === 'function' ? e.composedPath() : null;
const inside = path ? path.includes(container) : container.contains(e.target);
if (inside || pressedInside) return;
// full collapse, not just closePopup() — leaving the expanded input bar
// behind puts it on top of whatever nav menu the user just opened
if (container.classList.contains('expanded')) collapseSearch({ focusIcon: false });
else closePopup();
});
// keyboard equivalent: tabbing or focusing into a nav menu also dismisses it
document.addEventListener('focusin', (e) => {
if (!container.classList.contains('expanded')) return;
if (container.contains(e.target)) return;
collapseSearch({ focusIcon: false });
});
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
if (popupEl.classList.contains('open')) {
closePopup();
const input = getInput();
if (input) input.focus();
} else if (container.classList.contains('expanded')) {
collapseSearch();
}
});
})();