use super::*;
const PATH_SEPARATOR: &str = " / ";
const CARET_GLYPH: &str = "\u{25be}";
#[component]
pub fn euv_cascader(node: VirtualNode<EuvCascaderProps>) -> VirtualNode {
let EuvCascaderProps {
options,
value,
placeholder,
label: label_text,
on_select,
}: EuvCascaderProps = node.try_get_props().unwrap_or_default();
let chosen: Vec<String> = value.get();
let open: Signal<bool> = HookContext::use_hook(|| Signal::create(false));
let browsed: Signal<Vec<String>> = HookContext::use_hook(|| Signal::create(chosen.clone()));
let browsed_path: Vec<String> = browsed.get();
let labels: Vec<&'static str> = cascader_path_labels(&options, &chosen);
let has_path: bool = !labels.is_empty();
let trigger_text: String = if has_path {
labels.join(PATH_SEPARATOR)
} else {
String::from(placeholder)
};
let columns: Vec<&[EuvCascaderOption]> = cascader_columns(&options, &browsed_path);
let column_nodes: Vec<VirtualNode> = columns
.iter()
.enumerate()
.map(|entry: (usize, &&[EuvCascaderOption])| {
let depth: usize = entry.0;
let column: &&[EuvCascaderOption] = entry.1;
let active_value: Option<&String> = browsed_path.get(depth);
let rows: Vec<VirtualNode> = column
.iter()
.map(|option: &EuvCascaderOption| {
let is_active: bool =
active_value.is_some_and(|active: &String| active == option.value);
cascader_option_row(
option,
depth,
is_active,
value,
browsed,
open,
on_select.clone(),
)
})
.collect();
html! {
div {
class: c_euv_cascader_column()
rows
}
}
})
.collect();
html! {
div {
class: c_euv_cascader()
if !label_text.is_empty() {
label {
class: c_form_label()
label_text
}
}
button {
type: "button"
class: c_euv_cascader_trigger()
onclick: cascader_trigger_handler(open, browsed, value)
span {
class: if has_path {
c_euv_cascader_value()
} else {
c_euv_cascader_placeholder()
}
trigger_text
}
span {
class: c_euv_cascader_caret()
CARET_GLYPH
}
}
div {
class: c_euv_cascader_panel()
class: if { open } {
c_euv_cascader_panel_open()
} else {
c_euv_cascader_panel_closed()
}
div {
class: c_euv_cascader_columns()
column_nodes
}
}
}
}
}
pub fn cascader_path_labels(options: &[EuvCascaderOption], path: &[String]) -> Vec<&'static str> {
let mut labels: Vec<&'static str> = Vec::with_capacity(path.len());
let mut level: &[EuvCascaderOption] = options;
for value in path {
let Some(choice) = level
.iter()
.find(|option: &&EuvCascaderOption| option.value == value.as_str())
else {
break;
};
labels.push(choice.label);
level = &choice.children;
}
labels
}
pub fn cascader_columns<'a>(
options: &'a [EuvCascaderOption],
path: &[String],
) -> Vec<&'a [EuvCascaderOption]> {
let mut columns: Vec<&'a [EuvCascaderOption]> = vec![options];
let mut level: &'a [EuvCascaderOption] = options;
for value in path {
let Some(choice) = level
.iter()
.find(|option: &&EuvCascaderOption| option.value == value.as_str())
else {
break;
};
if choice.children.is_empty() {
break;
}
level = &choice.children;
columns.push(level);
}
columns
}
fn cascader_option_row(
option: &EuvCascaderOption,
depth: usize,
is_active: bool,
value: Signal<Vec<String>>,
browsed: Signal<Vec<String>>,
open: Signal<bool>,
on_select: Option<Rc<dyn Fn(&'static str)>>,
) -> VirtualNode {
let option_value: &'static str = option.value;
let option_label: &'static str = option.label;
let has_children: bool = !option.children.is_empty();
let handler: Option<Rc<dyn Fn(Event)>> = cascader_option_handler(
value,
browsed,
open,
on_select,
option_value,
depth,
has_children,
);
if is_active {
html! {
button {
type: "button"
class: c_euv_cascader_option()
class: c_euv_cascader_option_active()
onclick: handler
option_label
}
}
} else {
html! {
button {
type: "button"
class: c_euv_cascader_option()
onclick: handler
option_label
}
}
}
}
pub fn cascader_option_handler<E: 'static>(
value: Signal<Vec<String>>,
browsed: Signal<Vec<String>>,
open: Signal<bool>,
on_select: Option<Rc<dyn Fn(&'static str)>>,
option_value: &'static str,
depth: usize,
has_children: bool,
) -> Option<Rc<dyn Fn(E)>> {
Some(Rc::new(move |_: E| {
let mut path: Vec<String> = browsed.get();
path.truncate(depth);
path.push(String::from(option_value));
browsed.set(path.clone());
if has_children {
open.set(true);
return;
}
value.set(path);
if let Some(callback) = &on_select {
callback(option_value);
}
open.set(false);
}))
}
pub fn cascader_trigger_handler<E: 'static>(
open: Signal<bool>,
browsed: Signal<Vec<String>>,
value: Signal<Vec<String>>,
) -> Option<Rc<dyn Fn(E)>> {
Some(Rc::new(move |_: E| {
if open.get() {
open.set(false);
return;
}
browsed.set(value.get());
open.set(true);
}))
}