perspective_viewer/components/containers/
select.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use 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
72/// A `<select>` HTML elements, lifted to support parameterization over a set of
73/// values of a type `T`.
74pub 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    // The `<select>` has its own state not refelcted by `SelectProps`.
121    fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
122        self.selected = ctx.props().selected.clone();
123        true
124    }
125
126    // Annoyingly, `<select>` cannot be updated from its HTML alone.
127    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}