Refactor CSS for search results. Add support for keyboard control to full search results page

This commit is contained in:
Magnus Hoff 2017-10-25 16:02:30 +02:00
parent b90d7b9d1e
commit 74b8040d39
5 changed files with 71 additions and 49 deletions

View file

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

View file

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

View file

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

View file

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

View file

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