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}}