Implement interactive search

This commit is contained in:
Magnus Hoff 2017-10-25 13:24:42 +02:00
parent 2cdbd7c7f5
commit 056f1ddf72
12 changed files with 167 additions and 21 deletions

74
assets/search.js Normal file
View file

@ -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");
});
})();

View file

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

View file

@ -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)]

View file

@ -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()))
}))
}

View file

@ -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()))
))
}

View file

@ -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()))
}))
}

View file

@ -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()))
}))
}

View file

@ -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())),
}
}))

View file

@ -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()))
}))
}

View file

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

View file

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

View file

@ -5,10 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
{{#base}}<base href="{{.}}">{{/base}}
<link rel=preload href="_assets/amatic-sc-v9-latin-regular.woff" as=font crossorigin>
<link href="_assets/style-{{style_css_checksum}}.css" rel="stylesheet">
<link href="_assets/style-{{style_css_checksum()}}.css" rel="stylesheet">
</head>
<body>
<form class=search action=_search method=GET><input type=search name=q placeholder=search autocomplete=off></form>
<form class=search action=_search method=GET>
<input type=search name=q placeholder=search autocomplete=off>
<ul class="live-results search-results">
</ul>
</form>
<ul id="search-result-prototype" class="prototype"><li class="search-result" tabindex="0"><a class="link" href=""><span class="title"></span> &ndash; <span class="snippet"></span></a></li></ul>
<script src="_assets/search-{{search_js_checksum()}}.js" defer></script>
{{{body}}}
</body>
</html>