1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
//! The dynamic and composable [dual list selector](https://www.patternfly.org/components/dual-list-selector)

use crate::{components::tooltip::TooltipProperties, icon::Icon};
use yew::prelude::*;

mod control;
mod item_renderer;
mod list;
mod pane;

pub use control::*;
pub use item_renderer::*;
pub use list::*;
pub use pane::*;

/// The inputs of the onlistchanged event. Has the corresponding mouse event of the
/// button press, as well as the available and chosen options after the change.
pub type DualListSelectorOnListChangedInputs<T> = (MouseEvent, Vec<T>, Vec<T>);

/// The event causing an option to be selected
#[derive(Debug, Clone, PartialEq)]
pub enum OnOptionSelectEvent {
    Mouse(MouseEvent),
    Keyboard(KeyboardEvent),
}

impl From<MouseEvent> for OnOptionSelectEvent {
    fn from(e: MouseEvent) -> Self {
        Self::Mouse(e)
    }
}

impl From<KeyboardEvent> for OnOptionSelectEvent {
    fn from(e: KeyboardEvent) -> Self {
        Self::Keyboard(e)
    }
}

/// The arguments passed to an onoptionselect event.
#[derive(Debug, Clone, PartialEq)]
pub struct OnOptionSelectArgs {
    pub event: OnOptionSelectEvent,
    pub index: usize,
    pub is_chosen: bool,
}

/// Same as [`OnOptionsSelectArgs`] but without the `chosen` field
/// because that is passed in from the outside.
pub struct OnOptionSelectArgsNoChosen {
    pub event: OnOptionSelectEvent,
    pub index: usize,
}

impl OnOptionSelectArgsNoChosen {
    fn with_chosen(self, is_chosen: bool) -> OnOptionSelectArgs {
        OnOptionSelectArgs {
            event: self.event,
            index: self.index,
            is_chosen,
        }
    }
}

/// Acts as a container for all other DualListSelector sub-components when using a
/// composable dual list selector.
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DualListSelectorProps<T: DualListSelectorItemRenderer> {
    /// Additional classes applied to the dual list selector.
    #[prop_or_default]
    pub class: Classes,

    /// Title applied to the dynamically built available options pane.
    #[prop_or_default]
    pub available_options_title: Option<AttrValue>,

    /// Status message to display above the dynamically built available options pane.
    #[prop_or_default]
    pub available_options_status: Option<AttrValue>,
    /// Options to display in the dynamically built available options pane.
    #[prop_or_default]
    pub available: Vec<T>,

    /// Title applied to the dynamically built chosen options pane.
    #[prop_or_default]
    pub chosen_options_title: Option<AttrValue>,
    /// Status message to display above the dynamically built chosen options pane.
    #[prop_or_default]
    pub chosen_options_status: Option<AttrValue>,
    /// Options to display in the dynamically built chosen options pane.
    #[prop_or_default]
    pub chosen: Vec<T>,

    /// Tooltip content for the dynamically built add selected button.
    #[prop_or_default]
    pub add_selected_tooltip: Option<AttrValue>,
    /// Additional tooltip properties to the dynamically built add selected tooltip.
    #[prop_or_default]
    pub add_selected_tooltip_props: Option<TooltipProperties>,
    /// Optional callback for the dynamically built add selected button.
    #[prop_or_default]
    pub add_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
    /// Tooltip content for the dynamically built add all button.
    #[prop_or_default]
    pub add_all_available_tooltip: Option<AttrValue>,
    /// Additional tooltip properties to the dynamically built add all tooltip.
    #[prop_or_default]
    pub add_all_available_tooltip_props: Option<TooltipProperties>,
    /// Optional callback for the dynamically built add all button.
    #[prop_or_default]
    pub add_all: Option<Callback<(Vec<T>, Vec<T>)>>,
    /// Tooltip content for the dynamically built remove selected button.
    #[prop_or_default]
    pub remove_selected_tooltip: Option<AttrValue>,
    /// Additional tooltip properties to the dynamically built remove selected tooltip.
    #[prop_or_default]
    pub remove_selected_tooltip_props: Option<TooltipProperties>,
    /// Optional callback for the dynamically built remove selected button.
    #[prop_or_default]
    pub remove_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
    /// Tooltip content for the dynamically built remove all button.
    #[prop_or_default]
    pub remove_all_chosen_tooltip: Option<AttrValue>,
    /// Additional tooltip properties to the dynamically built remove selected tooltip.
    #[prop_or_default]
    pub remove_all_chosen_tooltip_props: Option<TooltipProperties>,
    /// Optional callback for the dynamically built remove all button.
    #[prop_or_default]
    pub remove_all: Option<Callback<(Vec<T>, Vec<T>)>>,

    /// Callback fired every time dynamically built options are chosen or removed.
    /// Inputs are the mouse event as well as the available and chosen options after the change.
    #[prop_or_default]
    pub onlistchange: Option<Callback<DualListSelectorOnListChangedInputs<T>>>,
    /// Optional callback fired when a dynamically built option is selected.
    #[prop_or_default]
    pub onoptionselect: Option<Callback<OnOptionSelectArgs>>,

    /// Flag indicating if the dual list selector is in a disabled state
    #[prop_or_default]
    pub disabled: bool,

    /// Content to be rendered in the dual list selector. Panes & controls will not be built dynamically when children are provided.
    #[prop_or_default]
    pub children: Children,
}

