perspective_viewer/components/
filter_dropdown.rs1use 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 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}