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