From b81137ad254331e8c16468225bb2fcee0b500f1d Mon Sep 17 00:00:00 2001 From: Magnus Hoff Date: Wed, 25 Oct 2017 16:34:21 +0200 Subject: [PATCH] Prev and next-links for search result. "More"-link for instant search --- assets/search.js | 29 +++++++++++-- assets/style.css | 1 + src/resources/search_resource.rs | 70 ++++++++++++++++++++++++++------ templates/search.html | 8 ++++ 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/assets/search.js b/assets/search.js index caa8b72..69a5045 100644 --- a/assets/search.js +++ b/assets/search.js @@ -18,6 +18,7 @@ function debouncer(interval, callback) { const results = form.querySelector('.live-results'); const resultPrototype = document.getElementById('search-result-prototype').firstChild; + let ongoing = false; function submit() { if (input.value === "") { results.classList.remove("show"); @@ -25,8 +26,12 @@ function debouncer(interval, callback) { return; } + if (ongoing) return; + ongoing = true; + + const query = input.value; fetch( - "_search?snippet_size=4&limit=3&q=" + encodeURIComponent(input.value), + "_search?snippet_size=4&limit=4&q=" + encodeURIComponent(query), { headers: { "Accept": "application/json", @@ -48,13 +53,27 @@ function debouncer(interval, callback) { item.querySelector('.snippet').textContent = hit.snippet; 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"); + + if (input.value !== query) submitter(); + + ongoing = false; }).catch(err => { + ongoing = false; 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); @@ -85,6 +104,10 @@ function debouncer(interval, callback) { ev.preventDefault(); ev.stopPropagation(); moveFocus(element, 1); + } else if (ev.key === 'Escape') { + ev.preventDefault(); + ev.stopPropagation(); + document.activeElement && document.activeElement.blur(); } } diff --git a/assets/style.css b/assets/style.css index e16f4b5..b81c9e6 100644 --- a/assets/style.css +++ b/assets/style.css @@ -338,6 +338,7 @@ article ul.search-results { width: 100%; max-width: 300px; box-sizing: border-box; + text-overflow: ellipsis; } .search input:focus, .search.focus input { diff --git a/src/resources/search_resource.rs b/src/resources/search_resource.rs index c8dc71b..4c5e808 100644 --- a/src/resources/search_resource.rs +++ b/src/resources/search_resource.rs @@ -11,34 +11,41 @@ use site::Layout; use state::State; use web::{Resource, ResponseFuture}; -const DEFAULT_LIMIT: i32 = 10; -const DEFAULT_SNIPPET_SIZE: i32 = 8; +const DEFAULT_LIMIT: u32 = 10; +const DEFAULT_SNIPPET_SIZE: u32 = 8; type BoxResource = Box; #[derive(Serialize, Deserialize, Default)] pub struct QueryParameters { q: Option, - offset: Option, - limit: Option, - snippet_size: Option, + offset: Option, + limit: Option, + snippet_size: Option, } impl QueryParameters { - pub fn offset(self, offset: i32) -> Self { + pub fn offset(self, offset: u32) -> Self { Self { offset: if offset != 0 { Some(offset) } else { None }, ..self } } - pub fn limit(self, limit: i32) -> Self { + pub fn limit(self, limit: u32) -> Self { Self { limit: if limit != DEFAULT_LIMIT { Some(limit) } else { None }, ..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 { let args = serde_urlencoded::to_string(self).expect("Serializing to String cannot fail"); if args.len() > 0 { @@ -79,9 +86,9 @@ pub struct SearchResource { response_type: ResponseType, query: Option, - limit: i32, - offset: i32, - snippet_size: i32, + limit: u32, + offset: u32, + snippet_size: u32, } // This is a complete hack, searching for a reasonable design: @@ -91,9 +98,19 @@ pub enum ResponseType { } impl SearchResource { - pub fn new(state: State, query: Option, limit: i32, offset: i32, snippet_size: i32) -> Self { + pub fn new(state: State, query: Option, limit: u32, offset: u32, snippet_size: u32) -> Self { 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 { @@ -132,6 +149,8 @@ impl Resource for SearchResource { struct JsonResponse<'a> { query: &'a str, hits: &'a [models::SearchResult], + prev: Option, + next: Option, } struct Hit<'a> { @@ -156,21 +175,44 @@ impl Resource for SearchResource { struct Template<'a> { query: &'a str, hits: &'a [Hit<'a>], + prev: Option, + next: Option, } // 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 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(); 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 { &ResponseType::Json => Ok(head .with_body(serde_json::to_string(&JsonResponse { query: self.query.as_ref().map(|x| &**x).unwrap_or(""), hits: &data, + prev, + next, }).expect("Should never fail")) ), &ResponseType::Html => Ok(head @@ -188,6 +230,8 @@ impl Resource for SearchResource { snippet: &result.snippet, }) .collect::>(), + prev, + next, }, }.to_string())), } diff --git a/templates/search.html b/templates/search.html index bca5d19..c6b64ef 100644 --- a/templates/search.html +++ b/templates/search.html @@ -7,6 +7,10 @@ {{#hits?}}

Search results for the query {{query}}:

+{{#prev}}{{/prev}} + {{/hits}} +{{#next}}{{/next}} + {{^hits?}}

Your search for {{query}} gave no results.

{{/hits}}