Prev and next-links for search result. "More"-link for instant search
This commit is contained in:
parent
74b8040d39
commit
b81137ad25
4 changed files with 92 additions and 16 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
Loading…
Reference in a new issue