/// The state of the dual list selector.
/// Saves which options exist and which of those are selected
/// for the "available" and "chosen" panels.
///
/// The selected vectors save the indices of the selected items
/// of the options vectors.
#[derive(Debug, Clone)]
struct State<T: DualListSelectorItemRenderer> {
    onlistchange: Option<Callback<DualListSelectorOnListChangedInputs<T>>>,
    available_options: Vec<T>,
    available_options_selected: Vec<usize>,
    chosen_options: Vec<T>,
    chosen_options_selected: Vec<usize>,
    add_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
    add_all: Option<Callback<(Vec<T>, Vec<T>)>>,
    remove_all: Option<Callback<(Vec<T>, Vec<T>)>>,
    remove_selected: Option<Callback<(Vec<T>, Vec<T>)>>,
}

impl<T: DualListSelectorItemRenderer> State<T> {
    pub fn toggle_chosen_option(&mut self, index: usize) {
        Self::toggle_option(&mut self.chosen_options_selected, index);
    }

    pub fn toggle_available_option(&mut self, index: usize) {
        Self::toggle_option(&mut self.available_options_selected, index);
    }

    pub fn add_all_visible(&mut self, e: MouseEvent) {
        Self::move_all(
            &mut self.available_options_selected,
            &mut self.available_options,
            &mut self.chosen_options,
        );
        self.emit_onlistchange(e);
        self.emit_callback(&self.add_all);
    }

    pub fn add_selected(&mut self, e: MouseEvent) {
        Self::move_selected(
            &mut self.available_options_selected,
            &mut self.available_options,
            &mut self.chosen_options,
        );
        self.emit_onlistchange(e);
        self.emit_callback(&self.add_selected);
    }

    pub fn remove_selected(&mut self, e: MouseEvent) {
        Self::move_selected(
            &mut self.chosen_options_selected,
            &mut self.chosen_options,
            &mut self.available_options,
        );
        self.emit_onlistchange(e);
        self.emit_callback(&self.remove_selected);
    }

    pub fn remove_all_visible(&mut self, e: MouseEvent) {
        Self::move_all(
            &mut self.chosen_options_selected,
            &mut self.chosen_options,
            &mut self.available_options,
        );
        self.emit_onlistchange(e);
        self.emit_callback(&self.remove_all);
    }

    fn move_all(src_selected: &mut Vec<usize>, src_options: &mut Vec<T>, dst_options: &mut Vec<T>) {
        dst_options.extend_from_slice(src_options);
        src_options.clear();
        src_selected.clear();
    }

    fn move_selected(
        src_selected: &mut Vec<usize>,
        src_options: &mut Vec<T>,
        dst_options: &mut Vec<T>,
    ) {
        let selected_html = src_selected
            .iter()
            .map(|&idx| src_options[idx].clone())
            .collect::<Vec<T>>();
        dst_options.extend_from_slice(&selected_html);
        src_options.retain(|i| !selected_html.contains(i));
        src_selected.clear();
    }

    fn toggle_option(v: &mut Vec<usize>, elem: usize) {
        match v.iter().position(|&x| x == elem) {
            // Remove from selected
            Some(i) => {
                v.remove(i);
            }
            // Add to selected
            None => v.push(elem),
        }
    }

    fn emit_onlistchange(&self, e: MouseEvent) {
        if let Some(f) = &self.onlistchange {
            f.emit((
                e,
                self.available_options.clone(),
                self.chosen_options.clone(),
            ))
        }
    }

    fn emit_callback(&self, f: &Option<Callback<(Vec<T>, Vec<T>)>>) {
        if let Some(f) = f {
            f.emit((self.available_options.clone(), self.chosen_options.clone()));
        }
    }
}

