dioxus_element_plug/components/
cascader.rs1use dioxus::prelude::*;
2
3#[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#[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
70type NodeRender = (String, String, bool, bool, bool);
72
73#[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 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 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 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 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
313fn 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}