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