diff --git a/code/services-application/search-service/src/main/resources/static/search/serp.js b/code/services-application/search-service/src/main/resources/static/search/serp.js index c04e78c9..cd2c6d38 100644 --- a/code/services-application/search-service/src/main/resources/static/search/serp.js +++ b/code/services-application/search-service/src/main/resources/static/search/serp.js @@ -1,23 +1,47 @@ +// This sets the data-has-js attribute on the body tag to true, so we can style the page with the assumption that +// the browser supports JS. This is a progressive enhancement, so the page will still work without JS. document.getElementsByTagName('body')[0].setAttribute('data-has-js', 'true'); +const registerCloseButton = () => { + // Add a button to close the filters for mobile; we do this in js to not pollute the DOM for text-only browsers + const closeButton = document.createElement('button'); + closeButton.setAttribute('id', 'menu-close'); + closeButton.setAttribute('title', 'Close the menu'); + closeButton.setAttribute('aria-controls', '#filters'); + closeButton.innerHTML = 'X'; + closeButton.onclick = (event) => { + document.getElementById('filters').style.display = 'none'; + event.stopPropagation(); + return false; + } + document.getElementById('filters').getElementsByTagName('h2')[0].append(closeButton); +} + // Add a button to open the filters for mobile; we do this in js to not pollute the DOM for text-only browsers -const button = document.createElement('button'); -button.setAttribute('id', 'mcfeast'); -button.setAttribute('aria-controls', '#filters'); -button.onclick = (event) => { - setDisplay(document.getElementById('filters'), 'block'); +const filtersButton = document.createElement('button'); +filtersButton.setAttribute('id', 'mcfeast'); +filtersButton.setAttribute('aria-controls', '#filters'); +filtersButton.onclick = (event) => { + // Defer creation of the close button until the menu is opened. This is needed because the script for creating + // the filter button is run early to avoid layout shifts. + + if (document.getElementById('menu-close') === null) { + registerCloseButton(); + } + + document.getElementById('filters').style.display = 'block'; event.stopPropagation(); return false; } -button.innerHTML = 'Filters'; -document.getElementById('search-box').getElementsByTagName('h1')[0].append(button); +filtersButton.innerHTML = 'Filters'; +document.getElementById('search-box').getElementsByTagName('h1')[0].append(filtersButton); -function setDisplay(element, value) { - element.style.display = value; -} - -document.getElementById('menu-close').onclick = (event) => { - setDisplay(document.getElementById('filters'), 'none'); - event.stopPropagation(); - return false; -} \ No newline at end of file +// To prevent the filter menu from being opened when the user hits enter on the search box, we need to add a keydown +// handler to the search box that stops the event from propagating. Janky hack, but it works. +document.getElementById('query').addEventListener('keydown', e=> { + if (e.key === "Enter") { + const form = document.getElementById('search-form'); + form.submit(); + e.preventDefault(); + } +}); \ No newline at end of file diff --git a/code/services-application/search-service/src/main/resources/static/search/serp.scss b/code/services-application/search-service/src/main/resources/static/search/serp.scss index ae5a1c2f..03be8190 100644 --- a/code/services-application/search-service/src/main/resources/static/search/serp.scss +++ b/code/services-application/search-service/src/main/resources/static/search/serp.scss @@ -272,6 +272,7 @@ section.cards { margin-left: 1ch; } + footer { clear: both; @@ -348,7 +349,7 @@ footer { padding: 0.5ch; background-color: $fg-light; display: grid; - grid-template-columns: max-content auto max-content; + grid-template-columns: max-content 0 auto max-content; grid-gap: 0.5ch; grid-auto-rows: minmax(1ch, auto); width: 100%; @@ -366,6 +367,11 @@ footer { text-align: center; } + #suggestions-anchor { + margin: -0.5ch; // We need this anchor for the typeahead suggestions, but we don't want it to affect the layout + padding: 0; + } + input[type="text"] { font-family: monospace; font-size: 12pt; @@ -375,13 +381,42 @@ footer { color: $fg-dark; } - button[type="submit"] { + input[type="submit"] { font-size: 12pt; border: 1px solid $border-color; background-color: $fg-light; color: $fg-dark; } + .suggestions { + background-color: #fff; + padding: .5ch; + margin-top: 5.5ch; + margin-left: 1ch; + position: absolute; + display: inline-block; + width: 300px; + border-left: 1px solid #ccc; + border-top: 1px solid #ccc; + box-shadow: 5px 5px 5px #888; + z-index: 10; + + a { + display: block; + color: #000; + font-size: 12pt; + font-family: 'fixedsys', monospace, serif; + text-decoration: none; + outline: none; + } + + a:focus { + display: block; + background-color: #000; + color: #eee; + } + } + } .filter-toggle-on { @@ -519,6 +554,8 @@ footer { padding: 0 0 0 0 !important; max-width: 100%; + #suggestions-anchor { display: none; } // suggestions are not useful on mobile + .sidebar-narrow { display: block; // fix for bizarre chrome rendering issue } diff --git a/code/services-application/search-service/src/main/resources/static/search/tts.js b/code/services-application/search-service/src/main/resources/static/search/tts.js index 2d07a38c..c48565be 100644 --- a/code/services-application/search-service/src/main/resources/static/search/tts.js +++ b/code/services-application/search-service/src/main/resources/static/search/tts.js @@ -1,86 +1,96 @@ -if(!window.matchMedia("(pointer: coarse)").matches) { - query = document.getElementById('query'); +function setupTypeahead() { + const query = document.getElementById('query'); query.setAttribute('autocomplete', 'off'); - timer = null; + const queryBox = document.getElementById('suggestions-anchor'); + let timer = null; + function fetchSuggestions(e) { - if (timer != null) { - clearTimeout(timer); - } - timer = setTimeout(() => { - req = new XMLHttpRequest(); + if (timer != null) { + clearTimeout(timer); + } + timer = setTimeout(() => { + const req = new XMLHttpRequest(); - req.onload = rsp => { - items = JSON.parse(req.responseText); + req.onload = rsp => { + let items = JSON.parse(req.responseText); - var old = document.getElementById('suggestions'); - if (old != null) old.remove(); - - if (items.length == 0) return; - - suggestions = document.createElement('div'); - suggestions.setAttribute('id', 'suggestions'); - suggestions.setAttribute('class', 'suggestions'); + const old = document.getElementById('suggestions'); + if (old != null) old.remove(); - for (i=0;i { + if (e.key === "ArrowDown") { + if (e.target.nextElementSibling != null) { + e.target.nextElementSibling.focus(); + } - function suggestionClickHandler(e) { - query.value = e.target.text; - query.focus(); - document.getElementById('suggestions').remove(); e.preventDefault() } - item.addEventListener('click', suggestionClickHandler); - - item.addEventListener('keydown', e=> { - if (e.key === "ArrowDown") { - if (e.target.nextElementSibling != null) { - e.target.nextElementSibling.focus(); - } - - e.preventDefault() + else if (e.key === "ArrowUp") { + if (e.target.previousElementSibling != null) { + e.target.previousElementSibling.focus(); } - else if (e.key === "ArrowUp") { - if (e.target.previousElementSibling != null) { - e.target.previousElementSibling.focus(); - } - else { - query.focus(); - } - e.preventDefault() - } - else if (e.key === "Escape") { - var suggestions = document.getElementById('suggestions'); - if (suggestions != null) { - suggestions.remove(); - } + else { query.focus(); - e.preventDefault(); } - }); - item.addEventListener('keypress', e=> { - if (e.key === "Enter") { - suggestionClickHandler(e); + e.preventDefault() + } + else if (e.key === "Escape") { + var suggestions = document.getElementById('suggestions'); + if (suggestions != null) { + suggestions.remove(); } - }); - suggestions.appendChild(item); - } - document.getElementsByClassName('input')[0].appendChild(suggestions); + query.focus(); + e.preventDefault(); + } + }); + item.addEventListener('keypress', e=> { + if (e.key === "Enter") { + suggestionClickHandler(e); + } + }); + suggestions.appendChild(item); } + queryBox.prepend(suggestions); + } - req.open("GET", "/suggest/?partial="+encodeURIComponent(query.value)); - req.send(); - }, 250); - } + req.open("GET", "/suggest/?partial="+encodeURIComponent(query.value)); + req.send(); + }, 250); + } query.addEventListener("input", fetchSuggestions); - query.addEventListener("click", e=> { var suggestions = document.getElementById('suggestions'); if (suggestions != null) suggestions.remove(); }); + query.addEventListener("click", e=> { + const suggestions = document.getElementById('suggestions'); + if (suggestions != null) { + suggestions.remove(); + } + }); query.addEventListener("keydown", e => { if (e.key === "ArrowDown") { - var suggestions = document.getElementById('suggestions'); + const suggestions = document.getElementById('suggestions'); if (suggestions != null) { suggestions.childNodes[0].focus(); } @@ -90,7 +100,7 @@ if(!window.matchMedia("(pointer: coarse)").matches) { e.preventDefault() } else if (e.key === "Escape") { - var suggestions = document.getElementById('suggestions'); + const suggestions = document.getElementById('suggestions'); if (suggestions != null) { suggestions.remove(); } @@ -99,3 +109,7 @@ if(!window.matchMedia("(pointer: coarse)").matches) { } }); } + +if(!window.matchMedia("(pointer: coarse)").matches) { + setupTypeahead(); +} diff --git a/code/services-application/search-service/src/main/resources/templates/search/parts/search-filters.hdb b/code/services-application/search-service/src/main/resources/templates/search/parts/search-filters.hdb index 21927803..49e8e472 100644 --- a/code/services-application/search-service/src/main/resources/templates/search/parts/search-filters.hdb +++ b/code/services-application/search-service/src/main/resources/templates/search/parts/search-filters.hdb @@ -1,4 +1,4 @@ -

Filters

+

Filters