patternfly_yew/components/dual_list_selector/
mod.rs1use crate::{components::tooltip::TooltipProperties, icon::Icon};
4use yew::prelude::*;
5
6mod control;
7mod item_renderer;
8mod list;
9mod pane;
10
11pub use control::*;
12pub use item_renderer::*;
13pub use list::*;
14pub use pane::*;
15
16pub type DualListSelectorOnListChangedInputs<T> = (MouseEvent, Vec<T>, Vec<T>);
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum OnOptionSelectEvent {
23 Mouse(MouseEvent),
24 Keyboard(KeyboardEvent),
25}
26
27impl From<MouseEvent> for OnOptionSelectEvent {
28 fn from(e: MouseEvent) -> Self {
29 Self::Mouse(e)
30 }
31}
32
33impl From<KeyboardEvent> for OnOptionSelectEvent {
34 fn from(e: KeyboardEvent) -> Self {
35 Self::Keyboard(e)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct OnOptionSelectArgs {
42 pub event: OnOptionSelectEvent,
43 pub index: usize,
44 pub is_chosen: bool,
45}
46
47pub struct OnOptionSelectArgsNoChosen {
50 pub event: OnOptionSelectEvent,
51 pub index: usize,
52}
53
54impl OnOptionSelectArgsNoChosen {
55 fn with_chosen(self, is_chosen: bool) -> OnOptionSelectArgs {
56 OnOptionSelectArgs {
57 event: self.event,
58 index: self.index,
59 is_chosen,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Properties)]
67pub struct DualListSelectorProps<T: DualListSelectorItemRenderer> {
68 #[prop_or_default]
70 pub class: Classes,
71
72 #[prop_or_default]
74 pub available_options_title: Option<AttrValue>,
75
76 #[prop_or_default]
78 pub available_options_status: Option<AttrValue>,
79 #[prop_or_default]
81 pub available: Vec<T>,
82
83 #[prop_or_default]
85 pub chosen_options_title: Option<AttrValue>,
86 #[prop_or_default]
88 pub chosen_options_status: Option<AttrValue>,
89 #[prop_or_default]
91 pub chosen: Vec<T>,
92
93 #[prop_or_default]
95 pub add_selected_tooltip: Option<AttrValue>,
96 #[prop_or_default]
98 pub add_selected_tooltip_props: Option<TooltipProperties>,
99 #[prop_or_default]
101 pub add_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
102 #[prop_or_default]
104 pub add_all_available_tooltip: Option<AttrValue>,
105 #[prop_or_default]
107 pub add_all_available_tooltip_props: Option<TooltipProperties>,
108 #[prop_or_default]
110 pub add_all: Option<Callback<(Vec<T>, Vec<T>)>>,
111 #[prop_or_default]
113 pub remove_selected_tooltip: Option<AttrValue>,
114 #[prop_or_default]
116 pub remove_selected_tooltip_props: Option<TooltipProperties>,
117 #[prop_or_default]
119 pub remove_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
120 #[prop_or_default]
122 pub remove_all_chosen_tooltip: Option<AttrValue>,
123 #[prop_or_default]
125 pub remove_all_chosen_tooltip_props: Option<TooltipProperties>,
126 #[prop_or_default]
128 pub remove_all: Option<Callback<(Vec<T>, Vec<T>)>>,
129
130 #[prop_or_default]
133 pub onlistchange: Option<Callback<DualListSelectorOnListChangedInputs<T>>>,
134 #[prop_or_default]
136 pub onoptionselect: Option<Callback<OnOptionSelectArgs>>,
137
138 #[prop_or_default]
140 pub disabled: bool,
141
142 #[prop_or_default]
144 pub children: Children,
145}
146
147#[derive(Debug, Clone)]
154struct State<T: DualListSelectorItemRenderer> {
155 onlistchange: Option<Callback<DualListSelectorOnListChangedInputs<T>>>,
156 available_options: Vec<T>,
157 available_options_selected: Vec<usize>,
158 chosen_options: Vec<T>,
159 chosen_options_selected: Vec<usize>,
160 add_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
161 add_all: Option<Callback<(Vec<T>, Vec<T>)>>,
162 remove_all: Option<Callback<(Vec<T>, Vec<T>)>>,
163 remove_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
164}
165
166impl<T: DualListSelectorItemRenderer> State<T> {
167 pub fn toggle_chosen_option(&mut self, index: usize) {
168 Self::toggle_option(&mut self.chosen_options_selected, index);
169 }
170
171 pub fn toggle_available_option(&mut self, index: usize) {
172 Self::toggle_option(&mut self.available_options_selected, index);
173 }
174
175 pub fn add_all_visible(&mut self, e: MouseEvent) {
176 Self::move_all(
177 &mut self.available_options_selected,
178 &mut self.available_options,
179 &mut self.chosen_options,
180 );
181 self.emit_onlistchange(e);
182 self.emit_callback(&self.add_all);
183 }
184
185 pub fn add_selected(&mut self, e: MouseEvent) {
186 Self::move_selected(
187 &mut self.available_options_selected,
188 &mut self.available_options,
189 &mut self.chosen_options,
190 );
191 self.emit_onlistchange(e);
192 self.emit_callback(&self.add_selected);
193 }
194
195 pub fn remove_selected(&mut self, e: MouseEvent) {
196 Self::move_selected(
197 &mut self.chosen_options_selected,
198 &mut self.chosen_options,
199 &mut self.available_options,
200 );
201 self.emit_onlistchange(e);
202 self.emit_callback(&self.remove_selected);
203 }
204
205 pub fn remove_all_visible(&mut self, e: MouseEvent) {
206 Self::move_all(
207 &mut self.chosen_options_selected,
208 &mut self.chosen_options,
209 &mut self.available_options,
210 );
211 self.emit_onlistchange(e);
212 self.emit_callback(&self.remove_all);
213 }
214
215 fn move_all(src_selected: &mut Vec<usize>, src_options: &mut Vec<T>, dst_options: &mut Vec<T>) {
216 dst_options.extend_from_slice(src_options);
217 src_options.clear();
218 src_selected.clear();
219 }
220
221 fn move_selected(
222 src_selected: &mut Vec<usize>,
223 src_options: &mut Vec<T>,
224 dst_options: &mut Vec<T>,
225 ) {
226 let selected_html = src_selected
227 .iter()
228 .map(|&idx| src_options[idx].clone())
229 .collect::<Vec<T>>();
230 dst_options.extend_from_slice(&selected_html);
231 src_options.retain(|i| !selected_html.contains(i));
232 src_selected.clear();
233 }
234
235 fn toggle_option(v: &mut Vec<usize>, elem: usize) {
236 match v.iter().position(|&x| x == elem) {
237 Some(i) => {
239 v.remove(i);
240 }
241 None => v.push(elem),
243 }
244 }
245
246 fn emit_onlistchange(&self, e: MouseEvent) {
247 if let Some(f) = &self.onlistchange {
248 f.emit((
249 e,
250 self.available_options.clone(),
251 self.chosen_options.clone(),
252 ))
253 }
254 }
255
256 fn emit_callback(&self, f: &Option<Callback<(Vec<T>, Vec<T>)>>) {
257 if let Some(f) = f {
258 f.emit((self.available_options.clone(), self.chosen_options.clone()));
259 }
260 }
261}
262
263#[function_component(DualListSelector)]
264pub fn dual_list_selector<T: DualListSelectorItemRenderer>(
265 props: &DualListSelectorProps<T>,
266) -> Html {
267 let state = use_state(|| State {
268 add_selected: props.add_selected.clone(),
269 add_all: props.add_all.clone(),
270 remove_all: props.remove_all.clone(),
271 remove_selected: props.remove_selected.clone(),
272 onlistchange: props.onlistchange.clone(),
273 available_options: props.available.clone(),
274 available_options_selected: Vec::new(),
275 chosen_options: props.chosen.clone(),
276 chosen_options_selected: Vec::new(),
277 });
278 let onoptionselect = {
279 let state = state.clone();
280 let onoptionselect = props.onoptionselect.clone();
281 Callback::from(move |args: OnOptionSelectArgs| {
282 let mut new_state = (*state).clone();
283 let onoptionselect = onoptionselect.clone();
284 if args.is_chosen {
285 new_state.toggle_chosen_option(args.index);
286 } else {
287 new_state.toggle_available_option(args.index);
288 }
289 state.set(new_state);
290 if let Some(f) = onoptionselect {
291 f.emit(args.clone())
292 }
293 })
294 };
295 let available_options_status = props.available_options_status.clone().unwrap_or_else(|| {
296 format!(
297 "{} of {} item selected",
298 state.available_options_selected.len(),
299 state.available_options.len()
300 )
301 .into()
302 });
303 let chosen_options_status = props.chosen_options_status.clone().unwrap_or_else(|| {
304 format!(
305 "{} of {} item selected",
306 state.chosen_options_selected.len(),
307 state.chosen_options.len()
308 )
309 .into()
310 });
311 let control_option = |f: fn(&mut State<T>, MouseEvent)| {
312 let state = state.clone();
313 Callback::from(move |e| {
314 let mut new_state = (*state).clone();
315 f(&mut new_state, e);
316 state.set(new_state);
317 })
318 };
319 html! {
320 <div class={classes!["pf-v5-c-dual-list-selector", props.class.clone()]}>
321 if !props.children.is_empty() {
322 { props.children.clone() }
323 } else {
324 <DualListSelectorPane<T>
325 title={props.available_options_title.clone()}
326 status={available_options_status}
327 options={state.available_options.clone()}
328 onoptionselect={
329 let onoptionselect = onoptionselect.clone();
330 Callback::from(move |args: OnOptionSelectArgsNoChosen| onoptionselect.emit(args.with_chosen(false)))
331 }
332 selected_options={state.available_options_selected.clone()}
333 disabled={props.disabled}
334 />
335 <DualListSelectorControlsWrapper>
336 <DualListSelectorControl
337 tooltip={props.add_selected_tooltip.clone()}
338 disabled={props.disabled}
339 onclick={control_option(State::add_selected)}
340 tooltip_props={props.add_selected_tooltip_props.clone()}
341 >
342 { Icon::AngleRight.with_style("width:1em;display:block;") }
343 </DualListSelectorControl>
344 <DualListSelectorControl
345 tooltip={props.add_all_available_tooltip.clone()}
346 disabled={props.disabled}
347 onclick={control_option(State::add_all_visible)}
348 tooltip_props={props.add_all_available_tooltip_props.clone()}
349 >
350 { Icon::AngleDoubleRight.with_style("width:1em;display:block;") }
351 </DualListSelectorControl>
352 <DualListSelectorControl
353 tooltip={props.remove_all_chosen_tooltip.clone()}
354 disabled={props.disabled}
355 onclick={control_option(State::remove_all_visible)}
356 tooltip_props={props.remove_all_chosen_tooltip_props.clone()}
357 >
358 { Icon::AngleDoubleLeft.with_style("width:1em;display:block;") }
359 </DualListSelectorControl>
360 <DualListSelectorControl
361 tooltip={props.remove_selected_tooltip.clone()}
362 disabled={props.disabled}
363 onclick={control_option(State::remove_selected)}
364 tooltip_props={props.remove_selected_tooltip_props.clone()}
365 >
366 { Icon::AngleLeft.with_style("width:1em;display:block;") }
367 </DualListSelectorControl>
368 </DualListSelectorControlsWrapper>
369 <DualListSelectorPane<T>
370 is_chosen=true
371 title={props.chosen_options_title.clone()}
372 status={chosen_options_status}
373 options={state.chosen_options.clone()}
374 onoptionselect={
375 let onoptionselect = onoptionselect.clone();
376 Callback::from(move |args: OnOptionSelectArgsNoChosen| onoptionselect.emit(args.with_chosen(true)))
377 }
378 selected_options={state.chosen_options_selected.clone()}
379 disabled={props.disabled}
380 />
381 }
382 </div>
383 }
384}