1use crate::components::config_provider::{ComponentSize, use_config};
2use crate::components::control::{ControlStatus, push_status_class};
3use crate::components::form::{FormItemControlContext, use_form_item_control};
4use crate::components::select_base::{
5 DropdownLayer, OptionKey, TreeNode, handle_option_list_key_event, option_key_to_value,
6 option_keys_to_value, toggle_option_key, use_dropdown_layer, value_to_option_key,
7 value_to_option_keys,
8};
9use dioxus::events::KeyboardEvent;
10use dioxus::prelude::*;
11use serde_json::Value;
12
13#[derive(Props, Clone, PartialEq)]
21pub struct TreeSelectProps {
22 #[props(optional)]
24 pub tree_data: Option<Vec<TreeNode>>,
25 #[props(optional)]
27 pub value: Option<String>,
28 #[props(optional)]
30 pub values: Option<Vec<String>>,
31 #[props(default)]
33 pub multiple: bool,
34 #[props(default)]
36 pub tree_checkable: bool,
37 #[props(default)]
39 pub show_search: bool,
40 #[props(optional)]
42 pub placeholder: Option<String>,
43 #[props(default)]
45 pub disabled: bool,
46 #[props(optional)]
48 pub status: Option<ControlStatus>,
49 #[props(optional)]
51 pub size: Option<ComponentSize>,
52 #[props(optional)]
54 pub class: Option<String>,
55 #[props(optional)]
56 pub style: Option<String>,
57 #[props(optional)]
59 pub dropdown_class: Option<String>,
60 #[props(optional)]
61 pub dropdown_style: Option<String>,
62 #[props(optional)]
64 pub on_change: Option<EventHandler<Vec<String>>>,
65}
66
67#[derive(Clone)]
70struct FlatNode {
71 key: OptionKey,
72 label: String,
73 disabled: bool,
74 depth: usize,
75}
76
77fn flatten_tree(nodes: &[TreeNode], depth: usize, out: &mut Vec<FlatNode>) {
78 for node in nodes {
79 out.push(FlatNode {
80 key: node.key.clone(),
81 label: node.label.clone(),
82 disabled: node.disabled,
83 depth,
84 });
85 if !node.children.is_empty() {
86 flatten_tree(&node.children, depth + 1, out);
87 }
88 }
89}
90
91#[component]
93pub fn TreeSelect(props: TreeSelectProps) -> Element {
94 let TreeSelectProps {
95 tree_data,
96 value,
97 values,
98 multiple,
99 tree_checkable,
100 show_search,
101 placeholder,
102 disabled,
103 status,
104 size,
105 class,
106 style,
107 dropdown_class,
108 dropdown_style,
109 on_change,
110 } = props;
111
112 let config = use_config();
113 let form_control = use_form_item_control();
114
115 let final_size = size.unwrap_or(config.size);
116
117 let is_disabled =
118 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
119
120 let multiple_flag = multiple || tree_checkable;
123
124 let internal_selected: Signal<Vec<OptionKey>> = use_signal(Vec::new);
126
127 let has_form = form_control.is_some();
128 let prop_single = value.clone();
129 let prop_multi = values.clone();
130
131 let selected_keys: Vec<OptionKey> = if let Some(ctx) = form_control.as_ref() {
133 if multiple_flag {
134 value_to_option_keys(ctx.value())
135 } else {
136 match value_to_option_key(ctx.value()) {
137 Some(k) => vec![k],
138 None => Vec::new(),
139 }
140 }
141 } else if let Some(vs) = prop_multi {
142 vs
143 } else if let Some(v) = prop_single {
144 vec![v]
145 } else {
146 internal_selected.read().clone()
147 };
148
149 let controlled_by_prop = has_form || value.is_some() || values.is_some();
150
151 let open_state: Signal<bool> = use_signal(|| false);
153 let active_index: Signal<Option<usize>> = use_signal(|| None);
154
155 let internal_click_flag: Signal<bool> = use_signal(|| false);
159
160 #[cfg(target_arch = "wasm32")]
163 {
164 let mut open_for_global = open_state;
165 let mut internal_flag = internal_click_flag;
166 use_effect(move || {
167 use wasm_bindgen::{JsCast, closure::Closure};
168
169 if let Some(window) = web_sys::window() {
170 if let Some(document) = window.document() {
171 let target: web_sys::EventTarget = document.into();
172 let handler = Closure::<dyn FnMut(web_sys::MouseEvent)>::wrap(Box::new(
173 move |_evt: web_sys::MouseEvent| {
174 let mut flag = internal_flag;
175 if *flag.read() {
176 flag.set(false);
178 return;
179 }
180 let mut open_signal = open_for_global;
181 if *open_signal.read() {
182 open_signal.set(false);
183 }
184 },
185 ));
186 let _ = target.add_event_listener_with_callback(
187 "click",
188 handler.as_ref().unchecked_ref(),
189 );
190 handler.forget();
192 }
193 }
194 });
195 }
196
197 let search_query: Signal<String> = use_signal(String::new);
199
200 let open_flag = *open_state.read();
201 let DropdownLayer { z_index, .. } = use_dropdown_layer(open_flag);
202 let current_z = *z_index.read();
203
204 let nodes: Vec<TreeNode> = tree_data.unwrap_or_else(Vec::new);
206 let mut flat_nodes: Vec<FlatNode> = Vec::new();
207 flatten_tree(&nodes, 0, &mut flat_nodes);
208
209 let placeholder_str = placeholder.unwrap_or_default();
210
211 let filtered_nodes: Vec<FlatNode> = if show_search {
212 let query = search_query.read().clone();
213 let trimmed = query.trim();
214 if trimmed.is_empty() {
215 flat_nodes.clone()
216 } else {
217 let lower = trimmed.to_lowercase();
218 flat_nodes
219 .iter()
220 .filter(|n| n.label.to_lowercase().contains(&lower))
221 .cloned()
222 .collect()
223 }
224 } else {
225 flat_nodes.clone()
226 };
227
228 let find_label = |key: &str| -> String {
230 flat_nodes
231 .iter()
232 .find(|n| n.key == key)
233 .map(|n| n.label.clone())
234 .unwrap_or_else(|| key.to_string())
235 };
236
237 let display_node = if multiple_flag {
238 if selected_keys.is_empty() {
239 rsx! { span { class: "adui-select-selection-placeholder", "{placeholder_str}" } }
240 } else {
241 rsx! {
242 div { class: "adui-select-selection-overflow",
243 {selected_keys.iter().map(|k| {
244 let label = find_label(k);
245 rsx! {
246 span { class: "adui-select-selection-item", "{label}" }
247 }
248 })}
249 }
250 }
251 }
252 } else if let Some(first) = selected_keys.first() {
253 let label = find_label(first);
254 rsx! { span { class: "adui-select-selection-item", "{label}" } }
255 } else {
256 rsx! { span { class: "adui-select-selection-placeholder", "{placeholder_str}" } }
257 };
258
259 let form_for_handlers = form_control.clone();
261 let _internal_selected_for_handlers = internal_selected;
262 let on_change_cb = on_change;
263 let controlled_flag = controlled_by_prop;
264
265 let open_for_toggle = open_state;
266 let is_disabled_flag = is_disabled;
267
268 let search_for_input = search_query;
269
270 let active_for_keydown = active_index;
271 let internal_selected_for_keydown = internal_selected;
272 let form_for_keydown = form_for_handlers.clone();
273 let open_for_keydown = open_for_toggle;
274
275 let internal_click_for_toggle = internal_click_flag;
277 let internal_click_for_keydown = internal_click_flag;
278
279 let dropdown_class_attr = {
280 let mut list = vec!["adui-select-dropdown".to_string()];
281 if let Some(extra) = dropdown_class {
282 list.push(extra);
283 }
284 list.join(" ")
285 };
286 let dropdown_style_attr = format!(
287 "position: absolute; top: 100%; left: 0; min-width: 100%; z-index: {}; {}",
288 current_z,
289 dropdown_style.unwrap_or_default()
290 );
291
292 let mut class_list = vec!["adui-select".to_string()];
294 if multiple_flag {
295 class_list.push("adui-select-multiple".into());
296 }
297 if is_disabled_flag {
298 class_list.push("adui-select-disabled".into());
299 }
300 if open_flag {
301 class_list.push("adui-select-open".into());
302 }
303 match final_size {
304 ComponentSize::Small => class_list.push("adui-select-sm".into()),
305 ComponentSize::Large => class_list.push("adui-select-lg".into()),
306 ComponentSize::Middle => {}
307 }
308 push_status_class(&mut class_list, status);
309 if let Some(extra) = class {
310 class_list.push(extra);
311 }
312 let class_attr = class_list.join(" ");
313 let style_attr = style.unwrap_or_default();
314
315 rsx! {
316 div {
317 class: "adui-select-root",
318 style: "position: relative; display: inline-block;",
319 div {
320 class: "{class_attr}",
321 style: "{style_attr}",
322 role: "combobox",
323 tabindex: 0,
324 "aria-expanded": open_flag,
325 "aria-disabled": is_disabled_flag,
326 onclick: move |_| {
327 if is_disabled_flag {
328 return;
329 }
330 let mut flag = internal_click_for_toggle;
333 flag.set(true);
334
335 let mut open_signal = open_for_toggle;
336 let current = *open_signal.read();
337 open_signal.set(!current);
338 },
339 onkeydown: move |evt: KeyboardEvent| {
340 if is_disabled_flag {
341 return;
342 }
343 use dioxus::prelude::Key;
344
345 let open_now = *open_for_keydown.read();
346 if !open_now {
347 match evt.key() {
348 Key::Enter | Key::ArrowDown => {
349 evt.prevent_default();
350 let mut open_signal = open_for_keydown;
351 open_signal.set(true);
352 }
353 Key::Escape => {
354 }
356 _ => {}
357 }
358 return;
359 }
360
361 if matches!(evt.key(), Key::Escape) {
362 let mut open_signal = open_for_keydown;
363 open_signal.set(false);
364 return;
365 }
366
367 let opts_len = filtered_nodes.len();
368 if opts_len == 0 {
369 return;
370 }
371
372 let mut flag = internal_click_for_keydown;
375 flag.set(true);
376
377 if let Some(idx) =
378 handle_option_list_key_event(&evt, opts_len, &active_for_keydown)
379 && idx < opts_len
380 {
381 let node = &filtered_nodes[idx];
382 if node.disabled {
383 return;
384 }
385
386 let key = node.key.clone();
387 let current_keys = selected_keys.clone();
388 let next_keys = if multiple_flag {
389 toggle_option_key(¤t_keys, &key)
390 } else {
391 vec![key.clone()]
392 };
393
394 apply_selected_keys(
395 &form_for_keydown,
396 multiple_flag,
397 controlled_flag,
398 &internal_selected_for_keydown,
399 on_change_cb,
400 next_keys,
401 );
402
403 if !multiple_flag {
404 let mut open_signal = open_for_keydown;
405 open_signal.set(false);
406 }
407 }
408 },
409 div { class: "adui-select-selector", {display_node} }
410 }
411 if open_flag {
412 div {
413 class: "{dropdown_class_attr}",
414 style: "{dropdown_style_attr}",
415 role: "tree",
416 "aria-multiselectable": multiple_flag,
417 if show_search {
418 div { class: "adui-select-search",
419 input {
420 class: "adui-select-search-input",
421 value: "{search_for_input.read()}",
422 oninput: move |evt| {
423 let mut signal = search_for_input;
424 signal.set(evt.value());
425 }
426 }
427 }
428 }
429 ul { class: "adui-select-item-list",
430 {filtered_nodes.iter().enumerate().map(|(index, node)| {
431 let key = node.key.clone();
432 let label = node.label.clone();
433 let disabled_opt = node.disabled || is_disabled_flag;
434 let is_selected = selected_keys.contains(&key);
435 let is_active = active_index
436 .read()
437 .as_ref()
438 .map(|i| *i == index)
439 .unwrap_or(false);
440 let selected_snapshot = selected_keys.clone();
441 let form_for_click = form_control.clone();
442 let internal_selected_for_click = internal_selected;
443 let open_for_click = open_state;
444 let internal_click_for_item = internal_click_flag;
445 let depth = node.depth;
446
447 rsx! {
448 li {
449 class: {
450 let mut classes = vec!["adui-select-item".to_string()];
451 if is_selected {
452 classes.push("adui-select-item-option-selected".into());
453 }
454 if disabled_opt {
455 classes.push("adui-select-item-option-disabled".into());
456 }
457 if is_active {
458 classes.push("adui-select-item-option-active".into());
459 }
460 classes.join(" ")
461 },
462 style: {format!("padding-left: {}px;", 12 + depth as i32 * 16)},
463 role: "treeitem",
464 "aria-selected": is_selected,
465 onclick: move |_| {
466 if disabled_opt {
467 return;
468 }
469 let mut flag = internal_click_for_item;
472 flag.set(true);
473
474 let current_keys = selected_snapshot.clone();
475 let next_keys = if multiple_flag {
476 toggle_option_key(¤t_keys, &key)
477 } else {
478 vec![key.clone()]
479 };
480
481 apply_selected_keys(
482 &form_for_click,
483 multiple_flag,
484 controlled_flag,
485 &internal_selected_for_click,
486 on_change_cb,
487 next_keys,
488 );
489
490 if !multiple_flag {
491 let mut open_signal = open_for_click;
492 open_signal.set(false);
493 }
494 },
495 "{label}"
496 }
497 }
498 })}
499 }
500 }
501 }
502 }
503 }
504}
505
506fn apply_selected_keys(
507 form_control: &Option<FormItemControlContext>,
508 multiple: bool,
509 controlled_by_prop: bool,
510 selected_signal: &Signal<Vec<OptionKey>>,
511 on_change: Option<EventHandler<Vec<String>>>,
512 new_keys: Vec<OptionKey>,
513) {
514 if let Some(ctx) = form_control {
515 if multiple {
516 let json = option_keys_to_value(&new_keys);
517 ctx.set_value(json);
518 } else if let Some(first) = new_keys.first() {
519 let json = option_key_to_value(first);
520 ctx.set_value(json);
521 } else {
522 ctx.set_value(Value::Null);
523 }
524 } else if !controlled_by_prop {
525 let mut signal = *selected_signal;
526 signal.set(new_keys.clone());
527 }
528
529 if let Some(cb) = on_change {
530 cb.call(new_keys);
531 }
532}
533
534#[cfg(test)]
535mod tree_select_tests {
536 use super::*;
537 use crate::components::select_base::TreeNode;
538
539 fn flatten_tree(nodes: &[TreeNode], depth: usize, out: &mut Vec<FlatNode>) {
540 for node in nodes {
541 out.push(FlatNode {
542 key: node.key.clone(),
543 label: node.label.clone(),
544 disabled: node.disabled,
545 depth,
546 });
547 if !node.children.is_empty() {
548 flatten_tree(&node.children, depth + 1, out);
549 }
550 }
551 }
552
553 #[test]
554 fn flatten_tree_empty() {
555 let nodes: Vec<TreeNode> = vec![];
556 let mut result = Vec::new();
557 flatten_tree(&nodes, 0, &mut result);
558 assert_eq!(result.len(), 0);
559 }
560
561 #[test]
562 fn flatten_tree_single_node() {
563 let nodes = vec![TreeNode {
564 key: "1".to_string(),
565 label: "Node 1".to_string(),
566 disabled: false,
567 children: vec![],
568 }];
569 let mut result = Vec::new();
570 flatten_tree(&nodes, 0, &mut result);
571 assert_eq!(result.len(), 1);
572 assert_eq!(result[0].key, "1");
573 assert_eq!(result[0].label, "Node 1");
574 assert_eq!(result[0].depth, 0);
575 }
576
577 #[test]
578 fn flatten_tree_nested() {
579 let nodes = vec![TreeNode {
580 key: "1".to_string(),
581 label: "Node 1".to_string(),
582 disabled: false,
583 children: vec![TreeNode {
584 key: "2".to_string(),
585 label: "Node 2".to_string(),
586 disabled: false,
587 children: vec![],
588 }],
589 }];
590 let mut result = Vec::new();
591 flatten_tree(&nodes, 0, &mut result);
592 assert_eq!(result.len(), 2);
593 assert_eq!(result[0].depth, 0);
594 assert_eq!(result[1].depth, 1);
595 }
596
597 #[test]
598 fn flatten_tree_deeply_nested() {
599 let nodes = vec![TreeNode {
600 key: "1".to_string(),
601 label: "Level 1".to_string(),
602 disabled: false,
603 children: vec![TreeNode {
604 key: "2".to_string(),
605 label: "Level 2".to_string(),
606 disabled: false,
607 children: vec![TreeNode {
608 key: "3".to_string(),
609 label: "Level 3".to_string(),
610 disabled: false,
611 children: vec![],
612 }],
613 }],
614 }];
615 let mut result = Vec::new();
616 flatten_tree(&nodes, 0, &mut result);
617 assert_eq!(result.len(), 3);
618 assert_eq!(result[0].depth, 0);
619 assert_eq!(result[1].depth, 1);
620 assert_eq!(result[2].depth, 2);
621 }
622
623 #[test]
624 fn flatten_tree_with_disabled_nodes() {
625 let nodes = vec![TreeNode {
626 key: "1".to_string(),
627 label: "Node 1".to_string(),
628 disabled: true,
629 children: vec![TreeNode {
630 key: "2".to_string(),
631 label: "Node 2".to_string(),
632 disabled: false,
633 children: vec![],
634 }],
635 }];
636 let mut result = Vec::new();
637 flatten_tree(&nodes, 0, &mut result);
638 assert_eq!(result.len(), 2);
639 assert!(result[0].disabled);
640 assert!(!result[1].disabled);
641 }
642
643 #[test]
644 fn flatten_tree_multiple_siblings() {
645 let nodes = vec![
646 TreeNode {
647 key: "1".to_string(),
648 label: "Node 1".to_string(),
649 disabled: false,
650 children: vec![],
651 },
652 TreeNode {
653 key: "2".to_string(),
654 label: "Node 2".to_string(),
655 disabled: false,
656 children: vec![],
657 },
658 TreeNode {
659 key: "3".to_string(),
660 label: "Node 3".to_string(),
661 disabled: false,
662 children: vec![],
663 },
664 ];
665 let mut result = Vec::new();
666 flatten_tree(&nodes, 0, &mut result);
667 assert_eq!(result.len(), 3);
668 assert_eq!(result[0].key, "1");
669 assert_eq!(result[1].key, "2");
670 assert_eq!(result[2].key, "3");
671 assert_eq!(result[0].depth, 0);
672 assert_eq!(result[1].depth, 0);
673 assert_eq!(result[2].depth, 0);
674 }
675
676 #[test]
677 fn flatten_tree_with_starting_depth() {
678 let nodes = vec![TreeNode {
679 key: "1".to_string(),
680 label: "Node 1".to_string(),
681 disabled: false,
682 children: vec![],
683 }];
684 let mut result = Vec::new();
685 flatten_tree(&nodes, 5, &mut result);
686 assert_eq!(result.len(), 1);
687 assert_eq!(result[0].depth, 5);
688 }
689}