Skip to main content

euv_ui/component/transfer/view/
fn.rs

1use super::*;
2
3/// A two-list shuttle that moves rows between a source and a target list.
4///
5/// Renders a `c_euv_transfer` row holding a `c_euv_transfer_panel` for
6/// each list and a `c_euv_transfer_actions` column in the middle. Every
7/// panel carries a `c_euv_transfer_header` — the list's title and the
8/// number of its rows — above a `c_euv_transfer_list` of
9/// `c_euv_transfer_item` rows.
10///
11/// A marked row carries `c_euv_transfer_item_active` on top of
12/// `c_euv_transfer_item`, and its marker box swaps `c_euv_transfer_box`
13/// for `c_euv_transfer_box_active` with a "✓" glyph. Clicking a row
14/// toggles its value in the `selected` signal; a disabled row carries no
15/// click handler at all, so it never toggles.
16///
17/// The middle column holds two `euv_button` actions: one moves every
18/// marked source row to the target, the other moves every marked target
19/// row back. The component never changes the two lists itself — it only
20/// reports each move through the callbacks, so the caller stays the owner
21/// of both lists.
22///
23/// # Arguments
24///
25/// - `VirtualNode<EuvTransferProps>` - The props node holding the two lists, the marked rows and the move callbacks.
26///
27/// # Returns
28///
29/// - `VirtualNode` - The transfer virtual DOM tree.
30#[component]
31pub fn euv_transfer(node: VirtualNode<EuvTransferProps>) -> VirtualNode {
32    let EuvTransferProps {
33        source,
34        target,
35        selected,
36        source_title,
37        target_title,
38        on_move_to_target,
39        on_move_to_source,
40    }: EuvTransferProps = node.try_get_props().unwrap_or_default();
41    let marked: Vec<String> = selected.get();
42    let source_count: usize = source.len();
43    let target_count: usize = target.len();
44    let source_values: Vec<&'static str> = source
45        .iter()
46        .map(|item: &EuvTransferItem| item.value)
47        .collect();
48    let target_values: Vec<&'static str> = target
49        .iter()
50        .map(|item: &EuvTransferItem| item.value)
51        .collect();
52    let source_rows: Vec<VirtualNode> = source
53        .into_iter()
54        .map(|item: EuvTransferItem| {
55            transfer_row(item, transfer_is_marked(&marked, item.value), selected)
56        })
57        .collect();
58    let target_rows: Vec<VirtualNode> = target
59        .into_iter()
60        .map(|item: EuvTransferItem| {
61            transfer_row(item, transfer_is_marked(&marked, item.value), selected)
62        })
63        .collect();
64    let move_to_target: Rc<dyn Fn(Event)> =
65        transfer_move_to_target(selected, source_values, on_move_to_target);
66    let move_to_source: Rc<dyn Fn(Event)> =
67        transfer_move_to_source(selected, target_values, on_move_to_source);
68    html! {
69        div {
70            class: c_euv_transfer()
71            div {
72                class: c_euv_transfer_panel()
73                div {
74                    class: c_euv_transfer_header()
75                    span {
76                        source_title
77                    }
78                    span {
79                        source_count
80                    }
81                }
82                div {
83                    class: c_euv_transfer_list()
84                    source_rows
85                }
86            }
87            div {
88                class: c_euv_transfer_actions()
89                euv_button {
90                    variant: EuvButtonVariant::Outline
91                    label: ">"
92                    onclick: Some(move_to_target)
93                }
94                euv_button {
95                    variant: EuvButtonVariant::Outline
96                    label: "<"
97                    onclick: Some(move_to_source)
98                }
99            }
100            div {
101                class: c_euv_transfer_panel()
102                div {
103                    class: c_euv_transfer_header()
104                    span {
105                        target_title
106                    }
107                    span {
108                        target_count
109                    }
110                }
111                div {
112                    class: c_euv_transfer_list()
113                    target_rows
114                }
115            }
116        }
117    }
118}
119
120/// Renders one row of a transfer list.
121///
122/// The marker box is `c_euv_transfer_box_active` with a "✓" glyph while
123/// the row is marked, and the plain `c_euv_transfer_box` otherwise. A
124/// disabled row gets no click handler at all, so it can never write into
125/// the selection signal.
126///
127/// # Arguments
128///
129/// - `EuvTransferItem` - The row this list renders.
130/// - `bool` - Whether the row is currently marked.
131/// - `Signal<Vec<String>>` - The marked-rows signal owned by the caller.
132///
133/// # Returns
134///
135/// - `VirtualNode` - The row virtual DOM tree.
136fn transfer_row(
137    item: EuvTransferItem,
138    is_marked: bool,
139    selected: Signal<Vec<String>>,
140) -> VirtualNode {
141    let disabled: bool = item.disabled;
142    let handler: Option<Rc<dyn Fn(Event)>> = if disabled {
143        None
144    } else {
145        transfer_toggle_handler(selected, item.value)
146    };
147    html! {
148        div {
149            class: transfer_row_class(is_marked)
150            onclick: handler
151            span {
152                class: if is_marked {
153                    c_euv_transfer_box_active()
154                } else {
155                    c_euv_transfer_box()
156                }
157                if is_marked {
158                    "✓"
159                }
160            }
161            span {
162                item.label
163            }
164        }
165    }
166}
167
168/// Builds the class list of one transfer row.
169///
170/// A marked row carries `c_euv_transfer_item_active` on top of
171/// `c_euv_transfer_item`, so the class list is assembled by hand rather
172/// than through two `class:` attributes — the registry has no separate
173/// "unmarked" class, and a second attribute would repeat the base name.
174///
175/// # Arguments
176///
177/// - `bool` - Whether the row is currently marked.
178///
179/// # Returns
180///
181/// - `String` - The class list of the row.
182fn transfer_row_class(is_marked: bool) -> String {
183    let base: &str = c_euv_transfer_item().get_name();
184    if !is_marked {
185        return String::from(base);
186    }
187    let active: &str = c_euv_transfer_item_active().get_name();
188    let mut class_name: String = String::with_capacity(base.len() + 1 + active.len());
189    class_name.push_str(base);
190    class_name.push(' ');
191    class_name.push_str(active);
192    class_name
193}
194
195/// Whether a row's value is among the marked rows.
196///
197/// # Arguments
198///
199/// - `&[String]` - The marked values, read once per render.
200/// - `&str` - The value of the row to look for.
201///
202/// # Returns
203///
204/// - `bool` - Whether the row is marked.
205pub fn transfer_is_marked(marked: &[String], value: &str) -> bool {
206    marked.iter().any(|entry: &String| entry.as_str() == value)
207}
208
209/// Builds the click handler that marks or unmarks one row.
210///
211/// # Arguments
212///
213/// - `Signal<Vec<String>>` - The marked-rows signal owned by the caller.
214/// - `&'static str` - The value of the clicked row.
215///
216/// # Returns
217///
218/// - `Option<Rc<dyn Fn(Event)>>` - The toggle click handler.
219fn transfer_toggle_handler(
220    selected: Signal<Vec<String>>,
221    value: &'static str,
222) -> Option<Rc<dyn Fn(Event)>> {
223    Some(Rc::new(move |_: Event| {
224        transfer_toggle_value(selected, value);
225    }))
226}
227
228/// Marks a row in the marked-rows signal, or unmarks it when it is marked.
229///
230/// # Arguments
231///
232/// - `Signal<Vec<String>>` - The marked-rows signal owned by the caller.
233/// - `&'static str` - The value of the toggled row.
234pub fn transfer_toggle_value(selected: Signal<Vec<String>>, value: &'static str) {
235    let mut marked: Vec<String> = selected.get();
236    if let Some(index) = marked.iter().position(|entry: &String| entry == value) {
237        marked.remove(index);
238    } else {
239        marked.push(String::from(value));
240    }
241    selected.set(marked);
242}
243
244/// Builds the click handler that moves the marked source rows to the
245/// target.
246///
247/// The handler reports every marked row of the source list through the
248/// callback, in the order the rows are declared, and never touches the
249/// two lists itself.
250///
251/// # Arguments
252///
253/// - `Signal<Vec<String>>` - The marked-rows signal owned by the caller.
254/// - `Vec<&'static str>` - The values of the source list's rows.
255/// - `Option<Rc<dyn Fn(&'static str)>>` - The move callback.
256///
257/// # Returns
258///
259/// - `Rc<dyn Fn(E)>` - The move click handler.
260pub fn transfer_move_to_target<E: 'static>(
261    selected: Signal<Vec<String>>,
262    source_values: Vec<&'static str>,
263    on_move: Option<Rc<dyn Fn(&'static str)>>,
264) -> Rc<dyn Fn(E)> {
265    Rc::new(move |_: E| {
266        transfer_report_moves(selected, &source_values, &on_move);
267    })
268}
269
270/// Builds the click handler that moves the marked target rows back to the
271/// source.
272///
273/// The handler reports every marked row of the target list through the
274/// callback, in the order the rows are declared, and never touches the
275/// two lists itself.
276///
277/// # Arguments
278///
279/// - `Signal<Vec<String>>` - The marked-rows signal owned by the caller.
280/// - `Vec<&'static str>` - The values of the target list's rows.
281/// - `Option<Rc<dyn Fn(&'static str)>>` - The move callback.
282///
283/// # Returns
284///
285/// - `Rc<dyn Fn(E)>` - The move click handler.
286pub fn transfer_move_to_source<E: 'static>(
287    selected: Signal<Vec<String>>,
288    target_values: Vec<&'static str>,
289    on_move: Option<Rc<dyn Fn(&'static str)>>,
290) -> Rc<dyn Fn(E)> {
291    Rc::new(move |_: E| {
292        transfer_report_moves(selected, &target_values, &on_move);
293    })
294}
295
296/// Reports every marked row of one list through the move callback.
297///
298/// The rows are reported in the order the list declares them, and only
299/// the rows that are actually marked are reported.
300///
301/// # Arguments
302///
303/// - `Signal<Vec<String>>` - The marked-rows signal owned by the caller.
304/// - `&[&'static str]` - The values of the list's rows.
305/// - `&Option<Rc<dyn Fn(&'static str)>>` - The move callback.
306pub fn transfer_report_moves(
307    selected: Signal<Vec<String>>,
308    values: &[&'static str],
309    on_move: &Option<Rc<dyn Fn(&'static str)>>,
310) {
311    let marked: Vec<String> = selected.get();
312    for value in values.iter() {
313        if transfer_is_marked(&marked, value)
314            && let Some(callback) = on_move
315        {
316            callback(value);
317        }
318    }
319}