Skip to main content

euv_ui/component/cascader/view/
fn.rs

1use super::*;
2
3/// The separator between the labels of a chosen path.
4const PATH_SEPARATOR: &str = " / ";
5
6/// The glyph drawn on the trailing edge of the trigger.
7const CARET_GLYPH: &str = "\u{25be}";
8
9/// A multi-level column picker.
10///
11/// Renders a `c_euv_cascader` wrapper holding the optional group label, a
12/// `c_euv_cascader_trigger` button that shows the labels of the chosen path
13/// joined with " / " — or the placeholder while the path is empty — next to
14/// a trailing `c_euv_cascader_caret` marker, and the `c_euv_cascader_panel`.
15///
16/// The panel is always mounted: opening and closing it swaps the
17/// `c_euv_cascader_panel_open` and `c_euv_cascader_panel_closed` classes
18/// rather than removing the columns from the tree, so the columns survive
19/// every re-render.
20///
21/// The panel body is a `c_euv_cascader_columns` row of `c_euv_cascader_column`s.
22/// The first column lists the top-level choices, the second the children of
23/// the choice the reader opened, and so on: only the columns the reader has
24/// actually opened are rendered. A choice on the path the reader is browsing
25/// carries `c_euv_cascader_option_active` as well.
26///
27/// Picking a choice with children opens the next column and leaves the panel
28/// open; picking a choice without children writes the whole path into the
29/// caller's signal, reports the choice to `on_select` and closes the panel.
30///
31/// # Arguments
32///
33/// - `VirtualNode<EuvCascaderProps>` - The props node holding the choices, the chosen path and the callback.
34///
35/// # Returns
36///
37/// - `VirtualNode` - The cascader virtual DOM tree.
38#[component]
39pub fn euv_cascader(node: VirtualNode<EuvCascaderProps>) -> VirtualNode {
40    let EuvCascaderProps {
41        options,
42        value,
43        placeholder,
44        label: label_text,
45        on_select,
46    }: EuvCascaderProps = node.try_get_props().unwrap_or_default();
47    let chosen: Vec<String> = value.get();
48    let open: Signal<bool> = HookContext::use_hook(|| Signal::create(false));
49    let browsed: Signal<Vec<String>> = HookContext::use_hook(|| Signal::create(chosen.clone()));
50    let browsed_path: Vec<String> = browsed.get();
51    let labels: Vec<&'static str> = cascader_path_labels(&options, &chosen);
52    let has_path: bool = !labels.is_empty();
53    let trigger_text: String = if has_path {
54        labels.join(PATH_SEPARATOR)
55    } else {
56        String::from(placeholder)
57    };
58    let columns: Vec<&[EuvCascaderOption]> = cascader_columns(&options, &browsed_path);
59    let column_nodes: Vec<VirtualNode> = columns
60        .iter()
61        .enumerate()
62        .map(|entry: (usize, &&[EuvCascaderOption])| {
63            let depth: usize = entry.0;
64            let column: &&[EuvCascaderOption] = entry.1;
65            let active_value: Option<&String> = browsed_path.get(depth);
66            let rows: Vec<VirtualNode> = column
67                .iter()
68                .map(|option: &EuvCascaderOption| {
69                    let is_active: bool =
70                        active_value.is_some_and(|active: &String| active == option.value);
71                    cascader_option_row(
72                        option,
73                        depth,
74                        is_active,
75                        value,
76                        browsed,
77                        open,
78                        on_select.clone(),
79                    )
80                })
81                .collect();
82            html! {
83                div {
84                    class: c_euv_cascader_column()
85                    rows
86                }
87            }
88        })
89        .collect();
90    html! {
91        div {
92            class: c_euv_cascader()
93            if !label_text.is_empty() {
94                label {
95                    class: c_form_label()
96                    label_text
97                }
98            }
99            button {
100                type: "button"
101                class: c_euv_cascader_trigger()
102                onclick: cascader_trigger_handler(open, browsed, value)
103                span {
104                    class: if has_path {
105                        c_euv_cascader_value()
106                    } else {
107                        c_euv_cascader_placeholder()
108                    }
109                    trigger_text
110                }
111                span {
112                    class: c_euv_cascader_caret()
113                    CARET_GLYPH
114                }
115            }
116            div {
117                class: c_euv_cascader_panel()
118                class: if { open } {
119                    c_euv_cascader_panel_open()
120                } else {
121                    c_euv_cascader_panel_closed()
122                }
123                div {
124                    class: c_euv_cascader_columns()
125                    column_nodes
126                }
127            }
128        }
129    }
130}
131
132/// Returns the labels of a chosen path.
133///
134/// The walk follows the path through the choice tree and collects the label
135/// of every choice on it. A path naming a choice the tree does not hold
136/// stops the walk, so a stale value can never produce a half-open path.
137///
138/// # Arguments
139///
140/// - `&[EuvCascaderOption]` - The top-level choices.
141/// - `&[String]` - The values of the path, outermost first.
142///
143/// # Returns
144///
145/// - `Vec<&'static str>` - The label of every choice on the path.
146pub fn cascader_path_labels(options: &[EuvCascaderOption], path: &[String]) -> Vec<&'static str> {
147    let mut labels: Vec<&'static str> = Vec::with_capacity(path.len());
148    let mut level: &[EuvCascaderOption] = options;
149    for value in path {
150        let Some(choice) = level
151            .iter()
152            .find(|option: &&EuvCascaderOption| option.value == value.as_str())
153        else {
154            break;
155        };
156        labels.push(choice.label);
157        level = &choice.children;
158    }
159    labels
160}
161
162/// Returns the columns the panel renders.
163///
164/// The first column holds the top-level choices; every further column holds
165/// the children of the choice the reader opened at the level above it. The
166/// walk stops at the first level the path does not name, so only the
167/// columns the reader has actually opened are rendered.
168///
169/// # Arguments
170///
171/// - `&'a [EuvCascaderOption]` - The top-level choices.
172/// - `&[String]` - The values of the path the reader is browsing, outermost first.
173///
174/// # Returns
175///
176/// - `Vec<&'a [EuvCascaderOption]>` - The choices of every open column, outermost first.
177pub fn cascader_columns<'a>(
178    options: &'a [EuvCascaderOption],
179    path: &[String],
180) -> Vec<&'a [EuvCascaderOption]> {
181    let mut columns: Vec<&'a [EuvCascaderOption]> = vec![options];
182    let mut level: &'a [EuvCascaderOption] = options;
183    for value in path {
184        let Some(choice) = level
185            .iter()
186            .find(|option: &&EuvCascaderOption| option.value == value.as_str())
187        else {
188            break;
189        };
190        if choice.children.is_empty() {
191            break;
192        }
193        level = &choice.children;
194        columns.push(level);
195    }
196    columns
197}
198
199/// Renders one choice of a column.
200///
201/// A choice on the path the reader is browsing carries the active class as
202/// well as the plain option class, so the column shows where the reader
203/// is. The click handler is built per choice, because every choice needs
204/// to know its own value and level.
205///
206/// # Arguments
207///
208/// - `&EuvCascaderOption` - The choice this row renders.
209/// - `usize` - The level of the choice, counting from the top-level column.
210/// - `bool` - Whether the choice is on the path the reader is browsing.
211/// - `Signal<Vec<String>>` - The chosen path owned by the caller.
212/// - `Signal<bool>` - The panel's open state.
213/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
214///
215/// # Returns
216///
217/// - `VirtualNode` - The choice row virtual DOM tree.
218fn cascader_option_row(
219    option: &EuvCascaderOption,
220    depth: usize,
221    is_active: bool,
222    value: Signal<Vec<String>>,
223    browsed: Signal<Vec<String>>,
224    open: Signal<bool>,
225    on_select: Option<Rc<dyn Fn(&'static str)>>,
226) -> VirtualNode {
227    let option_value: &'static str = option.value;
228    let option_label: &'static str = option.label;
229    let has_children: bool = !option.children.is_empty();
230    let handler: Option<Rc<dyn Fn(Event)>> = cascader_option_handler(
231        value,
232        browsed,
233        open,
234        on_select,
235        option_value,
236        depth,
237        has_children,
238    );
239    if is_active {
240        html! {
241            button {
242                type: "button"
243                class: c_euv_cascader_option()
244                class: c_euv_cascader_option_active()
245                onclick: handler
246                option_label
247            }
248        }
249    } else {
250        html! {
251            button {
252                type: "button"
253                class: c_euv_cascader_option()
254                onclick: handler
255                option_label
256            }
257        }
258    }
259}
260
261/// Builds the click handler of one choice.
262///
263/// The handler puts the choice on the path the reader is browsing, drops
264/// the levels below it and opens the next column. A choice without children
265/// ends the walk: the whole path is written into the caller's signal, the
266/// choice is reported to the callback and the panel is closed.
267///
268/// The event type is a parameter so the DOM-free test suite can drive the
269/// handler with a stand-in event; the component always instantiates it with
270/// [`Event`].
271///
272/// # Arguments
273///
274/// - `Signal<Vec<String>>` - The chosen path owned by the caller.
275/// - `Signal<bool>` - The panel's open state.
276/// - `Option<Rc<dyn Fn(&'static str)>>` - The select callback.
277/// - `&'static str` - The value of the clicked choice.
278/// - `usize` - The level of the clicked choice.
279/// - `bool` - Whether the choice has children.
280///
281/// # Returns
282///
283/// - `Option<Rc<dyn Fn(E)>>` - The click handler of the choice.
284pub fn cascader_option_handler<E: 'static>(
285    value: Signal<Vec<String>>,
286    browsed: Signal<Vec<String>>,
287    open: Signal<bool>,
288    on_select: Option<Rc<dyn Fn(&'static str)>>,
289    option_value: &'static str,
290    depth: usize,
291    has_children: bool,
292) -> Option<Rc<dyn Fn(E)>> {
293    Some(Rc::new(move |_: E| {
294        let mut path: Vec<String> = browsed.get();
295        path.truncate(depth);
296        path.push(String::from(option_value));
297        browsed.set(path.clone());
298        if has_children {
299            open.set(true);
300            return;
301        }
302        value.set(path);
303        if let Some(callback) = &on_select {
304            callback(option_value);
305        }
306        open.set(false);
307    }))
308}
309
310/// Builds the click handler of the trigger.
311///
312/// The handler opens and closes the panel. Opening it puts the panel back
313/// on the chosen path, so the reader always starts where the last choice
314/// left off.
315///
316/// The event type is a parameter so the DOM-free test suite can drive the
317/// handler with a stand-in event; the component always instantiates it with
318/// [`Event`].
319///
320/// # Arguments
321///
322/// - `Signal<bool>` - The panel's open state.
323/// - `Signal<Vec<String>>` - The browsed path owned by the component.
324///
325/// # Returns
326///
327/// - `Option<Rc<dyn Fn(E)>>` - The trigger click handler.
328pub fn cascader_trigger_handler<E: 'static>(
329    open: Signal<bool>,
330    browsed: Signal<Vec<String>>,
331    value: Signal<Vec<String>>,
332) -> Option<Rc<dyn Fn(E)>> {
333    Some(Rc::new(move |_: E| {
334        if open.get() {
335            open.set(false);
336            return;
337        }
338        browsed.set(value.get());
339        open.set(true);
340    }))
341}