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

View file

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

View file

@ -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<Resource + Sync + Send>;
#[derive(Serialize, Deserialize, Default)]
pub struct QueryParameters {
q: Option<String>,
offset: Option<i32>,
limit: Option<i32>,
snippet_size: Option<i32>,
offset: Option<u32>,
limit: Option<u32>,
snippet_size: Option<u32>,
}
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<String>,
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<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 }
}
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<String>,
next: Option<String>,
}
struct Hit<'a> {
@ -156,21 +175,44 @@ impl Resource for SearchResource {
struct Template<'a> {
query: &'a str,
hits: &'a [Hit<'a>],
prev: Option<String>,
next: Option<String>,
}
// 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::<Vec<_>>(),
prev,
next,
},
}.to_string())),
}

View file

@ -7,6 +7,10 @@
{{#hits?}}
<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">
{{#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>
@ -14,6 +18,10 @@
</ul>
{{/hits}}
{{#next}}<nav><ul class="dense"
><li><a rel="next" href="{{.}}">Next page</a></li
></ul></nav>{{/next}}
{{^hits?}}
<p>Your search for <b>{{query}}</b> gave no results.</p>
{{/hits}}