use dioxus::prelude::*;
use std::collections::HashSet;
#[derive(Clone, PartialEq)]
pub struct TreeNodeData {
pub label: String,
pub children: Vec<TreeNodeData>,
pub disabled: bool,
}
impl TreeNodeData {
pub fn new(label: &str) -> Self {
Self { label: label.to_string(), children: vec![], disabled: false }
}
pub fn child(mut self, node: TreeNodeData) -> Self {
self.children.push(node);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
#[derive(Props, Clone, PartialEq)]
pub struct TreeProps {
#[props(default)]
pub data: Vec<TreeNodeData>,
#[props(default = false)]
pub show_checkbox: bool,
#[props(default = false)]
pub default_expand_all: bool,
#[props(default)]
pub expanded_keys: Vec<String>,
#[props(default)]
pub checked_keys: Vec<String>,
#[props(default)]
pub current_key: Option<String>,
#[props(default = false)]
pub highlight_current: bool,
#[props(default = true)]
pub expand_on_click_node: bool,
#[props(default)]
pub on_node_click: Option<EventHandler<String>>,
#[props(default)]
pub on_node_expand: Option<EventHandler<(String, bool)>>,
#[props(default)]
pub on_node_check: Option<EventHandler<(String, bool)>>,
#[props(default)]
pub class: Option<String>,
#[props(default)]
pub style: Option<String>,
}
#[component]
pub fn Tree(props: TreeProps) -> Element {
let mut class_names = vec!["el-tree".to_string()];
if let Some(ref c) = props.class {
class_names.push(c.clone());
}
let expanded_set: HashSet<String> = if props.default_expand_all {
collect_all_labels(&props.data).into_iter().collect()
} else {
props.expanded_keys.iter().cloned().collect()
};
let checked_set: HashSet<String> = props.checked_keys.iter().cloned().collect();
let current_key = props.current_key.clone();
let node_data: Vec<TreeNodeRenderData> = props
.data
.iter()
.map(|node| build_node_render_data(node, 0, &expanded_set, &checked_set, ¤t_key, props.highlight_current, props.show_checkbox))
.collect();
rsx! {
div {
class: "{class_names.join(\" \")}",
style: props.style.clone().unwrap_or_default(),
role: "tree",
for render_node in node_data.into_iter() {
TreeChild {
node: render_node,
show_checkbox: props.show_checkbox,
highlight_current: props.highlight_current,
expand_on_click: props.expand_on_click_node,
on_click: props.on_node_click,
on_expand: props.on_node_expand,
on_check: props.on_node_check,
}
}
}
}
}
#[derive(Clone, PartialEq)]
struct TreeNodeRenderData {
label: String,
disabled: bool,
level: u32,
is_expanded: bool,
is_checked: bool,
is_current: bool,
has_children: bool,
children: Vec<TreeNodeRenderData>,
}
fn build_node_render_data(
node: &TreeNodeData,
level: u32,
expanded: &HashSet<String>,
checked: &HashSet<String>,
current: &Option<String>,
highlight: bool,
show_checkbox: bool,
) -> TreeNodeRenderData {
let is_expanded = expanded.contains(&node.label);
let is_checked = checked.contains(&node.label);
let is_current = highlight && current.as_ref().map_or(false, |c| c == &node.label);
let has_children = !node.children.is_empty();
let children = if has_children {
node.children
.iter()
.map(|child| build_node_render_data(child, level + 1, expanded, checked, current, highlight, show_checkbox))
.collect()
} else {
vec![]
};
TreeNodeRenderData {
label: node.label.clone(),
disabled: node.disabled,
level,
is_expanded,
is_checked,
is_current,
has_children,
children,
}
}
fn collect_all_labels(data: &[TreeNodeData]) -> Vec<String> {
let mut result = vec![];
for node in data {
result.push(node.label.clone());
if !node.children.is_empty() {
result.extend(collect_all_labels(&node.children));
}
}
result
}
#[derive(Props, Clone, PartialEq)]
struct TreeChildProps {
node: TreeNodeRenderData,
show_checkbox: bool,
highlight_current: bool,
expand_on_click: bool,
on_click: Option<EventHandler<String>>,
on_expand: Option<EventHandler<(String, bool)>>,
on_check: Option<EventHandler<(String, bool)>>,
}
#[component]
fn TreeChild(props: TreeChildProps) -> Element {
let node_label = props.node.label.clone();
let padding = props.node.level * 18;
let has_children = props.node.has_children;
let is_expanded = props.node.is_expanded;
let is_current = props.node.is_current;
let is_checked = props.node.is_checked;
let disabled = props.node.disabled;
let show_checkbox = props.show_checkbox;
let expand_on_click = props.expand_on_click;
let on_click = props.on_click;
let on_expand = props.on_expand;
let on_check = props.on_check;
let label_for_click = node_label.clone();
let label_for_expand = node_label.clone();
let label_for_expand_icon = node_label.clone();
let label_for_check = node_label.clone();
let child_nodes: Vec<TreeNodeRenderData> = if has_children && is_expanded {
props.node.children.clone()
} else {
vec![]
};
let content_class = {
let mut cls = vec!["el-tree-node__content".to_string()];
if is_current {
cls.push("is-current".to_string());
}
if disabled {
cls.push("is-disabled".to_string());
}
cls.join(" ")
};
let expand_icon_class = if is_expanded {
"el-tree-node__expand-icon el-icon-caret-right expanded"
} else {
"el-tree-node__expand-icon el-icon-caret-right"
};
let checkbox_class = {
let mut cls = vec!["el-checkbox".to_string()];
if is_checked {
cls.push("is-checked".to_string());
}
cls.push("el-tree-node__checkbox".to_string());
cls.join(" ")
};
rsx! {
div {
class: "el-tree-node",
role: "treeitem",
"aria-expanded": "{is_expanded}",
div {
class: "{content_class}",
style: "padding-left: {padding}px;",
onclick: move |_| {
if !disabled {
if let Some(handler) = on_click.as_ref() {
handler.call(label_for_click.clone());
}
if expand_on_click && has_children {
if let Some(handler) = on_expand.as_ref() {
handler.call((label_for_expand.clone(), !is_expanded));
}
}
}
},
if has_children {
span {
class: "{expand_icon_class}",
onclick: move |e: Event<MouseData>| {
e.stop_propagation();
if !disabled {
if let Some(handler) = on_expand.as_ref() {
handler.call((label_for_expand_icon.clone(), !is_expanded));
}
}
},
}
} else {
span {
class: "el-tree-node__expand-icon is-leaf",
}
}
if show_checkbox {
span {
class: "{checkbox_class}",
onclick: move |e: Event<MouseData>| {
e.stop_propagation();
if !disabled {
if let Some(handler) = on_check.as_ref() {
handler.call((label_for_check.clone(), !is_checked));
}
}
},
span { class: "el-checkbox__inner" }
}
}
span {
class: "el-tree-node__label",
"{props.node.label}"
}
}
if has_children && is_expanded {
div {
class: "el-tree-node__children",
role: "group",
for child in child_nodes.into_iter() {
TreeChild {
node: child,
show_checkbox: show_checkbox,
highlight_current: props.highlight_current,
expand_on_click: expand_on_click,
on_click: on_click,
on_expand: on_expand,
on_check: on_check,
}
}
}
}
}
}
}