Prev and next-links for search result. "More"-link for instant search

This commit is contained in:
Magnus Hoff 2017-10-25 16:34:21 +02:00
parent 74b8040d39
commit b81137ad25
4 changed files with 92 additions and 16 deletions

View file

@ -18,6 +18,7 @@ function debouncer(interval, callback) {
const results = form.querySelector('.live-results'); const results = form.querySelector('.live-results');
const resultPrototype = document.getElementById('search-result-prototype').firstChild; const resultPrototype = document.getElementById('search-result-prototype').firstChild;
let ongoing = false;
function submit() { function submit() {
if (input.value === "") { if (input.value === "") {
results.classList.remove("show"); results.classList.remove("show");
@ -25,8 +26,12 @@ function debouncer(interval, callback) {
return; return;
} }
if (ongoing) return;
ongoing = true;
const query = input.value;
fetch( fetch(
"_search?snippet_size=4&limit=3&q=" + encodeURIComponent(input.value), "_search?snippet_size=4&limit=4&q=" + encodeURIComponent(query),
{ {
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@ -48,13 +53,27 @@ function debouncer(interval, callback) {
item.querySelector('.snippet').textContent = hit.snippet; item.querySelector('.snippet').textContent = hit.snippet;
results.appendChild(item); results.appendChild(item);
}) })
if (result.next) {
const item = resultPrototype.cloneNode(true);
item.querySelector('.link').href = "_search?q=" + encodeURIComponent(query);
item.querySelector('.link').setAttribute("data-focusindex", result.hits.length + 1);
item.querySelector('.link').innerHTML = "See more results\u2026";
results.appendChild(item);
}
results.classList.add("show"); results.classList.add("show");
if (input.value !== query) submitter();
ongoing = false;
}).catch(err => { }).catch(err => {
ongoing = false;
console.error(err); console.error(err);
alert(err); alert(err); // TODO Better interactive error reporting
}); });
} }
const submitter = debouncer(200, submit); const submitter = debouncer(300, submit);
input.addEventListener('input', submitter); input.addEventListener('input', submitter);
@ -85,6 +104,10 @@ function debouncer(interval, callback) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
moveFocus(element, 1); moveFocus(element, 1);
} else if (ev.key === 'Escape') {
ev.preventDefault();
ev.stopPropagation();
document.activeElement && document.activeElement.blur();
} }
} }

View file

