Refactor CSS for search results. Add support for keyboard control to full search results page
This commit is contained in:
parent
b90d7b9d1e
commit
74b8040d39
5 changed files with 71 additions and 49 deletions
|
@ -68,25 +68,35 @@ function debouncer(interval, callback) {
|
||||||
form.classList.remove("focus");
|
form.classList.remove("focus");
|
||||||
});
|
});
|
||||||
|
|
||||||
function moveFocus(delta) {
|
function moveFocus(element, delta) {
|
||||||
const focusIndexText = document.activeElement.getAttribute("data-focusindex");
|
const focusIndexText = document.activeElement.getAttribute("data-focusindex");
|
||||||
if (!focusIndexText) return;
|
const nextIndex = focusIndexText ? parseInt(focusIndexText, 10) + delta : 0;
|
||||||
const currentIndex = parseInt(focusIndexText, 10);
|
|
||||||
const nextIndex = currentIndex + delta;
|
|
||||||
|
|
||||||
const candidate = form.querySelector("[data-focusindex=\"" + nextIndex + "\"]");
|
const candidate = element.querySelector("[data-focusindex=\"" + nextIndex + "\"]");
|
||||||
if (candidate) candidate.focus();
|
if (candidate) candidate.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
form.addEventListener('keydown', function (ev) {
|
function focusControl(element, ev) {
|
||||||
if (ev.key === 'ArrowUp') {
|
if (ev.key === 'ArrowUp') {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
moveFocus(-1);
|
moveFocus(element, -1);
|
||||||
} else if (ev.key === 'ArrowDown') {
|
} else if (ev.key === 'ArrowDown') {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
moveFocus(1);
|
moveFocus(element, 1);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
for (let element of document.querySelectorAll(".keyboard-focus-control")) {
|
||||||
|
const captureElement = element;
|
||||||
|
element.addEventListener('keydown', ev => focusControl(captureElement, ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultKeyboardFocusControl = document.querySelector(".default-keyboard-focus-control");
|
||||||
|
if (defaultKeyboardFocusControl) {
|
||||||
|
document.addEventListener('keydown', ev => {
|
||||||
|
focusControl(defaultKeyboardFocusControl, ev);
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
url('amatic-sc-v9-latin-regular.woff') format('woff');
|
url('amatic-sc-v9-latin-regular.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prototype {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: "Apple Garamond", "Baskerville",
|
font-family: "Apple Garamond", "Baskerville",
|
||||||
"Times New Roman", "Droid Serif", "Times",
|
"Times New Roman", "Droid Serif", "Times",
|
||||||
|
@ -292,13 +296,29 @@ article ul.search-results {
|
||||||
}
|
}
|
||||||
.search-result {
|
.search-result {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 8px 16px;
|
padding: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.search-result .title {
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.snippet {
|
.snippet {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
.search-result p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result a {
|
||||||
|
display: block;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.search-result a:hover, .search-result a:focus {
|
||||||
|
background: #0074D9;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -347,33 +367,10 @@ article ul.search-results {
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-results .search-result {
|
.live-results .search-result {
|
||||||
padding: 0;
|
|
||||||
border-top: none;
|
border-top: none;
|
||||||
margin: 0;
|
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, .live-results .search-result a:focus {
|
|
||||||
background: #0074D9;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.live-results .search-result .title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prototype {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 630px) {
|
@media (min-width: 630px) {
|
||||||
.search {
|
.search {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
|
@ -128,29 +128,36 @@ impl Resource for SearchResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(self: Box<Self>) -> ResponseFuture {
|
fn get(self: Box<Self>) -> ResponseFuture {
|
||||||
#[derive(BartDisplay)]
|
|
||||||
#[template="templates/search.html"]
|
|
||||||
struct Template<'a> {
|
|
||||||
query: &'a str,
|
|
||||||
hits: Vec<models::SearchResult>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct JsonResponse<'a> {
|
struct JsonResponse<'a> {
|
||||||
query: &'a str,
|
query: &'a str,
|
||||||
hits: &'a [models::SearchResult],
|
hits: &'a [models::SearchResult],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl models::SearchResult {
|
struct Hit<'a> {
|
||||||
fn link(&self) -> String {
|
index: usize,
|
||||||
|
slug: &'a str,
|
||||||
|
title: &'a str,
|
||||||
|
snippet: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Hit<'a> {
|
||||||
|
fn link(&self) -> &'a str {
|
||||||
if self.slug == "" {
|
if self.slug == "" {
|
||||||
".".to_owned()
|
"."
|
||||||
} else {
|
} else {
|
||||||
self.slug.clone()
|
self.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(BartDisplay)]
|
||||||
|
#[template="templates/search.html"]
|
||||||
|
struct Template<'a> {
|
||||||
|
query: &'a str,
|
||||||
|
hits: &'a [Hit<'a>],
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Show a search "front page" when no query is given:
|
// TODO: Show a search "front page" when no query is given:
|
||||||
let query = self.query.as_ref().map(|x| x.clone()).unwrap_or("".to_owned());
|
let query = self.query.as_ref().map(|x| x.clone()).unwrap_or("".to_owned());
|
||||||
|
|
||||||
|
@ -172,7 +179,15 @@ impl Resource for SearchResource {
|
||||||
title: "Search",
|
title: "Search",
|
||||||
body: &Template {
|
body: &Template {
|
||||||
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
|
||||||
hits: data,
|
hits: &data.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, result)| Hit {
|
||||||
|
index: i,
|
||||||
|
slug: &result.slug,
|
||||||
|
title: &result.title,
|
||||||
|
snippet: &result.snippet,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
},
|
},
|
||||||
}.to_string())),
|
}.to_string())),
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<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>
|
<form class="search keyboard-focus-control" action=_search method=GET>
|
||||||
<input data-focusindex="0" type=search name=q placeholder=search autocomplete=off>
|
<input data-focusindex="0" type=search name=q placeholder=search autocomplete=off>
|
||||||
<ul class="live-results search-results">
|
<ul class="live-results search-results">
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
{{#hits?}}
|
{{#hits?}}
|
||||||
<p>Search results for the query <b>{{query}}</b>:</p>
|
<p>Search results for the query <b>{{query}}</b>:</p>
|
||||||
|
|
||||||
<ul class="search-results">
|
<ul class="search-results default-keyboard-focus-control">
|
||||||
{{#hits}}
|
{{#hits}}
|
||||||
<li class="search-result"><a href="{{.link()}}">{{.title}}</a> – <span class="snippet">{{.snippet}}</span></li>
|
<li class="search-result"><a data-focusindex="{{.index}}" class="link" href="{{.link()}}"><p class="title">{{.title}}</p><p class="snippet">{{.snippet}}</p></a></li>
|
||||||
{{/hits}}
|
{{/hits}}
|
||||||
</ul>
|
</ul>
|
||||||
{{/hits}}
|
{{/hits}}
|
||||||
|
|
Loading…
Reference in a new issue