recipes/frontend/src/search.rs

143 lines
4.5 KiB
Rust

use common::web_api;
use futures::{
StreamExt,
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
};
use gloo::{events::EventListener, utils::document};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{Element, HtmlDivElement, HtmlInputElement, HtmlSpanElement, KeyboardEvent};
use crate::{
ron_request,
utils::{SelectorExt, get_current_lang, selector, selector_and_clone},
};
const MAX_NB_SEARCH_RESULT: u32 = 10;
pub fn setup() {
// Here we check if the search input exists.
// Otherwise the search elements are considered as not present.
let input_search = match document().query_selector("#search input") {
Ok(Some(e)) => e.dyn_into::<HtmlInputElement>().unwrap(),
_ => return,
};
let (sender, mut receiver): (UnboundedSender<String>, UnboundedReceiver<String>) = unbounded();
spawn_local(async move {
while let Some(search_term) = receiver.next().await {
perform_search(search_term).await;
}
});
EventListener::new(&input_search, "input", move |event| {
let input_search: HtmlInputElement = event.target().unwrap().dyn_into().unwrap();
let search_term = input_search.value().trim().to_string();
sender.unbounded_send(search_term).unwrap();
})
.forget();
EventListener::new(&input_search, "keydown", move |event| {
let key_event: &KeyboardEvent = event.dyn_ref().unwrap();
if key_event.key() == "Escape" {
let input_search: HtmlInputElement = key_event.target().unwrap().dyn_into().unwrap();
input_search.set_value("");
selector::<HtmlDivElement>("#search .results")
.style()
.set_property("display", "none")
.unwrap();
}
})
.forget();
}
async fn perform_search(search_term: String) {
let results_element: HtmlDivElement = selector("#search .results");
if search_term.is_empty() {
results_element
.style()
.set_property("display", "none")
.unwrap();
return;
}
results_element
.selector::<Element>("ul")
.set_text_content(None);
if search_term.len() < common::consts::MIN_SEARCH_STRING_LENGTH {
display_message(Message::TooFewCharacters);
} else {
display_message(Message::None);
let search_result: Vec<web_api::RecipeSearchResult> = ron_request::get_with_params(
"/ron-api/recipe/search",
web_api::SearchByTitleParams {
search_term,
max_nb_results: MAX_NB_SEARCH_RESULT,
},
)
.await
.unwrap();
if search_result.is_empty() {
display_message(Message::NoResults);
} else {
let ul_element = results_element.selector::<Element>("ul");
for recipe in search_result {
let li_element: Element = selector_and_clone("#hidden-templates-search li");
let a_element = li_element.selector::<Element>("a");
a_element
.set_attribute(
"href",
&format!("/{}/recipe/view/{}", get_current_lang(), recipe.recipe_id),
)
.unwrap();
a_element.set_inner_html(&recipe.title_highlighted);
ul_element.append_child(&li_element).unwrap();
}
}
}
results_element
.style()
.set_property("display", "block")
.unwrap();
}
enum Message {
None,
TooFewCharacters,
NoResults,
}
fn display_message(message: Message) {
let message_element: HtmlDivElement = selector("#search .message");
for m in message_element.selector_all::<HtmlSpanElement>("span") {
m.style().set_property("display", "none").unwrap();
}
message_element
.style()
.set_property("display", "block")
.unwrap();
match message {
Message::TooFewCharacters => message_element
.selector::<HtmlSpanElement>(".to-small")
.style()
.set_property("display", "inline")
.unwrap(),
Message::NoResults => message_element
.selector::<HtmlSpanElement>(".no-results")
.style()
.set_property("display", "inline")
.unwrap(),
Message::None => message_element
.style()
.set_property("display", "none")
.unwrap(),
}
}