diff --git a/assets/search.js b/assets/search.js new file mode 100644 index 0000000..cde8896 --- /dev/null +++ b/assets/search.js @@ -0,0 +1,74 @@ +function debouncer(interval, callback) { + let currentTimeout = null; + + function trigger() { + currentTimeout = null; + callback(); + } + + return function () { + clearTimeout(currentTimeout); + currentTimeout = setTimeout(trigger, interval); + }; +} + +(function () { + const form = document.querySelector('form.search'); + const input = form.querySelector('input'); + const results = form.querySelector('.live-results'); + const resultPrototype = document.getElementById('search-result-prototype').firstChild; + + form.addEventListener('submit', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + }); + + function submit() { + if (input.value === "") { + results.classList.remove("show"); + while (results.lastChild) results.removeChild(results.lastChild); + return; + } + + fetch( + "_search?snippet_size=4&limit=3&q=" + encodeURIComponent(input.value), + { + headers: { + "Accept": "application/json", + }, + credentials: "same-origin", + } + ).then(response => { + if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")"); + + return response.json(); + }).then(result => { + while (results.lastChild) results.removeChild(results.lastChild); + + result.hits.forEach(hit => { + const item = resultPrototype.cloneNode(true); + item.querySelector('.link').href = hit.slug || "."; + item.querySelector('.title').textContent = hit.title; + item.querySelector('.snippet').textContent = hit.snippet; + results.appendChild(item); + }) + results.classList.add("show"); + }).catch(err => { + console.error(err); + alert(err); + }); + } + const submitter = debouncer(200, submit); + + input.addEventListener('input', submitter); + + form.addEventListener('focusin', () => form.classList.add("focus")); + form.addEventListener('focusout', function (ev) { + for (let ancestor = ev.relatedTarget; ancestor; ancestor = ancestor.parentElement) { + if (ancestor === form) return; + } + + // We are now actually losing focus from the form: + form.classList.remove("focus"); + }); +})(); diff --git a/assets/style.css b/assets/style.css index efcd405..1e59880 100644 --- a/assets/style.css +++ b/assets/style.css @@ -286,6 +286,8 @@ h1>input { article ul.search-results { padding-left: 8px; +} +.search-results { list-style: none; } .search-result { @@ -301,6 +303,7 @@ article ul.search-results { .search { text-align: center; margin-top: 30px; + position: relative; } .search input { @@ -313,15 +316,60 @@ article ul.search-results { border: 1px solid #ccc; transition: max-width 200ms; width: 100%; - max-width: 250px; + max-width: 300px; box-sizing: border-box; } -.search input:focus { - max-width: 250px; +.search input:focus, .search.focus input { + max-width: 300px; border-color: #999; } +.search .live-results { + text-align: left; + + box-sizing: border-box; + width: 100%; + max-width: 268px; + + background: white; + padding: 0; + margin: 0 auto; + + overflow: hidden; + + transition: max-height 200ms; + max-height: 0px; +} + +.live-results.show { + max-height: 500px; +} + +.live-results .search-result { + padding: 0; + border-top: none; + margin: 0; +} + +.live-results .search-result li { + padding: 0; +} +.live-results .search-result a { + display: block; + color: inherit; + text-decoration: none; + padding: 8px; +} +.live-results .search-result a:hover { + background: #0074D9; + color: white; +} + +.prototype { + display: none; +} + @media (min-width: 630px) { .search { text-align: right; @@ -330,4 +378,15 @@ article ul.search-results { .search input { max-width: 125px; } + + .search .live-results { + position: absolute; + right: 8px; + margin: 0 16px; + display: none; + } + + .search.focus .live-results { + display: block; + } } diff --git a/src/assets.rs b/src/assets.rs index fba9ad1..91375c9 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -11,6 +11,11 @@ pub struct StyleCss; #[mime = "application/javascript"] pub struct ScriptJs; +#[derive(StaticResource)] +#[filename = "assets/search.js"] +#[mime = "application/javascript"] +pub struct SearchJs; + // SIL Open Font License 1.1: http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL // Copyright 2015 The Amatic SC Project Authors (contact@sansoxygen.com) #[derive(StaticResource)] diff --git a/src/resources/article_resource.rs b/src/resources/article_resource.rs index d4203e1..8306975 100644 --- a/src/resources/article_resource.rs +++ b/src/resources/article_resource.rs @@ -6,7 +6,7 @@ use hyper::server::*; use serde_json; use serde_urlencoded; -use assets::{StyleCss, ScriptJs}; +use assets::ScriptJs; use mimes::*; use rendering::render_markdown; use site::Layout; @@ -105,7 +105,6 @@ impl Resource for ArticleResource { rendered: render_markdown(&data.body), script_js_checksum: ScriptJs::checksum(), }, - style_css_checksum: StyleCss::checksum(), }.to_string())) })) } diff --git a/src/resources/article_revision_resource.rs b/src/resources/article_revision_resource.rs index 70a70c9..05297b3 100644 --- a/src/resources/article_revision_resource.rs +++ b/src/resources/article_revision_resource.rs @@ -4,7 +4,6 @@ use hyper; use hyper::header::ContentType; use hyper::server::*; -use assets::StyleCss; use mimes::*; use models; use rendering::render_markdown; @@ -105,7 +104,6 @@ impl Resource for ArticleRevisionResource { title: &data.title, rendered: render_markdown(&data.body), }, - style_css_checksum: StyleCss::checksum(), }.to_string())) )) } diff --git a/src/resources/changes_resource.rs b/src/resources/changes_resource.rs index 305ff25..e8e2b1f 100644 --- a/src/resources/changes_resource.rs +++ b/src/resources/changes_resource.rs @@ -6,7 +6,6 @@ use hyper::header::ContentType; use hyper::server::*; use serde_urlencoded; -use assets::StyleCss; use mimes::*; use schema::article_revisions; use site::Layout; @@ -344,7 +343,6 @@ impl Resource for ChangesResource { older, changes }, - style_css_checksum: StyleCss::checksum(), }.to_string())) })) } diff --git a/src/resources/new_article_resource.rs b/src/resources/new_article_resource.rs index 5acfc34..67868ca 100644 --- a/src/resources/new_article_resource.rs +++ b/src/resources/new_article_resource.rs @@ -5,7 +5,7 @@ use hyper::server::*; use serde_json; use serde_urlencoded; -use assets::{StyleCss, ScriptJs}; +use assets::ScriptJs; use mimes::*; use rendering::render_markdown; use site::Layout; @@ -87,7 +87,6 @@ impl Resource for NewArticleResource { rendered: EMPTY_ARTICLE_MESSAGE, script_js_checksum: ScriptJs::checksum(), }, - style_css_checksum: StyleCss::checksum(), }.to_string())) })) } diff --git a/src/resources/search_resource.rs b/src/resources/search_resource.rs index 921346a..c5da67b 100644 --- a/src/resources/search_resource.rs +++ b/src/resources/search_resource.rs @@ -5,7 +5,6 @@ use hyper::server::*; use serde_json; use serde_urlencoded; -use assets::StyleCss; use mimes::*; use models; use site::Layout; @@ -175,7 +174,6 @@ impl Resource for SearchResource { query: self.query.as_ref().map(|x| &**x).unwrap_or(""), hits: data, }, - style_css_checksum: StyleCss::checksum(), }.to_string())), } })) diff --git a/src/resources/sitemap_resource.rs b/src/resources/sitemap_resource.rs index 07443a5..340178b 100644 --- a/src/resources/sitemap_resource.rs +++ b/src/resources/sitemap_resource.rs @@ -3,7 +3,6 @@ use hyper; use hyper::header::ContentType; use hyper::server::*; -use assets::StyleCss; use mimes::*; use site::Layout; use state::State; @@ -63,7 +62,6 @@ impl Resource for SitemapResource { base: None, // Hmm, should perhaps accept `base` as argument title: "Sitemap", body: &Template { articles }, - style_css_checksum: StyleCss::checksum(), }.to_string())) })) } diff --git a/src/site.rs b/src/site.rs index cdff3e0..00295bb 100644 --- a/src/site.rs +++ b/src/site.rs @@ -9,7 +9,7 @@ use hyper::mime; use hyper::server::*; use hyper; -use assets::StyleCss; +use assets::{StyleCss, SearchJs}; use web::Lookup; use wiki_lookup::WikiLookup; @@ -25,7 +25,16 @@ pub struct Layout<'a, T: 'a + fmt::Display> { pub base: Option<&'a str>, pub title: &'a str, pub body: &'a T, - pub style_css_checksum: &'a str, +} + +impl<'a, T: 'a + fmt::Display> Layout<'a, T> { + pub fn style_css_checksum(&self) -> &str { + StyleCss::checksum() + } + + pub fn search_js_checksum(&self) -> &str { + SearchJs::checksum() + } } #[derive(BartDisplay)] @@ -53,7 +62,6 @@ impl Site { base: base, title: "Not found", body: &NotFound, - style_css_checksum: StyleCss::checksum(), }.to_string()) .with_status(hyper::StatusCode::NotFound) } @@ -67,7 +75,6 @@ impl Site { base, title: "Internal server error", body: &InternalServerError, - style_css_checksum: StyleCss::checksum(), }.to_string()) .with_status(hyper::StatusCode::InternalServerError) } diff --git a/src/wiki_lookup.rs b/src/wiki_lookup.rs index f1c77bf..bf00c2e 100644 --- a/src/wiki_lookup.rs +++ b/src/wiki_lookup.rs @@ -29,6 +29,11 @@ lazy_static! { Box::new(|| Box::new(ScriptJs) as BoxResource) as ResourceFn ); + map.insert( + format!("search-{}.js", SearchJs::checksum()), + Box::new(|| Box::new(SearchJs) as BoxResource) as ResourceFn + ); + map.insert( format!("amatic-sc-v9-latin-regular.woff"), Box::new(|| Box::new(AmaticFont) as BoxResource) as ResourceFn diff --git a/templates/layout.html b/templates/layout.html index ee72796..e688fee 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -5,10 +5,16 @@ {{#base}}{{/base}} - + - + + + {{{body}}}