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 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
|
Loading…
Reference in a new issue