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 {
|
article ul.search-results {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
.search-results {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.search-result {
|
.search-result {
|
||||||
|
@ -301,6 +303,7 @@ article ul.search-results {
|
||||||
.search {
|
.search {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search input {
|
.search input {
|
||||||
|
@ -313,15 +316,60 @@ article ul.search-results {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
transition: max-width 200ms;
|
transition: max-width 200ms;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 250px;
|
max-width: 300px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search input:focus {
|
.search input:focus, .search.focus input {
|
||||||
max-width: 250px;
|
max-width: 300px;
|
||||||
border-color: #999;
|
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) {
|
@media (min-width: 630px) {
|
||||||
.search {
|
.search {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
@ -330,4 +378,15 @@ article ul.search-results {
|
||||||
.search input {
|
.search input {
|
||||||
max-width: 125px;
|
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"]
|
#[mime = "application/javascript"]
|
||||||
pub struct ScriptJs;
|
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
|
// 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)
|
// Copyright 2015 The Amatic SC Project Authors (contact@sansoxygen.com)
|
||||||
#[derive(StaticResource)]
|
#[derive(StaticResource)]
|
||||||
|
|
|
@ -6,7 +6,7 @@ use hyper::server::*;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use assets::{StyleCss, ScriptJs};
|
use assets::ScriptJs;
|
||||||
use mimes::*;
|
use mimes::*;
|
||||||
use rendering::render_markdown;
|
use rendering::render_markdown;
|
||||||
use site::Layout;
|
use site::Layout;
|
||||||
|
@ -105,7 +105,6 @@ impl Resource for ArticleResource {
|
||||||
rendered: render_markdown(&data.body),
|
rendered: render_markdown(&data.body),
|
||||||
script_js_checksum: ScriptJs::checksum(),
|
script_js_checksum: ScriptJs::checksum(),
|
||||||
},
|
},
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string()))
|
}.to_string()))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use assets::StyleCss;
|
|
||||||
use mimes::*;
|
use mimes::*;
|
||||||
use models;
|
use models;
|
||||||
use rendering::render_markdown;
|
use rendering::render_markdown;
|
||||||
|
@ -105,7 +104,6 @@ impl Resource for ArticleRevisionResource {
|
||||||
title: &data.title,
|
title: &data.title,
|
||||||
rendered: render_markdown(&data.body),
|
rendered: render_markdown(&data.body),
|
||||||
},
|
},
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string()))
|
}.to_string()))
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use assets::StyleCss;
|
|
||||||
use mimes::*;
|
use mimes::*;
|
||||||
use schema::article_revisions;
|
use schema::article_revisions;
|
||||||
use site::Layout;
|
use site::Layout;
|
||||||
|
@ -344,7 +343,6 @@ impl Resource for ChangesResource {
|
||||||
older,
|
older,
|
||||||
changes
|
changes
|
||||||
},
|
},
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string()))
|
}.to_string()))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use hyper::server::*;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use assets::{StyleCss, ScriptJs};
|
use assets::ScriptJs;
|
||||||
use mimes::*;
|
use mimes::*;
|
||||||
use rendering::render_markdown;
|
use rendering::render_markdown;
|
||||||
use site::Layout;
|
use site::Layout;
|
||||||
|
@ -87,7 +87,6 @@ impl Resource for NewArticleResource {
|
||||||
rendered: EMPTY_ARTICLE_MESSAGE,
|
rendered: EMPTY_ARTICLE_MESSAGE,
|
||||||
script_js_checksum: ScriptJs::checksum(),
|
script_js_checksum: ScriptJs::checksum(),
|
||||||
},
|
},
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string()))
|
}.to_string()))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ use hyper::server::*;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_urlencoded;
|
use serde_urlencoded;
|
||||||
|
|
||||||
use assets::StyleCss;
|
|
||||||
use mimes::*;
|
use mimes::*;
|
||||||
use models;
|
use models;
|
||||||
use site::Layout;
|
use site::Layout;
|
||||||
|
@ -175,7 +174,6 @@ impl Resource for SearchResource {
|
||||||
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
||||||
hits: data,
|
hits: data,
|
||||||
},
|
},
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string())),
|
}.to_string())),
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -3,7 +3,6 @@ use hyper;
|
||||||
use hyper::header::ContentType;
|
use hyper::header::ContentType;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
|
|
||||||
use assets::StyleCss;
|
|
||||||
use mimes::*;
|
use mimes::*;
|
||||||
use site::Layout;
|
use site::Layout;
|
||||||
use state::State;
|
use state::State;
|
||||||
|
@ -63,7 +62,6 @@ impl Resource for SitemapResource {
|
||||||
base: None, // Hmm, should perhaps accept `base` as argument
|
base: None, // Hmm, should perhaps accept `base` as argument
|
||||||
title: "Sitemap",
|
title: "Sitemap",
|
||||||
body: &Template { articles },
|
body: &Template { articles },
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string()))
|
}.to_string()))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
15
src/site.rs
15
src/site.rs
|
@ -9,7 +9,7 @@ use hyper::mime;
|
||||||
use hyper::server::*;
|
use hyper::server::*;
|
||||||
use hyper;
|
use hyper;
|
||||||
|
|
||||||
use assets::StyleCss;
|
use assets::{StyleCss, SearchJs};
|
||||||
use web::Lookup;
|
use web::Lookup;
|
||||||
use wiki_lookup::WikiLookup;
|
use wiki_lookup::WikiLookup;
|
||||||
|
|
||||||
|
@ -25,7 +25,16 @@ pub struct Layout<'a, T: 'a + fmt::Display> {
|
||||||
pub base: Option<&'a str>,
|
pub base: Option<&'a str>,
|
||||||
pub title: &'a str,
|
pub title: &'a str,
|
||||||
pub body: &'a T,
|
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)]
|
#[derive(BartDisplay)]
|
||||||
|
@ -53,7 +62,6 @@ impl Site {
|
||||||
base: base,
|
base: base,
|
||||||
title: "Not found",
|
title: "Not found",
|
||||||
body: &NotFound,
|
body: &NotFound,
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string())
|
}.to_string())
|
||||||
.with_status(hyper::StatusCode::NotFound)
|
.with_status(hyper::StatusCode::NotFound)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +75,6 @@ impl Site {
|
||||||
base,
|
base,
|
||||||
title: "Internal server error",
|
title: "Internal server error",
|
||||||
body: &InternalServerError,
|
body: &InternalServerError,
|
||||||
style_css_checksum: StyleCss::checksum(),
|
|
||||||
}.to_string())
|
}.to_string())
|
||||||
.with_status(hyper::StatusCode::InternalServerError)
|
.with_status(hyper::StatusCode::InternalServerError)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,11 @@ lazy_static! {
|
||||||
Box::new(|| Box::new(ScriptJs) as BoxResource) as ResourceFn
|
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(
|
map.insert(
|
||||||
format!("amatic-sc-v9-latin-regular.woff"),
|
format!("amatic-sc-v9-latin-regular.woff"),
|
||||||
Box::new(|| Box::new(AmaticFont) as BoxResource) as ResourceFn
|
Box::new(|| Box::new(AmaticFont) as BoxResource) as ResourceFn
|
||||||
|
|
|
@ -5,10 +5,16 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{{#base}}<base href="{{.}}">{{/base}}
|
{{#base}}<base href="{{.}}">{{/base}}
|
||||||
<link rel=preload href="_assets/amatic-sc-v9-latin-regular.woff" as=font crossorigin>
|
<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>
|
</head>
|
||||||
<body>
|
<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}}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue