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 "
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}