use super::*;
const MENTION_TRIGGER: char = '@';
#[component]
pub fn euv_mentions(node: VirtualNode<EuvMentionsProps>) -> VirtualNode {
let EuvMentionsProps {
items,
value,
placeholder,
label: label_text,
on_select,
}: EuvMentionsProps = node.try_get_props().unwrap_or_default();
let text: String = value.get();
let query: Option<&str> = mentions_trigger(&text);
let has_trigger: bool = query.is_some();
let open: Signal<bool> = HookContext::use_hook(|| Signal::create(has_trigger));
let filtered: Vec<EuvMentionItem> = match query {
Some(fragment) => mentions_filter(&items, fragment),
None => Vec::new(),
};
let rows: Vec<VirtualNode> = filtered
.iter()
.enumerate()
.map(|entry: (usize, &EuvMentionItem)| {
let index: usize = entry.0;
let item: &EuvMentionItem = entry.1;
let is_active: bool = index == 0;
let item_value: &'static str = item.value;
let item_label: &'static str = item.label;
html! {
button {
type: "button"
class: if is_active {
c_euv_mentions_option_active()
} else {
c_euv_mentions_option()
}
onclick: on_mentions_select(value, open, item_value, item_label, on_select.clone())
item_label
}
}
})
.collect();
html! {
div {
class: c_euv_mentions()
if !label_text.is_empty() {
label {
class: c_form_label()
label_text
}
}
input {
type: "text"
placeholder: placeholder
value: value
class: c_euv_mentions_input()
oninput: on_mentions_input(value, open)
}
div {
class: c_euv_mentions_panel()
class: if { open } {
c_euv_mentions_panel_open()
} else {
c_euv_mentions_panel_closed()
}
rows
}
}
}
}
pub fn mentions_trigger(text: &str) -> Option<&str> {
let at: usize = text.rfind(MENTION_TRIGGER)?;
let fragment: &str = &text[at + MENTION_TRIGGER.len_utf8()..];
if fragment.is_empty() || fragment.chars().any(char::is_whitespace) {
return None;
}
Some(fragment)
}
pub fn mentions_matches(item: &EuvMentionItem, query: &str) -> bool {
mentions_matches_lowered(item, &query.to_lowercase())
}
fn mentions_matches_lowered(item: &EuvMentionItem, lowered_query: &str) -> bool {
if lowered_query.is_empty() {
return true;
}
item.label.to_lowercase().contains(lowered_query)
|| item.value.to_lowercase().contains(lowered_query)
}
pub fn mentions_filter(items: &[EuvMentionItem], query: &str) -> Vec<EuvMentionItem> {
let lowered: String = query.to_lowercase();
items
.iter()
.filter(|item: &&EuvMentionItem| mentions_matches_lowered(item, &lowered))
.copied()
.collect()
}
pub fn mentions_apply(text: &str, label: &str) -> String {
let Some(at) = text.rfind(MENTION_TRIGGER) else {
return String::from(text);
};
let mut applied: String = String::with_capacity(text.len() + label.len());
applied.push_str(&text[..at + MENTION_TRIGGER.len_utf8()]);
applied.push_str(label);
applied
}
fn on_mentions_input(value: Signal<String>, open: Signal<bool>) -> 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;
};
open.set(mentions_trigger(&reported).is_some());
value.set(reported);
}))
}
pub fn on_mentions_select<E: 'static>(
value: Signal<String>,
open: Signal<bool>,
item_value: &'static str,
item_label: &'static str,
on_select: Option<Rc<dyn Fn(&'static str)>>,
) -> Option<Rc<dyn Fn(E)>> {
Some(Rc::new(move |_: E| {
value.set(mentions_apply(&value.get(), item_label));
if let Some(callback) = &on_select {
callback(item_value);
}
open.set(false);
}))
}