#[function_component(DualListSelector)]
pub fn dual_list_selector<T: DualListSelectorItemRenderer>(
    props: &DualListSelectorProps<T>,
) -> Html {
    let state = use_state(|| State {
        add_selected: props.add_selected.clone(),
        add_all: props.add_all.clone(),
        remove_all: props.remove_all.clone(),
        remove_selected: props.remove_selected.clone(),
        onlistchange: props.onlistchange.clone(),
        available_options: props.available.clone(),
        available_options_selected: Vec::new(),
        chosen_options: props.chosen.clone(),
        chosen_options_selected: Vec::new(),
    });
    let onoptionselect = {
        let state = state.clone();
        let onoptionselect = props.onoptionselect.clone();
        Callback::from(move |args: OnOptionSelectArgs| {
            let mut new_state = (*state).clone();
            let onoptionselect = onoptionselect.clone();
            if args.is_chosen {
                new_state.toggle_chosen_option(args.index);
            } else {
                new_state.toggle_available_option(args.index);
            }
            state.set(new_state);
            if let Some(f) = onoptionselect {
                f.emit(args.clone())
            }
        })
    };
    let available_options_status = props.available_options_status.clone().unwrap_or_else(|| {
        format!(
            "{} of {} item selected",
            state.available_options_selected.len(),
            state.available_options.len()
        )
        .into()
    });
    let chosen_options_status = props.chosen_options_status.clone().unwrap_or_else(|| {
        format!(
            "{} of {} item selected",
            state.chosen_options_selected.len(),
            state.chosen_options.len()
        )
        .into()
    });
    let control_option = |f: fn(&mut State<T>, MouseEvent)| {
        let state = state.clone();
        Callback::from(move |e| {
            let mut new_state = (*state).clone();
            f(&mut new_state, e);
            state.set(new_state);
        })
    };
    html! {
      <div class={classes!["pf-v5-c-dual-list-selector", props.class.clone()]}>
        if !props.children.is_empty() {
            { props.children.clone() }
        } else {
            <DualListSelectorPane<T>
                title={props.available_options_title.clone()}
                status={available_options_status}
                options={state.available_options.clone()}
                onoptionselect={
                    let onoptionselect = onoptionselect.clone();
                    Callback::from(move |args: OnOptionSelectArgsNoChosen| onoptionselect.emit(args.with_chosen(false)))
                }
                selected_options={state.available_options_selected.clone()}
                disabled={props.disabled}
            />
            <DualListSelectorControlsWrapper>
                <DualListSelectorControl
                    tooltip={props.add_selected_tooltip.clone()}
                    disabled={props.disabled}
                    onclick={control_option(State::add_selected)}
                    tooltip_props={props.add_selected_tooltip_props.clone()}
                >
                    { Icon::AngleRight.with_style("width:1em;display:block;") }
                </DualListSelectorControl>
                <DualListSelectorControl
                    tooltip={props.add_all_available_tooltip.clone()}
                    disabled={props.disabled}
                    onclick={control_option(State::add_all_visible)}
                    tooltip_props={props.add_all_available_tooltip_props.clone()}
                >
                    { Icon::AngleDoubleRight.with_style("width:1em;display:block;") }
                </DualListSelectorControl>
                <DualListSelectorControl
                    tooltip={props.remove_all_chosen_tooltip.clone()}
                    disabled={props.disabled}
                    onclick={control_option(State::remove_all_visible)}
                    tooltip_props={props.remove_all_chosen_tooltip_props.clone()}
                >
                    { Icon::AngleDoubleLeft.with_style("width:1em;display:block;") }
                </DualListSelectorControl>
                <DualListSelectorControl
                    tooltip={props.remove_selected_tooltip.clone()}
                    disabled={props.disabled}
                    onclick={control_option(State::remove_selected)}
                    tooltip_props={props.remove_selected_tooltip_props.clone()}
                >
                    { Icon::AngleLeft.with_style("width:1em;display:block;") }
                </DualListSelectorControl>
            </DualListSelectorControlsWrapper>
            <DualListSelectorPane<T>
                is_chosen=true
                title={props.chosen_options_title.clone()}
                status={chosen_options_status}
                options={state.chosen_options.clone()}
                onoptionselect={
                    let onoptionselect = onoptionselect.clone();
                    Callback::from(move |args: OnOptionSelectArgsNoChosen| onoptionselect.emit(args.with_chosen(true)))
                }
                selected_options={state.chosen_options_selected.clone()}
                disabled={props.disabled}
            />
        }
      </div>
    }
}