use super::*;
const NO_MATCHES_TEXT: &str = "No matches";
pub fn auto_complete_matches(option: &EuvAutoCompleteOption, query: &str) -> bool {
matches_lowered(option, &query.to_lowercase())
}
fn matches_lowered(option: &EuvAutoCompleteOption, lowered_query: &str) -> bool {
if lowered_query.is_empty() {
return true;
}
option.label.to_lowercase().contains(lowered_query)
|| option.value.to_lowercase().contains(lowered_query)
}
pub fn auto_complete_active<'a>(
filtered: &'a [EuvAutoCompleteOption],
query: &str,
) -> Option<&'a EuvAutoCompleteOption> {
let exact: Option<&'a EuvAutoCompleteOption> = filtered
.iter()
.find(|option: &&EuvAutoCompleteOption| option.label == query || option.value == query);
exact.or_else(|| filtered.first())
}
fn on_auto_complete_input(
value: Signal<String>,
open: Signal<bool>,
has_options: bool,
on_change: Option<Rc<dyn Fn(&'static str)>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |event: Event| {
let reported: Option<String> = event.target().and_then(|target: EventTarget| {
if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
return Some(input.value());
}
None
});
let Some(reported) = reported else {
return;
};
value.set(reported.clone());
open.set(has_options && !reported.is_empty());
if let Some(callback) = &on_change {
let leaked: &'static str = Box::leak(reported.into_boxed_str());
callback(leaked);
}
}))
}
fn on_auto_complete_select(
value: Signal<String>,
open: Signal<bool>,
option_value: &'static str,
on_select: Option<Rc<dyn Fn(&'static str)>>,
) -> Option<Rc<dyn Fn(Event)>> {
Some(Rc::new(move |_event: Event| {
value.set(String::from(option_value));
open.set(false);
if let Some(callback) = &on_select {
callback(option_value);
}
}))
}
#[component]
pub fn euv_auto_complete(node: VirtualNode<EuvAutoCompleteProps>) -> VirtualNode {
let EuvAutoCompleteProps {
options,
value,
placeholder,
label: label_text,
on_change,
on_select,
}: EuvAutoCompleteProps = node.try_get_props().unwrap_or_default();
let open: Signal<bool> = HookContext::use_hook(|| Signal::create(false));
let query: String = value.get();
let lowered_query: String = query.to_lowercase();
let has_options: bool = !options.is_empty();
let filtered: Vec<EuvAutoCompleteOption> = options
.iter()
.filter(|option: &&EuvAutoCompleteOption| matches_lowered(option, &lowered_query))
.copied()
.collect();
let active: Option<&EuvAutoCompleteOption> = auto_complete_active(&filtered, &query);
let mut rows: Vec<VirtualNode> = filtered
.iter()
.map(|option: &EuvAutoCompleteOption| {
let option_value: &'static str = option.value;
let option_label: &'static str = option.label;
let is_active: bool = active
.map(|active_option: &EuvAutoCompleteOption| eq(active_option, option))
.unwrap_or(false);
let option_class: fn() -> &'static Css = if is_active {
c_euv_auto_complete_option_active
} else {
c_euv_auto_complete_option
};
html! {
button {
type: "button"
class: option_class()
onclick: on_auto_complete_select(value, open, option_value, on_select.clone())
option_label
}
}
})
.collect();
if rows.is_empty() && has_options && !query.is_empty() {
rows.push(html! {
div {
class: c_euv_auto_complete_empty()
NO_MATCHES_TEXT
}
});
}
html! {
div {
class: c_euv_auto_complete()
if !label_text.is_empty() {
label {
class: c_form_label()
label_text
}
}
input {
type: "text"
placeholder: placeholder
value: value
class: c_euv_auto_complete_input()
oninput: on_auto_complete_input(value, open, has_options, on_change.clone())
}
div {
class: c_euv_auto_complete_panel()
class: if { open } {
c_euv_auto_complete_panel_open()
} else {
c_euv_auto_complete_panel_closed()
}
rows
}
}
}
}