perspective_viewer/components/containers/
select.rs1use std::borrow::{Borrow, Cow};
14use std::fmt::Display;
15
16use wasm_bindgen::JsCast;
17use yew::prelude::*;
18
19#[derive(Clone, Eq, PartialEq)]
20pub enum SelectItem<T> {
21 Option(T),
22 OptGroup(Cow<'static, str>, Vec<T>),
23}
24
25impl<T: Display> SelectItem<T> {
26 pub fn name<'a>(&self) -> Cow<'a, str> {
27 match self {
28 Self::Option(x) => format!("{}", x).into(),
29 Self::OptGroup(x, _) => x.clone(),
30 }
31 }
32}
33
34pub enum SelectMsg {
35 SelectedChanged(i32),
36}
37
38#[derive(Properties)]
39pub struct SelectProps<T>
40where
41 T: Clone + Display + PartialEq + 'static,
42{
43 pub values: Vec<SelectItem<T>>,
44 pub selected: T,
45 pub on_select: Callback<T>,
46
47 #[prop_or_default]
48 pub is_autosize: bool,
49
50 #[prop_or_default]
51 pub label: Option<&'static str>,
52
53 #[prop_or_default]
54 pub id: Option<&'static str>,
55
56 #[prop_or_default]
57 pub class: Option<String>,
58
59 #[prop_or_default]
60 pub wrapper_class: Option<String>,
61}
62
63impl<T> PartialEq for SelectProps<T>
64where
65 T: Clone + Display + PartialEq + 'static,
66{
67 fn eq(&self, rhs: &Self) -> bool {
68 self.selected == rhs.selected && self.values == rhs.values
69 }
70}
71
72pub struct Select<T>
75where
76 T: Clone + Display + PartialEq + 'static,
77{
78 select_ref: NodeRef,
79 selected: T,
80}
81
82fn find_nth<T>(mut count: i32, items: &[SelectItem<T>]) -> Option<&T> {
83 for ref item in items.iter() {
84 match item {
85 SelectItem::Option(_) if count > 0 => {
86 count -= 1;
87 },
88 SelectItem::OptGroup(_, items) if count >= items.len() as i32 => {
89 count -= items.len() as i32;
90 },
91 SelectItem::OptGroup(_, items) => return items.get(count as usize),
92 SelectItem::Option(x) => return Some(x),
93 }
94 }
95
96 None
97}
98
99impl<T> Component for Select<T>
100where
101 T: Clone + Display + PartialEq + 'static,
102{
103 type Message = SelectMsg;
104 type Properties = SelectProps<T>;
105
106 fn create(_ctx: &Context<Self>) -> Self {
107 Self {
108 select_ref: NodeRef::default(),
109 selected: _ctx.props().selected.clone(),
110 }
111 }
112
113 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
114 let SelectMsg::SelectedChanged(x) = msg;
115 self.selected = find_nth(x, &ctx.props().values).unwrap().clone();
116 ctx.props().on_select.emit(self.selected.clone());
117 true
118 }
119
120 fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
122 self.selected = ctx.props().selected.clone();
123 true
124 }
125
126 fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
128 if let Some(elem) = self.select_ref.cast::<web_sys::HtmlSelectElement>() {
129 elem.set_value(&format!("{}", self.selected))
130 }
131 }
132
133 fn view(&self, ctx: &Context<Self>) -> Html {
134 let callback = ctx.link().callback(|event: Event| {
135 let value = event
136 .target()
137 .unwrap()
138 .unchecked_into::<web_sys::HtmlSelectElement>()
139 .selected_index();
140 SelectMsg::SelectedChanged(value)
141 });
142
143 let class = if let Some(class) = &ctx.props().class {
144 format!("noselect {}", class)
145 } else {
146 "noselect".to_owned()
147 };
148
149 let is_group_selected = !ctx
150 .props()
151 .values
152 .iter()
153 .any(|x| matches!(x, SelectItem::Option(y) if *y == ctx.props().selected));
154
155 let select = html! {
156 <select id={ctx.props().id} {class} ref={&self.select_ref} onchange={callback}>
157 { for ctx.props().values.iter().map(|value| match value {
158 SelectItem::Option(value) => {
159 let selected = *value == ctx.props().selected;
160 html! {
161 <option
162 key={ format!("{}", value) }
163 selected={ selected }
164 value={ format!("{}", value) }>
165 { format!("{}", value) }
166 </option>
167 }
168 },
169 SelectItem::OptGroup(name, group) => html! {
170 <optgroup
171 key={ name.to_string() }
172 label={ name.to_string() }>
173 {
174 for group.iter().map(|value| {
175 let selected =
176 *value == ctx.props().selected;
177
178 let label = format!("{}", value);
179 let category: &str = name.borrow();
180 let label = label
181 .strip_prefix(category)
182 .unwrap_or(&label)
183 .trim()
184 .to_owned();
185
186 html! {
187 <option
188 key={ format!("{}", value) }
189 selected={ selected }
190 value={ format!("{}", value) }>
191 { label }
192 </option>
193 }
194 })
195 }
196 </optgroup>
197 }
198 }) }
199 </select>
200 };
201
202 let wrapper_class = match &ctx.props().wrapper_class {
203 Some(x) => classes!("dropdown-width-container", x),
204 None => classes!("dropdown-width-container"),
205 };
206
207 let value = if ctx.props().is_autosize {
208 self.selected.to_string()
209 } else {
210 "".to_owned()
211 };
212
213 html! {
214 if is_group_selected && ctx.props().label.is_some() {
215 <label>{ ctx.props().label.unwrap() }</label>
216 <div class={wrapper_class} data-value={value.clone()}>{ select }</div>
217 } else {
218 <div class={wrapper_class} data-value={value}>{ select }</div>
219 }
220 }
221 }
222}