Implement interactive search
This commit is contained in:
parent
2cdbd7c7f5
commit
056f1ddf72
12 changed files with 167 additions and 21 deletions
74
assets/search.js
Normal file
74
assets/search.js
Normal 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");
|
||||
});
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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()))
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
))
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
}))
|
||||
|
|
|
@ -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()))
|
||||
}))
|
||||
}
|
||||
|
|
15
src/site.rs
15
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> – <span class="snippet"></span></a></li></ul>
|
||||
<script src="_assets/search-{{search_js_checksum()}}.js" defer></script>
|
||||
{{{body}}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue