patternfly_yew/components/dual_list_selector/
mod.rs

1//! The dynamic and composable [dual list selector](https://www.patternfly.org/components/dual-list-selector)
2
3use 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
16/// The inputs of the onlistchanged event. Has the corresponding mouse event of the
17/// button press, as well as the available and chosen options after the change.
18pub type DualListSelectorOnListChangedInputs<T> = (MouseEvent, Vec<T>, Vec<T>);
19
20/// The event causing an option to be selected
21#[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/// The arguments passed to an onoptionselect event.
40#[derive(Debug, Clone, PartialEq)]
41pub struct OnOptionSelectArgs {
42    pub event: OnOptionSelectEvent,
43    pub index: usize,
44    pub is_chosen: bool,
45}
46
47/// Same as [`OnOptionsSelectArgs`] but without the `chosen` field
48/// because that is passed in from the outside.
49pub 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/// Acts as a container for all other DualListSelector sub-components when using a
65/// composable dual list selector.
66#[derive(Debug, Clone, PartialEq, Properties)]
67pub struct DualListSelectorProps<T: DualListSelectorItemRenderer> {
68    /// Additional classes applied to the dual list selector.
69    #[prop_or_default]
70    pub class: Classes,
71
72    /// Title applied to the dynamically built available options pane.
73    #[prop_or_default]
74    pub available_options_title: Option<AttrValue>,
75
76    /// Status message to display above the dynamically built available options pane.
77    #[prop_or_default]
78    pub available_options_status: Option<AttrValue>,
79    /// Options to display in the dynamically built available options pane.
80    #[prop_or_default]
81    pub available: Vec<T>,
82
83    /// Title applied to the dynamically built chosen options pane.
84    #[prop_or_default]
85    pub chosen_options_title: Option<AttrValue>,
86    /// Status message to display above the dynamically built chosen options pane.
87    #[prop_or_default]
88    pub chosen_options_status: Option<AttrValue>,
89    /// Options to display in the dynamically built chosen options pane.
90    #[prop_or_default]
91    pub chosen: Vec<T>,
92
93    /// Tooltip content for the dynamically built add selected button.
94    #[prop_or_default]
95    pub add_selected_tooltip: Option<AttrValue>,
96    /// Additional tooltip properties to the dynamically built add selected tooltip.
97    #[prop_or_default]
98    pub add_selected_tooltip_props: Option<TooltipProperties>,
99    /// Optional callback for the dynamically built add selected button.
100    #[prop_or_default]
101    pub add_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
102    /// Tooltip content for the dynamically built add all button.
103    #[prop_or_default]
104    pub add_all_available_tooltip: Option<AttrValue>,
105    /// Additional tooltip properties to the dynamically built add all tooltip.
106    #[prop_or_default]
107    pub add_all_available_tooltip_props: Option<TooltipProperties>,
108    /// Optional callback for the dynamically built add all button.
109    #[prop_or_default]
110    pub add_all: Option<Callback<(Vec<T>, Vec<T>)>>,
111    /// Tooltip content for the dynamically built remove selected button.
112    #[prop_or_default]
113    pub remove_selected_tooltip: Option<AttrValue>,
114    /// Additional tooltip properties to the dynamically built remove selected tooltip.
115    #[prop_or_default]
116    pub remove_selected_tooltip_props: Option<TooltipProperties>,
117    /// Optional callback for the dynamically built remove selected button.
118    #[prop_or_default]
119    pub remove_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
120    /// Tooltip content for the dynamically built remove all button.
121    #[prop_or_default]
122    pub remove_all_chosen_tooltip: Option<AttrValue>,
123    /// Additional tooltip properties to the dynamically built remove selected tooltip.
124    #[prop_or_default]
125    pub remove_all_chosen_tooltip_props: Option<TooltipProperties>,
126    /// Optional callback for the dynamically built remove all button.
127    #[prop_or_default]
128    pub remove_all: Option<Callback<(Vec<T>, Vec<T>)>>,
129
130    /// Callback fired every time dynamically built options are chosen or removed.
131    /// Inputs are the mouse event as well as the available and chosen options after the change.
132    #[prop_or_default]
133    pub onlistchange: Option<Callback<DualListSelectorOnListChangedInputs<T>>>,
134    /// Optional callback fired when a dynamically built option is selected.
135    #[prop_or_default]
136    pub onoptionselect: Option<Callback<OnOptionSelectArgs>>,
137
138    /// Flag indicating if the dual list selector is in a disabled state
139    #[prop_or_default]
140    pub disabled: bool,
141
142    /// Content to be rendered in the dual list selector. Panes & controls will not be built dynamically when children are provided.
143    #[prop_or_default]
144    pub children: Children,
145}
146
147/// The state of the dual list selector.
148/// Saves which options exist and which of those are selected
149/// for the "available" and "chosen" panels.
150///
151/// The selected vectors save the indices of the selected items
152/// of the options vectors.
153#[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            // Remove from selected
238            Some(i) => {
239                v.remove(i);
240            }
241            // Add to selected
242            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}