+
+ // Categorizes pages to display
+ const categorised = {
+ docs: {title: 'Documentation', filter: '/docs/', pages: []},
+ hacks: {title: 'Hacks', filter: '/hacks/', pages: []},
+ blog: {title: 'From the blog', filter: '/blog/', pages: []},
+ other: {title: 'Site Pages', filter: '', pages: []},
+ };
+ const categoryNames = Object.keys(categorised);
+
+ for (const page of pages) {
+ let pageCategory = null;
+ for (const categoryName of categoryNames) {
+ const category = categorised[categoryName];
+ if (page.url.startsWith(category.filter)) {
+ pageCategory = category;
+ break;
+ }
+ }
+ (pageCategory || categorised.other).pages.push(page);
+ }
+
+ const categoryResults = categoryNames.map(name => {
+ const category = categorised[name];
+ if (category.pages.length === 0) {
+ return null;
+ }
+ return el('div', {}, [
+ el('strong', {class: 'search-category-title'}, [category.title]),
+ el('div', {}, category.pages.slice(0, 5).map(page => {
+ return el('a', {href: page.url}, [page.title]);
+ })),
+ ]);
+ }).filter(Boolean);
+
+ const emptyResult = el('strong', {class: 'search-category-title'}, [el('em', {}, 'No results found')]);
+ const resultList = categoryResults.length ? categoryResults : [emptyResult];
+ const results = el('div', {}, resultList);
+
+ while (searchDialog.firstChild) {
+ searchDialog.removeChild(searchDialog.firstChild);
+ }
+
+ searchDialog.append(results);
+ showSearchDialog();
+}
+
+function showSearchDialog() {
+ if (searchDialog.open) {
+ return;
+ }
+ searchDialog.show();
+ searchInput.focus();
+
+ const clickListener = e => {
+ if (!e.target.closest('dialog')) {
+ closeListener();
+ }
+ };
+
+ const escListener = e => {
+ if (e.key === 'Escape') {
+ closeListener();
+ }
+ };
+
+ const arrowListener = e => {
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
+ return;
+ }
+ e.preventDefault();
+
+ const links = Array.from(searchDialog.querySelectorAll('a'));
+ const focusables = [searchInput, ...links];
+ if (focusables.length < 2) { // Only search input
+ return;
+ }
+
+ const active = document.activeElement;
+ let currentIndex = focusables.indexOf(active);
+
+ if (e.key === 'ArrowDown') {
+ currentIndex = (currentIndex + 1) % focusables.length;
+ } else { // ArrowUp
+ currentIndex = (currentIndex - 1 + focusables.length) % focusables.length;
+ }
+
+ focusables[currentIndex].focus();
+ };
+
+ const mouseLeaveListener = e => {
+ closeListener();
+ };
+
+ const closeListener = () => {
+ searchDialog.close();
+ document.removeEventListener('click', clickListener);
+ document.removeEventListener('keydown', escListener);
+ searchForm.removeEventListener('mouseleave', mouseLeaveListener);
+ searchForm.removeEventListener('keydown', arrowListener);
+ };
+
+ document.addEventListener('click', clickListener);
+ document.addEventListener('keydown', escListener);
+ searchForm.addEventListener('mouseleave', mouseLeaveListener);
+ searchForm.addEventListener('keydown', arrowListener);
+}
+
+function showSearchLoading() {
+ searchDialog.innerHTML = `<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>`;
+ showSearchDialog();