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}