perspective_viewer/components/
filter_dropdown.rs1use std::cell::RefCell;
14use std::collections::HashSet;
15use std::rc::Rc;
16
17use web_sys::*;
18use yew::html::ImplicitClone;
19use yew::prelude::*;
20
21use super::style::StyleSurface;
22use crate::session::Session;
23use crate::ui::PortalModal;
24use crate::utils::*;
25use crate::*;
26
27struct OpenState {
29 values: Vec<String>,
30 selected: usize,
31 on_select: Callback<String>,
32 target: HtmlElement,
33}
34
35#[derive(Default)]
36enum FilterDropDownState {
37 #[default]
38 Hidden,
39 Open(OpenState),
40}
41
42#[derive(Clone)]
43pub struct FilterDropDownElement {
44 state: Rc<RefCell<FilterDropDownState>>,
45 session: Session,
46 column: Rc<RefCell<Option<(usize, String)>>>,
47 all_values: Rc<RefCell<Option<Vec<String>>>>,
48 notify: Rc<PubSub<()>>,
49}
50
51impl PartialEq for FilterDropDownElement {
52 fn eq(&self, other: &Self) -> bool {
53 Rc::ptr_eq(&self.state, &other.state)
54 }
55}
56
57impl ImplicitClone for FilterDropDownElement {}
58
59impl FilterDropDownElement {
60 pub fn new(session: Session) -> Self {
61 Self {
62 state: Default::default(),
63 session,
64 column: Default::default(),
65 all_values: Default::default(),
66 notify: Rc::default(),
67 }
68 }
69
70 pub fn reautocomplete(&self) {
71 self.notify.emit(());
72 }
73
74 pub fn autocomplete(
75 &self,
76 column: (usize, String),
77 input: String,
78 exclude: HashSet<String>,
79 target: HtmlElement,
80 callback: Callback<String>,
81 ) {
82 let current_column = self.column.borrow().clone();
83 match current_column {
84 Some(filter_col) if filter_col == column => {
85 let values = filter_values(&input, &self.all_values, &exclude);
86 self.show(values, &input, target, callback);
87 },
88 _ => {
89 let this = self.clone();
90 ApiFuture::spawn(async move {
91 let fetched =
92 crate::queries::get_column_values(&this.session, column.1.clone()).await?;
93 *this.all_values.borrow_mut() = Some(fetched);
94 *this.column.borrow_mut() = Some(column);
95 let values = filter_values(&input, &this.all_values, &exclude);
96 this.show(values, &input, target, callback);
97 Ok(())
98 });
99 },
100 }
101 }
102
103 fn show(
105 &self,
106 values: Vec<String>,
107 input: &str,
108 target: HtmlElement,
109 on_select: Callback<String>,
110 ) {
111 if values.len() == 1 && values[0] == input {
112 self.dismiss();
113 } else {
114 *self.state.borrow_mut() = FilterDropDownState::Open(OpenState {
115 values,
116 selected: 0,
117 on_select,
118 target,
119 });
120
121 self.notify.emit(());
122 }
123 }
124
125 pub fn item_select(&self) {
127 let selected = match &*self.state.borrow() {
128 FilterDropDownState::Open(open) => open
129 .values
130 .get(open.selected)
131 .map(|value| (value.clone(), open.on_select.clone())),
132 FilterDropDownState::Hidden => None,
133 };
134
135 if let Some((value, on_select)) = selected {
136 on_select.emit(value);
137 }
138 }
139
140 pub fn item_down(&self) {
141 self.step(1);
142 }
143
144 pub fn item_up(&self) {
145 self.step(-1);
146 }
147
148 fn step(&self, delta: isize) {
149 let moved = match &mut *self.state.borrow_mut() {
150 FilterDropDownState::Open(open) if !open.values.is_empty() => {
151 let len = open.values.len() as isize;
152 open.selected = (open.selected as isize + delta).rem_euclid(len) as usize;
153 true
154 },
155 _ => false,
156 };
157
158 if moved {
159 self.notify.emit(());
160 }
161 }
162
163 pub fn dismiss(&self) {
165 *self.state.borrow_mut() = FilterDropDownState::Hidden;
166 self.notify.emit(());
167 }
168
169 pub fn reset(&self) {
171 self.column.borrow_mut().take();
172 self.all_values.borrow_mut().take();
173 self.dismiss();
174 }
175}
176
177#[derive(Properties, PartialEq)]
178pub struct FilterDropDownPortalProps {
179 pub element: FilterDropDownElement,
180 pub theme: String,
181}
182
183pub struct FilterDropDownPortal {
184 _sub: Subscription,
185}
186
187impl Component for FilterDropDownPortal {
188 type Message = ();
189 type Properties = FilterDropDownPortalProps;
190
191 fn create(ctx: &Context<Self>) -> Self {
192 let link = ctx.link().clone();
193 let sub = ctx
194 .props()
195 .element
196 .notify
197 .add_listener(move |()| link.send_message(()));
198 Self { _sub: sub }
199 }
200
201 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
202 true
203 }
204
205 fn view(&self, ctx: &Context<Self>) -> Html {
206 let element = &ctx.props().element;
207 let state = element.state.borrow();
208 let FilterDropDownState::Open(open) = &*state else {
209 return html! {};
210 };
211
212 let on_close = {
213 let element = element.clone();
214 Callback::from(move |()| element.dismiss())
215 };
216
217 html! {
218 <PortalModal
219 tag_name="perspective-dropdown"
220 sheet={StyleSurface::FilterDropdown.sheet()}
221 target={Some(open.target.clone())}
222 own_focus=false
223 {on_close}
224 theme={ctx.props().theme.clone()}
225 >
226 <FilterDropDownView
227 values={open.values.clone()}
228 selected={open.selected}
229 on_select={open.on_select.clone()}
230 />
231 </PortalModal>
232 }
233 }
234}
235
236#[derive(Properties, PartialEq)]
237struct FilterDropDownViewProps {
238 values: Vec<String>,
239 selected: usize,
240 on_select: 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.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! { <>{ 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}