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 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 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}