Skip to main content

dioxus_element_plug/components/
cascader.rs

1use dioxus::prelude::*;
2
3/// Cascader option
4#[derive(Clone, PartialEq)]
5pub struct CascaderOption {
6    pub value: String,
7    pub label: String,
8    pub children: Vec<CascaderOption>,
9    pub disabled: bool,
10}
11
12impl CascaderOption {
13    pub fn new(value: &str, label: &str) -> Self {
14        Self {
15            value: value.to_string(),
16            label: label.to_string(),
17            children: vec![],
18            disabled: false,
19        }
20    }
21
22    pub fn child(mut self, option: CascaderOption) -> Self {
23        self.children.push(option);
24        self
25    }
26
27    pub fn disabled(mut self, disabled: bool) -> Self {
28        self.disabled = disabled;
29        self
30    }
31}
32
33/// Cascader props
34#[derive(Props, Clone, PartialEq)]
35pub struct CascaderProps {
36    #[props(default)]
37    pub options: Vec<CascaderOption>,
38
39    #[props(default)]
40    pub model_value: Vec<String>,
41
42    #[props(default = "Select".to_string())]
43    pub placeholder: String,
44
45    #[props(default = false)]
46    pub disabled: bool,
47
48    #[props(default = false)]
49    pub clearable: bool,
50
51    #[props(default = false)]
52    pub filterable: bool,
53
54    #[props(default = true)]
55    pub show_all_levels: bool,
56
57    #[props(default = " / ".to_string())]
58    pub separator: String,
59
60    #[props(default)]
61    pub on_change: Option<EventHandler<Vec<String>>>,
62
63    #[props(default)]
64    pub class: Option<String>,
65
66    #[props(default)]
67    pub style: Option<String>,
68}
69
70/// Cascader node render data: (value, label, disabled, has_children, is_active)
71type NodeRender = (String, String, bool, bool, bool);
72
73/// Cascader component for multi-level selection
74#[component]
75pub fn Cascader(props: CascaderProps) -> Element {
76    let mut class_names = vec!["el-cascader".to_string()];
77    if props.disabled {
78        class_names.push("is-disabled".to_string());
79    }
80    if let Some(ref c) = props.class {
81        class_names.push(c.clone());
82    }
83
84    // Build display text from model_value
85    let display_text = if props.model_value.is_empty() {
86        String::new()
87    } else {
88        let labels = resolve_path_labels(&props.options, &props.model_value);
89        if props.show_all_levels {
90            labels.join(&props.separator)
91        } else {
92            labels.last().cloned().unwrap_or_default()
93        }
94    };
95
96    // Pre-compute level 1 nodes: (value, label, disabled, has_children, is_active)
97    let level1_nodes: Vec<NodeRender> = props
98        .options
99        .iter()
100        .map(|opt| {
101            let is_active = props.model_value.first().map_or(false, |v| v == &opt.value);
102            (opt.value.clone(), opt.label.clone(), opt.disabled, !opt.children.is_empty(), is_active)
103        })
104        .collect();
105
106    // Pre-compute level 2 nodes: (value, label, disabled, has_children, is_active, l1_value)
107    let level2_nodes: Vec<(String, String, bool, bool, bool, String)> = if props.model_value.len() >= 1 {
108        let l1 = &props.model_value[0];
109        props
110            .options
111            .iter()
112            .find(|o| &o.value == l1)
113            .map(|parent| {
114                let l1_val = l1.clone();
115                parent
116                    .children
117                    .iter()
118                    .map(|opt| {
119                        let is_active = props.model_value.get(1).map_or(false, |v| v == &opt.value);
120                        (opt.value.clone(), opt.label.clone(), opt.disabled, !opt.children.is_empty(), is_active, l1_val.clone())
121                    })
122                    .collect()
123            })
124            .unwrap_or_default()
125    } else {
126        vec![]
127    };
128
129    // Pre-compute level 3 nodes: (value, label, disabled, has_children, is_active, l1_value, l2_value)
130    let level3_nodes: Vec<(String, String, bool, bool, bool, String, String)> = if props.model_value.len() >= 2 {
131        let l1 = &props.model_value[0];
132        let l2 = &props.model_value[1];
133        props
134            .options
135            .iter()
136            .find(|o| &o.value == l1)
137            .and_then(|p1| p1.children.iter().find(|o| &o.value == l2))
138            .map(|p2| {
139                let l1_val = l1.clone();
140                let l2_val = l2.clone();
141                p2
142                    .children
143                    .iter()
144                    .map(|opt| {
145                        let is_active = props.model_value.get(2).map_or(false, |v| v == &opt.value);
146                        (opt.value.clone(), opt.label.clone(), opt.disabled, !opt.children.is_empty(), is_active, l1_val.clone(), l2_val.clone())
147                    })
148                    .collect()
149            })
150            .unwrap_or_default()
151    } else {
152        vec![]
153    };
154
155    let has_value = !props.model_value.is_empty();
156    let placeholder = props.placeholder.clone();
157    let show_clear = props.clearable && has_value && !props.disabled;
158    let show_l1 = !level1_nodes.is_empty();
159    let show_l2 = !level2_nodes.is_empty();
160    let show_l3 = !level3_nodes.is_empty();
161    let on_change = props.on_change;
162
163    rsx! {
164        div {
165            class: "{class_names.join(\" \")}",
166            style: props.style.clone().unwrap_or_default(),
167
168            div {
169                class: "el-cascader__wrapper",
170
171                div {
172                    class: "el-input el-input--suffix el-cascader__input",
173
174                    input {
175                        class: "el-input__inner",
176                        r#type: "text",
177                        placeholder: "{placeholder}",
178                        readonly: true,
179                        disabled: props.disabled,
180                        value: "{display_text}",
181                    }
182
183                    if show_clear {
184                        span {
185                            class: "el-input__suffix el-cascader__clear-icon",
186                            onclick: move |_| {
187                                if let Some(handler) = on_change.as_ref() {
188                                    handler.call(vec![]);
189                                }
190                            },
191                            i { class: "el-icon-circle-close" }
192                        }
193                    } else {
194                        span {
195                            class: "el-input__suffix el-cascader__arrow-icon",
196                            i { class: "el-icon-arrow-down" }
197                        }
198                    }
199                }
200            }
201
202            if has_value {
203                div {
204                    class: "el-cascader__dropdown",
205                    style: "position: absolute; z-index: 2000; margin-top: 4px;",
206
207                    div {
208                        class: "el-cascader-panel",
209                        style: "display: flex;",
210
211                        if show_l1 {
212                            div {
213                                class: "el-cascader-menu",
214
215                                for (value, label, disabled, has_children, is_active) in level1_nodes.into_iter() {
216                                    div {
217                                        class: if is_active {
218                                            "el-cascader-node is-active"
219                                        } else if disabled {
220                                            "el-cascader-node is-disabled"
221                                        } else {
222                                            "el-cascader-node"
223                                        },
224                                        onclick: move |_| {
225                                            if !disabled {
226                                                if let Some(handler) = on_change.as_ref() {
227                                                    handler.call(vec![value.clone()]);
228                                                }
229                                            }
230                                        },
231
232                                        span {
233                                            class: "el-cascader-node__label",
234                                            "{label}"
235                                        }
236                                        if has_children {
237                                            i { class: "el-cascader-node__postfix el-icon-arrow-right" }
238                                        }
239                                    }
240                                }
241                            }
242                        }
243
244                        if show_l2 {
245                            div {
246                                class: "el-cascader-menu",
247
248                                for (value, label, disabled, has_children, is_active, l1_val) in level2_nodes.into_iter() {
249                                    div {
250                                        class: if is_active {
251                                            "el-cascader-node is-active"
252                                        } else if disabled {
253                                            "el-cascader-node is-disabled"
254                                        } else {
255                                            "el-cascader-node"
256                                        },
257                                        onclick: move |_| {
258                                            if !disabled {
259                                                if let Some(handler) = on_change.as_ref() {
260                                                    handler.call(vec![l1_val.clone(), value.clone()]);
261                                                }
262                                            }
263                                        },
264
265                                        span {
266                                            class: "el-cascader-node__label",
267                                            "{label}"
268                                        }
269                                        if has_children {
270                                            i { class: "el-cascader-node__postfix el-icon-arrow-right" }
271                                        }
272                                    }
273                                }
274                            }
275                        }
276
277                        if show_l3 {
278                            div {
279                                class: "el-cascader-menu",
280
281                                for (value, label, disabled, _has_children, is_active, l1_val, l2_val) in level3_nodes.into_iter() {
282                                    div {
283                                        class: if is_active {
284                                            "el-cascader-node is-active"
285                                        } else if disabled {
286                                            "el-cascader-node is-disabled"
287                                        } else {
288                                            "el-cascader-node"
289                                        },
290                                        onclick: move |_| {
291                                            if !disabled {
292                                                if let Some(handler) = on_change.as_ref() {
293                                                    handler.call(vec![l1_val.clone(), l2_val.clone(), value.clone()]);
294                                                }
295                                            }
296                                        },
297
298                                        span {
299                                            class: "el-cascader-node__label",
300                                            "{label}"
301                                        }
302                                    }
303                                }
304                            }
305                        }
306                    }
307                }
308            }
309        }
310    }
311}
312
313/// Resolve labels for a path of values
314fn resolve_path_labels(options: &[CascaderOption], path: &[String]) -> Vec<String> {
315    let mut labels = vec![];
316    let mut current_options = options;
317
318    for value in path {
319        if let Some(opt) = current_options.iter().find(|o| &o.value == value) {
320            labels.push(opt.label.clone());
321            current_options = &opt.children;
322        } else {
323            break;
324        }
325    }
326
327    labels
328}