Skip to main content

perspective_viewer/components/
filter_dropdown.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::cell::RefCell;
14use std::collections::HashSet;
15use std::rc::Rc;
16
17use perspective_client::clone;
18use web_sys::*;
19use yew::html::ImplicitClone;
20use yew::prelude::*;
21
22use super::portal::PortalModal;
23use crate::session::Session;
24use crate::utils::*;
25use crate::*;
26
27static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/filter-dropdown.css"));
28
29#[derive(Default)]
30struct FilterDropDownState {
31    values: Vec<String>,
32    selected: usize,
33    on_select: Option<Callback<String>>,
34    target: Option<HtmlElement>,
35}
36
37#[derive(Clone)]
38pub struct FilterDropDownElement {
39    state: Rc<RefCell<FilterDropDownState>>,
40    session: Session,
41    column: Rc<RefCell<Option<(usize, String)>>>,
42    all_values: Rc<RefCell<Option<Vec<String>>>>,
43    notify: Rc<PubSub<()>>,
44}
45
46impl PartialEq for FilterDropDownElement {
47    fn eq(&self, other: &Self) -> bool {
48        Rc::ptr_eq(&self.state, &other.state)
49    }
50}
51
52impl ImplicitClone for FilterDropDownElement {}
53
54impl FilterDropDownElement {
55    pub fn new(session: Session) -> Self {
56        Self {
57            state: Default::default(),
58            session,
59            column: Default::default(),
60            all_values: Default::default(),
61            notify: Rc::default(),
62        }
63    }
64
65    pub fn reautocomplete(&self) {
66        // Re-open portal with current target
67        self.notify.emit(());
68    }
69
70    pub fn autocomplete(
71        &self,
72        column: (usize, String),
73        input: String,
74        exclude: HashSet<String>,
75        target: HtmlElement,
76        callback: Callback<String>,
77    ) {
78        let current_column = self.column.borrow().clone();
79        match current_column {
80            Some(filter_col) if filter_col == column => {
81                let values = filter_values(&input, &self.all_values, &exclude);
82                if values.len() == 1 && values[0] == input {
83                    let _ = self.hide();
84                } else {
85                    let mut s = self.state.borrow_mut();
86                    s.values = values;
87                    s.selected = 0;
88                    s.on_select = Some(callback);
89                    if s.target.is_none() {
90                        s.target = Some(target);
91                    }
92
93                    drop(s);
94                    self.notify.emit(());
95                }
96            },
97            _ => {
98                clone!(
99                    self.state,
100                    self.session,
101                    self.all_values,
102                    self.notify,
103                    old_column = self.column
104                );
105                ApiFuture::spawn(async move {
106                    let fetched = session.get_column_values(column.1.clone()).await?;
107                    *all_values.borrow_mut() = Some(fetched);
108                    let values = filter_values(&input, &all_values, &exclude);
109                    let should_hide = values.len() == 1 && values[0] == input;
110
111                    *old_column.borrow_mut() = Some(column);
112                    {
113                        let mut s = state.borrow_mut();
114                        s.on_select = Some(callback);
115                        if should_hide {
116                            let fv = self::filter_values("", &all_values, &exclude);
117                            s.values = fv;
118                            s.target = Some(target);
119                        } else {
120                            s.values = values;
121                            s.target = Some(target);
122                        }
123                        s.selected = 0;
124                    }
125                    if should_hide {
126                        state.borrow_mut().target = None;
127                    }
128
129                    notify.emit(());
130                    Ok(())
131                });
132            },
133        }
134    }
135
136    pub fn item_select(&self) {
137        let state = self.state.borrow();
138        if let Some(value) = state.values.get(state.selected)
139            && let Some(ref cb) = state.on_select
140        {
141            cb.emit(value.clone());
142        }
143    }
144
145    pub fn item_down(&self) {
146        let mut state = self.state.borrow_mut();
147        state.selected += 1;
148        if state.selected >= state.values.len() {
149            state.selected = 0;
150        }
151
152        drop(state);
153        self.notify.emit(());
154    }
155
156    pub fn item_up(&self) {
157        let mut state = self.state.borrow_mut();
158        if state.selected < 1 {
159            state.selected = state.values.len();
160        }
161
162        state.selected -= 1;
163        drop(state);
164        self.notify.emit(());
165    }
166
167    pub fn hide(&self) -> ApiResult<()> {
168        self.state.borrow_mut().target = None;
169        self.column.borrow_mut().take();
170        self.notify.emit(());
171        Ok(())
172    }
173}
174
175#[derive(Properties, PartialEq)]
176pub struct FilterDropDownPortalProps {
177    pub element: FilterDropDownElement,
178    pub theme: String,
179}
180
181pub struct FilterDropDownPortal {
182    _sub: Subscription,
183}
184
185impl Component for FilterDropDownPortal {
186    type Message = ();
187    type Properties = FilterDropDownPortalProps;
188
189    fn create(ctx: &Context<Self>) -> Self {
190        let link = ctx.link().clone();
191        let sub = ctx
192            .props()
193            .element
194            .notify
195            .add_listener(move |()| link.send_message(()));
196        Self { _sub: sub }
197    }
198
199    fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
200        true
201    }
202
203    fn view(&self, ctx: &Context<Self>) -> Html {
204        let state = ctx.props().element.state.borrow();
205        let target = state.target.clone();
206        let on_close = {
207            let element = ctx.props().element.clone();
208            Callback::from(move |()| {
209                let _ = element.hide();
210            })
211        };
212
213        if target.is_some() {
214            let values = state.values.clone();
215            let selected = state.selected;
216            let on_select = state.on_select.clone();
217            drop(state);
218
219            html! {
220                <PortalModal
221                    tag_name="perspective-dropdown"
222                    {target}
223                    own_focus=false
224                    {on_close}
225                    theme={ctx.props().theme.clone()}
226                >
227                    <FilterDropDownView {values} {selected} {on_select} />
228                </PortalModal>
229            }
230        } else {
231            html! {}
232        }
233    }
234}
235
236#[derive(Properties, PartialEq)]
237struct FilterDropDownViewProps {
238    values: Vec<String>,
239    selected: usize,
240    on_select: Option<Callback<String>>,
241}
242
243#[function_component]
244fn FilterDropDownView(props: &FilterDropDownViewProps) -> Html {
245    let body = html! {
246        if !props.values.is_empty() {
247            { for props.values
248                    .iter()
249                    .enumerate()
250                    .map(|(idx, value)| {
251                        let click = props.on_select.as_ref().unwrap().reform({
252                            let value = value.clone();
253                            move |_: MouseEvent| value.clone()
254                        });
255
256                        html! {
257                            if idx == props.selected {
258                                <span onmousedown={click} class="selected">{ value }</span>
259                            } else {
260                                <span onmousedown={click}>{ value }</span>
261                            }
262                        }
263                    }) }
264        } else {
265            <span class="no-results">{ "No Completions" }</span>
266        }
267    };
268
269    html! { <><style>{ CSS }</style>{ body }</> }
270}
271
272fn filter_values(
273    input: &str,
274    values: &Rc<RefCell<Option<Vec<String>>>>,
275    exclude: &HashSet<String>,
276) -> Vec<String> {
277    let input = input.to_lowercase();
278    if let Some(values) = &*values.borrow() {
279        values
280            .iter()
281            .filter(|x| x.to_lowercase().contains(&input) && !exclude.contains(x.as_str()))
282            .take(10)
283            .cloned()
284            .collect::<Vec<String>>()
285    } else {
286        vec![]
287    }
288}