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 super::style::StyleSurface;
24use crate::session::Session;
25use crate::utils::*;
26use crate::*;
27
28#[derive(Default)]
29struct FilterDropDownState {
30    values: Vec<String>,
31    selected: usize,
32    on_select: Option<Callback<String>>,
33    target: Option<HtmlElement>,
34}
35
36#[derive(Clone)]
37pub struct FilterDropDownElement {
38    state: Rc<RefCell<FilterDropDownState>>,
39    session: Session,
40    column: Rc<RefCell<Option<(usize, String)>>>,
41    all_values: Rc<RefCell<Option<Vec<String>>>>,
42    notify: Rc<PubSub<()>>,
43}
44
45impl PartialEq for FilterDropDownElement {
46    fn eq(&self, other: &Self) -> bool {
47        Rc::ptr_eq(&self.state, &other.state)
48    }
49}
50
51impl ImplicitClone for FilterDropDownElement {}
52
53impl FilterDropDownElement {
54    pub fn new(session: Session) -> Self {
55        Self {
56            state: Default::default(),
57            session,
58            column: Default::default(),
59            all_values: Default::default(),
60            notify: Rc::default(),
61        }
62    }
63
64    pub fn reautocomplete(&self) {
65        // Re-open portal with current target
66        self.notify.emit(());
67    }
68
69    pub fn autocomplete(
70        &self,
71        column: (usize, String),
72        input: String,
73        exclude: HashSet<String>,
74        target: HtmlElement,
75        callback: Callback<String>,
76    ) {
77        let current_column = self.column.borrow().clone();
78        match current_column {
79            Some(filter_col) if filter_col == column => {
80                let values = filter_values(&input, &self.all_values, &exclude);
81                if values.len() == 1 && values[0] == input {
82                    let _ = self.hide();
83                } else {
84                    let mut s = self.state.borrow_mut();
85                    s.values = values;
86                    s.selected = 0;
87                    s.on_select = Some(callback);
88                    if s.target.is_none() {
89                        s.target = Some(target);
90                    }
91
92                    drop(s);
93                    self.notify.emit(());
94                }
95            },
96            _ => {
97                clone!(
98                    self.state,
99                    self.session,
100                    self.all_values,
101                    self.notify,
102                    old_column = self.column
103                );
104                ApiFuture::spawn(async move {
105                    let fetched =
106                        crate::queries::get_column_values(&session, 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                    surface={StyleSurface::FilterDropdown}
223                    {target}
224                    own_focus=false
225                    {on_close}
226                    theme={ctx.props().theme.clone()}
227                >
228                    <FilterDropDownView {values} {selected} {on_select} />
229                </PortalModal>
230            }
231        } else {
232            html! {}
233        }
234    }
235}
236
237#[derive(Properties, PartialEq)]
238struct FilterDropDownViewProps {
239    values: Vec<String>,
240    selected: usize,
241    on_select: Option<Callback<String>>,
242}
243
244#[function_component]
245fn FilterDropDownView(props: &FilterDropDownViewProps) -> Html {
246    let body = html! {
247        if !props.values.is_empty() {
248            { for props.values
249                    .iter()
250                    .enumerate()
251                    .map(|(idx, value)| {
252                        let click = props.on_select.as_ref().unwrap().reform({
253                            let value = value.clone();
254                            move |_: MouseEvent| value.clone()
255                        });
256
257                        html! {
258                            if idx == props.selected {
259                                <span onmousedown={click} class="selected">{ value }</span>
260                            } else {
261                                <span onmousedown={click}>{ value }</span>
262                            }
263                        }
264                    }) }
265        } else {
266            <span class="no-results">{ "No Completions" }</span>
267        }
268    };
269
270    html! { <>{ body }</> }
271}
272
273fn filter_values(
274    input: &str,
275    values: &Rc<RefCell<Option<Vec<String>>>>,
276    exclude: &HashSet<String>,
277) -> Vec<String> {
278    let input = input.to_lowercase();
279    if let Some(values) = &*values.borrow() {
280        values
281            .iter()
282            .filter(|x| x.to_lowercase().contains(&input) && !exclude.contains(x.as_str()))
283            .take(10)
284            .cloned()
285            .collect::<Vec<String>>()
286    } else {
287        vec![]
288    }
289}