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