@ -338,6 +338,7 @@ article ul.search-results {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
box-sizing: border-box; box-sizing: border-box;
text-overflow: ellipsis;
} }
.search input:focus, .search.focus input { .search input:focus, .search.focus input {

View file

@ -11,34 +11,41 @@ use site::Layout;
use state::State; use state::State;
use web::{Resource, ResponseFuture}; use web::{Resource, ResponseFuture};
const DEFAULT_LIMIT: i32 = 10; const DEFAULT_LIMIT: u32 = 10;
const DEFAULT_SNIPPET_SIZE: i32 = 8; const DEFAULT_SNIPPET_SIZE: u32 = 8;
type BoxResource = Box<Resource + Sync + Send>; type BoxResource = Box<Resource + Sync + Send>;
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
pub struct QueryParameters { pub struct QueryParameters {
q: Option<String>, q: Option<String>,
offset: Option<i32>, offset: Option<u32>,
limit: Option<i32>, limit: Option<u32>,
snippet_size: Option<i32>, snippet_size: Option<u32>,
} }
impl QueryParameters { impl QueryParameters {
pub fn offset(self, offset: i32) -> Self { pub fn offset(self, offset: u32) -> Self {
Self { Self {
offset: if offset != 0 { Some(offset) } else { None }, offset: if offset != 0 { Some(offset) } else { None },
..self ..self
} }
} }
pub fn limit(self, limit: i32) -> Self { pub fn limit(self, limit: u32) -> Self {
Self { Self {
limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None }, limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None },
..self ..self
} }
} }
pub fn snippet_size(self, snippet_size: u32) -> Self {
Self {
snippet_size: if snippet_size != DEFAULT_SNIPPET_SIZE { Some(snippet_size) } else { None },
..self
}
}
pub fn into_link(self) -> String { pub fn into_link(self) -> String {
let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail"); let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail");
if args.len() > 0 { if args.len() > 0 {
@ -79,9 +86,9 @@ pub struct SearchResource {
response_type: ResponseType, response_type: ResponseType,
query: Option<String>, query: Option<String>,
limit: i32, limit: u32,
offset: i32, offset: u32,
snippet_size: i32, snippet_size: u32,
} }
// This is a complete hack, searching for a reasonable design: // This is a complete hack, searching for a reasonable design:
@ -91,9 +98,19 @@ pub enum ResponseType {
} }
impl SearchResource { impl SearchResource {
pub fn new(state: State, query: Option<String>, limit: i32, offset: i32, snippet_size: i32) -> Self { pub fn new(state: State, query: Option<String>, limit: u32, offset: u32, snippet_size: u32) -> Self {
Self { state, response_type: ResponseType::Html, query, limit, offset, snippet_size } Self { state, response_type: ResponseType::Html, query, limit, offset, snippet_size }
} }
fn query_args(&self) -> QueryParameters {
QueryParameters {
q: self.query.clone(),
..QueryParameters::default()
}
.offset(self.offset)
.limit(self.limit)
.snippet_size(self.snippet_size)
}
} }
impl Resource for SearchResource { impl Resource for SearchResource {
@ -132,6 +149,8 @@ impl Resource for SearchResource {
struct JsonResponse<'a> { struct JsonResponse<'a> {
query: &'a str, query: &'a str,
hits: &'a [models::SearchResult], hits: &'a [models::SearchResult],
prev: Option<String>,
next: Option<String>,
} }
struct Hit<'a> { struct Hit<'a> {
@ -156,21 +175,44 @@ impl Resource for SearchResource {
struct Template<'a> { struct Template<'a> {
query: &'a str, query: &'a str,
hits: &'a [Hit<'a>], hits: &'a [Hit<'a>],
prev: Option<String>,
next: Option<String>,
} }
// 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());
let data = self.state.search_query(query, self.limit, self.offset, self.snippet_size); let data = self.state.search_query(query, (self.limit + 1) as i32, self.offset as i32, self.snippet_size as i32);
let head = self.head(); let head = self.head();
Box::new(data.join(head) Box::new(data.join(head)
.and_then(move |(data, head)| { .and_then(move |(mut data, head)| {
let prev = if self.offset > 0 {
Some(self.query_args()
.offset(self.offset.saturating_sub(self.limit))
.into_link()
)
} else {
None
};
let next = if data.len() > self.limit as usize {
data.pop();
Some(self.query_args()
.offset(self.offset + self.limit)
.into_link()
)
} else {
None
};
match &self.response_type { match &self.response_type {
&ResponseType::Json => Ok(head &ResponseType::Json => Ok(head
.with_body(serde_json::to_string(&JsonResponse { .with_body(serde_json::to_string(&JsonResponse {
query: self.query.as_ref().map(|x| &**x).unwrap_or(""), query: self.query.as_ref().map(|x| &**x).unwrap_or(""),
hits: &data, hits: &data,
prev,
next,
}).expect("Should never fail")) }).expect("Should never fail"))
), ),
&ResponseType::Html => Ok(head &ResponseType::Html => Ok(head
@ -188,6 +230,8 @@ impl Resource for SearchResource {
snippet: &result.snippet, snippet: &result.snippet,
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
prev,
next,
}, },
}.to_string())), }.to_string())),
} }

View file

@ -7,6 +7,10 @@
{{#hits?}} {{#hits?}}
<p>Search results for the query <b>{{query}}</b>:</p> <p>Search results for the query <b>{{query}}</b>:</p>
{{#prev}}<nav><ul class="dense"
><li><a rel="prev" href="{{.}}">Previous page</a></li
></ul></nav>{{/prev}}
<ul class="search-results default-keyboard-focus-control"> <ul class="search-results default-keyboard-focus-control">
{{#hits}} {{#hits}}
<li class="search-result"><a data-focusindex="{{.index}}" class="link" href="{{.link()}}"><p class="title">{{.title}}</p><p class="snippet">{{.snippet}}</p></a></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>
@ -14,6 +18,10 @@
</ul> </ul>
{{/hits}} {{/hits}}
{{#next}}<nav><ul class="dense"
><li><a rel="next" href="{{.}}">Next page</a></li
></ul></nav>{{/next}}
{{^hits?}} {{^hits?}}
<p>Your search for <b>{{query}}</b> gave no results.</p> <p>Your search for <b>{{query}}</b> gave no results.</p>
{{/hits}} {{/hits}}