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 =
107                        crate::queries::get_column_values(&session, column.1.clone()).await?;
108                    *all_values.borrow_mut() = Some(fetched);
109                    let values = filter_values(&input, &all_values, &exclude);
110                    let should_hide = values.len() == 1 && values[0] == input;
111
112                    *old_column.borrow_mut() = Some(column);
113                    {
114                        let mut s = state.borrow_mut();
115                        s.on_select = Some(callback);
116                        if should_hide {
117                            let fv = self::filter_values("", &all_values, &exclude);
118                            s.values = fv;
119                            s.target = Some(target);
120                        } else {
121                            s.values = values;
122                            s.target = Some(target);
123                        }
124                        s.selected = 0;
125                    }
126                    if should_hide {
127                        state.borrow_mut().target = None;
128                    }
129
130                    notify.emit(());
131                    Ok(())
132                });
133            },
134        }
135    }
136
137    pub fn item_select(&self) {
138        let state = self.state.borrow();
139        if let Some(value) = state.values.get(state.selected)
140            && let Some(ref cb) = state.on_select
141        {
142            cb.emit(value.clone());
143        }
144    }
145
146    pub fn item_down(&self) {
147        let mut state = self.state.borrow_mut();
148        state.selected += 1;
149        if state.selected >= state.values.len() {
150            state.selected = 0;
151        }
152
153        drop(state);
154        self.notify.emit(());
155    }
156
157    pub fn item_up(&self) {
158        let mut state = self.state.borrow_mut();
159        if state.selected < 1 {
160            state.selected = state.values.len();
161        }
162
163        state.selected -= 1;
164        drop(state);
165        self.notify.emit(());
166    }
167
168    pub fn hide(&self) -> ApiResult<()> {
169        self.state.borrow_mut().target = None;
170        self.column.borrow_mut().take();
171        self.notify.emit(());
172        Ok(())
173    }
174}
175
176#[derive(Properties, PartialEq)]
177pub struct FilterDropDownPortalProps {
178    pub element: FilterDropDownElement,
179    pub theme: String,
180}
181
182pub struct FilterDropDownPortal {
183    _sub: Subscription,
184}
185
186impl Component for FilterDropDownPortal {
187    type Message = ();
188    type Properties = FilterDropDownPortalProps;
189
190    fn create(ctx: &Context<Self>) -> Self {
191        let link = ctx.link().clone();
192        let sub = ctx
193            .props()
194            .element
195            .notify
196            .add_listener(move |()| link.send_message(()));
197        Self { _sub: sub }
198    }
199
200    fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
201        true
202    }
203
204    fn view(&self, ctx: &Context<Self>) -> Html {
205        let state = ctx.props().element.state.borrow();
206        let target = state.target.clone();
207        let on_close = {
208            let element = ctx.props().element.clone();
209            Callback::from(move |()| {
210                let _ = element.hide();
211            })
212        };
213
214        if target.is_some() {
215            let values = state.values.clone();
216            let selected = state.selected;
217            let on_select = state.on_select.clone();
218            drop(state);
219
220            html! {
221                <PortalModal
222                    tag_name="perspective-dropdown"
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! { <><style>{ CSS }</style>{ 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}