dioxus_element_plug/components/
tree.rs1use dioxus::prelude::*;
2use std::collections::HashSet;
3
4#[derive(Clone, PartialEq)]
6pub struct TreeNodeData {
7 pub label: String,
8 pub children: Vec<TreeNodeData>,
9 pub disabled: bool,
10}
11
12impl TreeNodeData {
13 pub fn new(label: &str) -> Self {
14 Self { label: label.to_string(), children: vec![], disabled: false }
15 }
16
17 pub fn child(mut self, node: TreeNodeData) -> Self {
18 self.children.push(node);
19 self
20 }
21
22 pub fn disabled(mut self, disabled: bool) -> Self {
23 self.disabled = disabled;
24 self
25 }
26}
27
28#[derive(Props, Clone, PartialEq)]
30pub struct TreeProps {
31 #[props(default)]
33 pub data: Vec<TreeNodeData>,
34
35 #[props(default = false)]
37 pub show_checkbox: bool,
38
39 #[props(default = false)]
41 pub default_expand_all: bool,
42
43 #[props(default)]
45 pub expanded_keys: Vec<String>,
46
47 #[props(default)]
49 pub checked_keys: Vec<String>,
50
51 #[props(default)]
53 pub current_key: Option<String>,
54
55 #[props(default = false)]
57 pub highlight_current: bool,
58
59 #[props(default = true)]
61 pub expand_on_click_node: bool,
62
63 #[props(default)]
65 pub on_node_click: Option<EventHandler<String>>,
66
67 #[props(default)]
69 pub on_node_expand: Option<EventHandler<(String, bool)>>,
70
71 #[props(default)]
73 pub on_node_check: Option<EventHandler<(String, bool)>>,
74
75 #[props(default)]
76 pub class: Option<String>,
77
78 #[props(default)]
79 pub style: Option<String>,
80}
81
82#[component]
107pub fn Tree(props: TreeProps) -> Element {
108 let mut class_names = vec!["el-tree".to_string()];
109 if let Some(ref c) = props.class {
110 class_names.push(c.clone());
111 }
112
113 let expanded_set: HashSet<String> = if props.default_expand_all {
115 collect_all_labels(&props.data).into_iter().collect()
116 } else {
117 props.expanded_keys.iter().cloned().collect()
118 };
119
120 let checked_set: HashSet<String> = props.checked_keys.iter().cloned().collect();
121 let current_key = props.current_key.clone();
122
123 let node_data: Vec<TreeNodeRenderData> = props
125 .data
126 .iter()
127 .map(|node| build_node_render_data(node, 0, &expanded_set, &checked_set, ¤t_key, props.highlight_current, props.show_checkbox))
128 .collect();
129
130 rsx! {
131 div {
132 class: "{class_names.join(\" \")}",
133 style: props.style.clone().unwrap_or_default(),
134 role: "tree",
135
136 for render_node in node_data.into_iter() {
137 TreeChild {
138 node: render_node,
139 show_checkbox: props.show_checkbox,
140 highlight_current: props.highlight_current,
141 expand_on_click: props.expand_on_click_node,
142 on_click: props.on_node_click,
143 on_expand: props.on_node_expand,
144 on_check: props.on_node_check,
145 }
146 }
147 }
148 }
149}
150
151#[derive(Clone, PartialEq)]
153struct TreeNodeRenderData {
154 label: String,
155 disabled: bool,
156 level: u32,
157 is_expanded: bool,
158 is_checked: bool,
159 is_current: bool,
160 has_children: bool,
161 children: Vec<TreeNodeRenderData>,
162}
163
164fn build_node_render_data(
165 node: &TreeNodeData,
166 level: u32,
167 expanded: &HashSet<String>,
168 checked: &HashSet<String>,
169 current: &Option<String>,
170 highlight: bool,
171 show_checkbox: bool,
172) -> TreeNodeRenderData {
173 let is_expanded = expanded.contains(&node.label);
174 let is_checked = checked.contains(&node.label);
175 let is_current = highlight && current.as_ref().map_or(false, |c| c == &node.label);
176 let has_children = !node.children.is_empty();
177
178 let children = if has_children {
179 node.children
180 .iter()
181 .map(|child| build_node_render_data(child, level + 1, expanded, checked, current, highlight, show_checkbox))
182 .collect()
183 } else {
184 vec![]
185 };
186
187 TreeNodeRenderData {
188 label: node.label.clone(),
189 disabled: node.disabled,
190 level,
191 is_expanded,
192 is_checked,
193 is_current,
194 has_children,
195 children,
196 }
197}
198
199fn collect_all_labels(data: &[TreeNodeData]) -> Vec<String> {
200 let mut result = vec![];
201 for node in data {
202 result.push(node.label.clone());
203 if !node.children.is_empty() {
204 result.extend(collect_all_labels(&node.children));
205 }
206 }
207 result
208}
209
210#[derive(Props, Clone, PartialEq)]
211struct TreeChildProps {
212 node: TreeNodeRenderData,
213 show_checkbox: bool,
214 highlight_current: bool,
215 expand_on_click: bool,
216 on_click: Option<EventHandler<String>>,
217 on_expand: Option<EventHandler<(String, bool)>>,
218 on_check: Option<EventHandler<(String, bool)>>,
219}
220
221#[component]
222fn TreeChild(props: TreeChildProps) -> Element {
223 let node_label = props.node.label.clone();
224 let padding = props.node.level * 18;
225 let has_children = props.node.has_children;
226 let is_expanded = props.node.is_expanded;
227 let is_current = props.node.is_current;
228 let is_checked = props.node.is_checked;
229 let disabled = props.node.disabled;
230 let show_checkbox = props.show_checkbox;
231 let expand_on_click = props.expand_on_click;
232 let on_click = props.on_click;
233 let on_expand = props.on_expand;
234 let on_check = props.on_check;
235
236 let label_for_click = node_label.clone();
238 let label_for_expand = node_label.clone();
239 let label_for_expand_icon = node_label.clone();
240 let label_for_check = node_label.clone();
241
242 let child_nodes: Vec<TreeNodeRenderData> = if has_children && is_expanded {
244 props.node.children.clone()
245 } else {
246 vec![]
247 };
248
249 let content_class = {
251 let mut cls = vec!["el-tree-node__content".to_string()];
252 if is_current {
253 cls.push("is-current".to_string());
254 }
255 if disabled {
256 cls.push("is-disabled".to_string());
257 }
258 cls.join(" ")
259 };
260
261 let expand_icon_class = if is_expanded {
263 "el-tree-node__expand-icon el-icon-caret-right expanded"
264 } else {
265 "el-tree-node__expand-icon el-icon-caret-right"
266 };
267
268 let checkbox_class = {
270 let mut cls = vec!["el-checkbox".to_string()];
271 if is_checked {
272 cls.push("is-checked".to_string());
273 }
274 cls.push("el-tree-node__checkbox".to_string());
275 cls.join(" ")
276 };
277
278 rsx! {
279 div {
280 class: "el-tree-node",
281 role: "treeitem",
282 "aria-expanded": "{is_expanded}",
283
284 div {
285 class: "{content_class}",
286 style: "padding-left: {padding}px;",
287
288 onclick: move |_| {
289 if !disabled {
290 if let Some(handler) = on_click.as_ref() {
291 handler.call(label_for_click.clone());
292 }
293 if expand_on_click && has_children {
294 if let Some(handler) = on_expand.as_ref() {
295 handler.call((label_for_expand.clone(), !is_expanded));
296 }
297 }
298 }
299 },
300
301 if has_children {
302 span {
303 class: "{expand_icon_class}",
304 onclick: move |e: Event<MouseData>| {
305 e.stop_propagation();
306 if !disabled {
307 if let Some(handler) = on_expand.as_ref() {
308 handler.call((label_for_expand_icon.clone(), !is_expanded));
309 }
310 }
311 },
312 }
313 } else {
314 span {
315 class: "el-tree-node__expand-icon is-leaf",
316 }
317 }
318
319 if show_checkbox {
320 span {
321 class: "{checkbox_class}",
322 onclick: move |e: Event<MouseData>| {
323 e.stop_propagation();
324 if !disabled {
325 if let Some(handler) = on_check.as_ref() {
326 handler.call((label_for_check.clone(), !is_checked));
327 }
328 }
329 },
330 span { class: "el-checkbox__inner" }
331 }
332 }
333
334 span {
335 class: "el-tree-node__label",
336 "{props.node.label}"
337 }
338 }
339
340 if has_children && is_expanded {
341 div {
342 class: "el-tree-node__children",
343 role: "group",
344
345 for child in child_nodes.into_iter() {
346 TreeChild {
347 node: child,
348 show_checkbox: show_checkbox,
349 highlight_current: props.highlight_current,
350 expand_on_click: expand_on_click,
351 on_click: on_click,
352 on_expand: on_expand,
353 on_check: on_check,
354 }
355 }
356 }
357 }
358 }
359 }
360}