jinya_ui/widgets/form/
multi_select.rs

1use std::rc::Rc;
2
3use yew::prelude::*;
4use yew::services::ConsoleService;
5use yew::{Callback, Component, ComponentLink, Html};
6
7pub fn get_css<'a>() -> &'a str {
8    // language=CSS
9    "
10.jinya-multi-select__color-container {
11    width: 100%;
12}
13
14.jinya-multi-select__color-container--default {
15    --state-color: var(--primary-color);
16}
17
18.jinya-multi-select__color-container--negative {
19    --state-color: var(--negative-color);
20}
21
22.jinya-multi-select__color-container--positive {
23    --state-color: var(--positive-color);
24}
25
26.jinya-multi-select__color-container--disabled {
27    --state-color: var(--disabled-border-color);
28}
29
30.jinya-multi-select__container {
31    display: inline-block;
32    border: 2px solid var(--state-color);
33    border-radius: 5px;
34    padding: 0.5rem 0.75rem 0.25rem;
35    position: relative;
36    margin-top: 0.75rem;
37    width: 100%;
38    box-sizing: border-box;
39}
40
41.jinya-multi-select__item-holder {
42    font-size: var(--font-size-16);
43    color: var(--state-color);
44    background: var(--white);
45    border: none;
46    padding: 0;
47    width: 100%;
48    display: flex;
49    align-items: center;
50    flex-wrap: wrap;
51}
52
53.jinya-multi-select__item-holder:disabled {
54    cursor: not-allowed;
55}
56
57.jinya-multi-select__item-holder:invalid {
58    outline: none;
59    box-shadow: none;
60    border: none;
61}
62
63.jinya-multi-select__label {
64    display: block;
65    font-size: var(--font-size-12);
66    color: var(--state-color);
67    position: absolute;
68    top: -0.75rem;
69    background: var(--white);
70    padding-left: 0.25rem;
71    padding-right: 0.25rem;
72    box-sizing: border-box;
73    left: 0.5rem;
74    z-index: 0;
75}
76
77.jinya-multi-select__validation-message {
78    display: block;
79    font-size: var(--font-size-12);
80    color: var(--state-color);
81}
82
83.jinya-multi-select__chip {
84    display: inline-block;
85    font-size: var(--font-size-12);
86    color: var(--state-color);
87    border: 2px solid var(--state-color);
88    position: relative;
89    border-radius: 5px;
90    line-height: var(--line-height-18);
91    padding: 0 0.25rem;
92    margin-right: 0.5rem;
93}
94
95.jinya-multi-select__search-field {
96    flex: 1;
97    padding: 0;
98    margin: 0;
99    border: 0;
100    font-size: var(--font-size-16);
101    color: var(--state-color);
102    font-family: var(--font-family);
103    background: var(--white);
104    outline: none;
105}
106
107.jinya-multi-select__dropdown {
108    position: absolute;
109    display: none;
110    width: calc(100% - 10px);
111    background: var(--white);
112    border: 2px solid var(--state-color);
113    left: 2px;
114    top: 2.25rem;
115    border-bottom-left-radius: 5px;
116    border-bottom-right-radius: 5px;
117    max-height: 15rem;
118    overflow-y: auto;
119}
120
121.jinya-multi-select__search-field:focus + .jinya-multi-select__dropdown,
122.jinya-multi-select__dropdown--open {
123    display: block;
124}
125
126.jinya-multi-select__dropdown-item {
127    padding: 0.25rem 0.5rem;
128}
129
130.jinya-multi-select__dropdown-item:hover {
131    background: var(--input-background-color);
132    cursor: pointer;
133}
134"
135}
136
137struct Chip {
138    link: ComponentLink<Self>,
139    item: MultiSelectItem,
140    on_remove: Callback<MultiSelectItem>,
141}
142
143enum ChipMsg {
144    Remove,
145}
146
147#[derive(Clone, PartialEq, Properties)]
148struct ChipProps {
149    pub item: MultiSelectItem,
150    pub on_remove: Callback<MultiSelectItem>,
151}
152
153impl Component for Chip {
154    type Message = ChipMsg;
155    type Properties = ChipProps;
156
157    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
158        Chip {
159            link,
160            item: props.item,
161            on_remove: props.on_remove,
162        }
163    }
164
165    fn update(&mut self, msg: Self::Message) -> bool {
166        match msg {
167            ChipMsg::Remove => self.on_remove.emit(self.item.clone()),
168        }
169        true
170    }
171
172    fn change(&mut self, _props: Self::Properties) -> bool {
173        false
174    }
175
176    fn view(&self) -> Html {
177        html! {
178            <div class="jinya-multi-select__chip">
179                {&self.item.text}
180                <a href="#" class="mdi mdi-close" onclick=self.link.callback(|_| ChipMsg::Remove)></a>
181            </div>
182        }
183    }
184}
185
186struct DropdownItem {
187    link: ComponentLink<Self>,
188    item: MultiSelectItem,
189    on_select: Callback<MultiSelectItem>,
190}
191
192enum DropdownItemMsg {
193    Select,
194}
195
196#[derive(Clone, PartialEq, Properties)]
197struct DropdownItemProps {
198    pub item: MultiSelectItem,
199    pub on_select: Callback<MultiSelectItem>,
200}
201
202impl Component for DropdownItem {
203    type Message = DropdownItemMsg;
204    type Properties = DropdownItemProps;
205
206    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
207        DropdownItem {
208            link,
209            item: props.item,
210            on_select: props.on_select,
211        }
212    }
213
214    fn update(&mut self, msg: Self::Message) -> bool {
215        match msg {
216            DropdownItemMsg::Select => {
217                ConsoleService::log("selected");
218                self.on_select.emit(self.item.clone());
219            }
220        }
221        true
222    }
223
224    fn change(&mut self, _props: Self::Properties) -> bool {
225        false
226    }
227
228    fn view(&self) -> Html {
229        html! {
230            <div class="jinya-multi-select__dropdown-item" onclick=self.link.callback(|_| DropdownItemMsg::Select)>
231                {&self.item.text}
232            </div>
233        }
234    }
235}
236
237#[derive(Clone, PartialEq)]
238pub enum MultiSelectState {
239    Default,
240    Negative,
241    Positive,
242}
243
244pub struct MultiSelect {
245    link: ComponentLink<Self>,
246    label: String,
247    on_select: Callback<MultiSelectItem>,
248    on_deselect: Callback<MultiSelectItem>,
249    on_filter: Callback<String>,
250    state: MultiSelectState,
251    validation_message: String,
252    placeholder: String,
253    disabled: bool,
254    options: Vec<MultiSelectItem>,
255    selected_items: Vec<MultiSelectItem>,
256    flyout_open: bool,
257}
258
259#[derive(Clone, PartialEq, Properties)]
260pub struct MultiSelectProps {
261    pub label: String,
262    pub on_select: Callback<MultiSelectItem>,
263    pub on_deselect: Callback<MultiSelectItem>,
264    #[prop_or_default]
265    pub on_filter: Callback<String>,
266    #[prop_or(MultiSelectState::Default)]
267    pub state: MultiSelectState,
268    #[prop_or("".to_string())]
269    pub validation_message: String,
270    #[prop_or("".to_string())]
271    pub placeholder: String,
272    #[prop_or(false)]
273    pub disabled: bool,
274    pub options: Vec<MultiSelectItem>,
275    pub selected_items: Vec<MultiSelectItem>,
276}
277
278pub enum Msg {
279    Select(MultiSelectItem),
280    Remove(MultiSelectItem),
281    Filter(String),
282    CloseFlyout,
283    OpenFlyout,
284}
285
286impl Default for MultiSelectState {
287    fn default() -> Self {
288        MultiSelectState::Default
289    }
290}
291
292#[derive(Clone, PartialEq, Properties)]
293pub struct MultiSelectItem {
294    pub value: String,
295    pub text: String,
296}
297
298impl Component for MultiSelect {
299    type Message = Msg;
300    type Properties = MultiSelectProps;
301
302    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
303        MultiSelect {
304            link,
305            label: props.label,
306            on_select: props.on_select,
307            on_deselect: props.on_deselect,
308            state: props.state,
309            validation_message: props.validation_message,
310            placeholder: props.placeholder,
311            disabled: props.disabled,
312            options: props.options,
313            selected_items: props.selected_items,
314            on_filter: props.on_filter,
315            flyout_open: false,
316        }
317    }
318
319    fn update(&mut self, msg: Self::Message) -> bool {
320        match msg {
321            Msg::Select(value) => {
322                ConsoleService::log(format!("{} {}", value.text, value.value).as_str());
323                self.on_select.emit(value);
324            }
325            Msg::Remove(value) => {
326                self.on_deselect.emit(value);
327            }
328            Msg::Filter(value) => {
329                self.on_filter.emit(value);
330            }
331            Msg::CloseFlyout => {
332                self.flyout_open = false;
333            }
334            Msg::OpenFlyout => {
335                self.flyout_open = true;
336            }
337        }
338
339        true
340    }
341
342    fn change(&mut self, _props: Self::Properties) -> bool {
343        self.label = _props.label;
344        self.state = _props.state;
345        self.validation_message = _props.validation_message;
346        self.placeholder = _props.placeholder;
347        self.disabled = _props.disabled;
348        self.options = _props.options;
349        self.on_deselect = _props.on_deselect;
350        self.on_select = _props.on_select;
351        self.selected_items = _props.selected_items;
352
353        true
354    }
355
356    fn view(&self) -> Html {
357        let id = super::super::super::id_generator::generate_id();
358        html! {
359            <div class=self.get_multi_select_container_class() onmouseleave=self.link.callback(|_| Msg::CloseFlyout)>
360                <div class="jinya-multi-select__container">
361                    <label for=id class="jinya-multi-select__label">{&self.label}</label>
362                    <div disabled=self.disabled placeholder=self.placeholder class="jinya-multi-select__item-holder">
363                        {for self.selected_items.iter().enumerate().map(|(_, mut item)| {
364                            let key = Rc::new(format!("chip-{}", item.value));
365                            html! {
366                                <Chip key=key item=item on_remove=self.link.callback(|value| Msg::Remove(value)) />
367                            }
368                        })}
369                        <input
370                            id=id
371                            class="jinya-multi-select__search-field"
372                            oninput=self.link.callback(|e: InputData| Msg::Filter(e.value))
373                            onfocus=self.link.callback(|_| Msg::OpenFlyout)
374                            placeholder=self.get_placeholder()
375                        />
376                        <div class=self.get_flyout_class()>
377                            {for self.options.iter().enumerate().map(|(_, mut item)| {
378                                let key = Rc::new(format!("item-{}", item.value));
379                                html! {
380                                    <DropdownItem key=key item=item on_select=self.link.callback(|value| Msg::Select(value)) />
381                                }
382                            })}
383                        </div>
384                    </div>
385                </div>
386                <span class="jinya-multi-select__validation-message">{&self.validation_message}</span>
387            </div>
388        }
389    }
390}
391
392impl MultiSelect {
393    fn get_placeholder(&self) -> String {
394        let placeholder = &self.placeholder;
395        if self.selected_items.is_empty() {
396            placeholder.to_string()
397        } else {
398            "".to_string()
399        }
400    }
401
402    fn get_flyout_class(&self) -> String {
403        if self.flyout_open {
404            "jinya-multi-select__dropdown jinya-multi-select__dropdown--open".to_string()
405        } else {
406            "jinya-multi-select__dropdown".to_string()
407        }
408    }
409
410    fn get_multi_select_container_class(&self) -> String {
411        let class = match self.state {
412            MultiSelectState::Default => {
413                "jinya-multi-select__color-container jinya-multi-select__color-container--default"
414            }
415            MultiSelectState::Negative => {
416                "jinya-multi-select__color-container jinya-multi-select__color-container--negative"
417            }
418            MultiSelectState::Positive => {
419                "jinya-multi-select__color-container jinya-multi-select__color-container--positive"
420            }
421        }
422        .to_string();
423
424        if self.disabled {
425            "jinya-multi-select__color-container jinya-multi-select__color-container--disabled"
426                .to_string()
427        } else {
428            class
429        }
430    }
431}