X-Git-Url: http://source.bookstackapp.com/website/blobdiff_plain/367afb9a97e932ade6f431fa3fa1bcc8def27ed1..HEAD:/themes/bookstack/static/js/script.js diff --git a/themes/bookstack/static/js/script.js b/themes/bookstack/static/js/script.js index 1e4d51d..6b7e5a7 100644 --- a/themes/bookstack/static/js/script.js +++ b/themes/bookstack/static/js/script.js @@ -10,8 +10,11 @@ menuButton.addEventListener('click', function(event) { }); document.body.addEventListener('click', function(event) { - menuDropDown.classList.remove('showing'); - event.stopPropagation(); + const isShown = menuDropDown.classList.contains('showing'); + if (isShown) { + menuDropDown.classList.remove('showing'); + event.stopPropagation(); + } }); @@ -32,4 +35,185 @@ document.body.addEventListener('dblclick', event => { if (isHeader && event.target.id) { window.location.hash = event.target.id; } -}); \ No newline at end of file +}); + +function el(tag, attributes = {}, children = []) { + const elem = document.createElement(tag); + for (const attr of Object.keys(attributes)) { + elem.setAttribute(attr, attributes[attr]); + } + for (let child of children) { + if (typeof child === 'string') { + child = new Text(child); + } + elem.append(child); + } + return elem; +} + +// Site search +const searchForm = document.getElementById('site-search-form'); +const searchInput = document.getElementById('site-search-input'); +const searchDialog = searchForm.querySelector('dialog'); + +async function runSearch() { + const searchTerm = searchInput.value; + + let pages = []; + try { + const resp = await fetch(`/search.php?query=${encodeURIComponent(searchTerm)}`); + pages = await resp.json(); + } catch (error) { + searchDialog.innerHTML = 'Failed to load search results'; + console.error(error); + return; + } + + // Sort pages to prioritise those with word in title + const lowerSearchTerm = searchTerm.toLowerCase(); + pages.sort((a, b) => { + const aScore = (a.url.includes(lowerSearchTerm) || a.title.toLowerCase().includes(lowerSearchTerm)) ? 1 : 0; + const bScore = (b.url.includes(lowerSearchTerm) || b.title.toLowerCase().includes(lowerSearchTerm)) ? 1 : 0; + return bScore - aScore; + }); + + // 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 = `
`; + showSearchDialog(); +} + +searchForm.addEventListener('submit', event => { + event.preventDefault(); + showSearchLoading(); + runSearch(); +}); + +searchInput.addEventListener('input', event => { + const termLength = searchInput.value.length; + if (termLength === 0) { + searchDialog.close(); + } else if (termLength > 2) { + showSearchLoading(); + runSearch(); + } +}); + + +// Email display +const emailDisplayLinks = document.querySelectorAll('a.email-display'); +const eb64 = 'ZW1haWxAYm9v' + 'a3N0YWNrYXBwLmNvbQ=='; +for (const link of emailDisplayLinks) { + const email = atob(eb64); + link.addEventListener('click', e => { + e.preventDefault(); + e.target.textContent = email; + e.target.href = 'mailto:' + email; + }); +} \ No newline at end of file