perspective_viewer/components/
column_dropdown.rs1use std::cell::RefCell;
14use std::collections::HashSet;
15use std::rc::Rc;
16
17use perspective_client::clone;
18use perspective_client::config::Expression;
19use web_sys::*;
20use yew::html::ImplicitClone;
21use yew::prelude::*;
22
23use super::column_selector::InPlaceColumn;
24use super::portal::PortalModal;
25use super::style::StyleSurface;
26use crate::session::Session;
27use crate::utils::*;
28use crate::*;
29
30#[derive(Default)]
32pub struct ColumnDropDownState {
33 pub values: Vec<InPlaceColumn>,
34 pub selected: usize,
35 pub width: f64,
36 pub on_select: Option<Callback<InPlaceColumn>>,
37 pub target: Option<HtmlElement>,
38 pub no_results: bool,
39}
40
41#[derive(Clone)]
43pub struct ColumnDropDownElement {
44 state: Rc<RefCell<ColumnDropDownState>>,
45 session: Session,
46 notify: Rc<PubSub<()>>,
47}
48
49impl PartialEq for ColumnDropDownElement {
50 fn eq(&self, other: &Self) -> bool {
51 Rc::ptr_eq(&self.state, &other.state)
52 }
53}
54
55impl ImplicitClone for ColumnDropDownElement {}
56
57impl ColumnDropDownElement {
58 pub fn new(session: Session) -> Self {
59 Self {
60 state: Default::default(),
61 session,
62 notify: Rc::default(),
63 }
64 }
65
66 pub fn autocomplete(
67 &self,
68 target: HtmlInputElement,
69 exclude: HashSet<String>,
70 callback: Callback<InPlaceColumn>,
71 ) -> Option<()> {
72 let input = target.value();
73 let small_input = input.to_lowercase();
74 let mut values: Vec<InPlaceColumn> = vec![];
75 {
76 let metadata = self.session.metadata();
77 for col in metadata.get_table_columns()? {
78 if !exclude.contains(col) && col.to_lowercase().contains(&small_input) {
79 values.push(InPlaceColumn::Column(col.to_owned()));
80 }
81 }
82
83 for col in metadata.get_expression_columns() {
84 if !exclude.contains(col) && col.to_lowercase().contains(&small_input) {
85 values.push(InPlaceColumn::Column(col.to_owned()));
86 }
87 }
88 }
89
90 let width = target.get_bounding_client_rect().width();
91 let target_elem: HtmlElement = target.clone().into();
92
93 {
98 let mut s = self.state.borrow_mut();
99 s.values = values;
100 s.selected = 0;
101 s.width = width;
102 s.on_select = Some(callback);
103 s.target = Some(target_elem);
104 s.no_results = s.values.is_empty();
105 }
106 self.notify.emit(());
107
108 if !exclude.contains(&input) {
109 clone!(self.state, self.session, self.notify);
110 ApiFuture::spawn(async move {
111 let is_expr = crate::queries::validate_expr(&session, &input)
112 .await?
113 .is_none();
114 if is_expr && target.value() == input {
115 let mut s = state.borrow_mut();
116 s.values.push(InPlaceColumn::Expression(Expression::new(
117 None,
118 input.into(),
119 )));
120 s.no_results = false;
121 drop(s);
122 notify.emit(());
123 }
124 Ok(())
125 });
126 }
127
128 Some(())
129 }
130
131 pub fn item_select(&self) {
132 let state = self.state.borrow();
133 if let Some(value) = state.values.get(state.selected)
134 && let Some(ref cb) = state.on_select
135 {
136 cb.emit(value.clone());
137 }
138 }
139
140 pub fn item_down(&self) {
141 let mut state = self.state.borrow_mut();
142 state.selected += 1;
143 if state.selected >= state.values.len() {
144 state.selected = 0;
145 }
146
147 drop(state);
148 self.notify.emit(());
149 }
150
151 pub fn item_up(&self) {
152 let mut state = self.state.borrow_mut();
153 if state.selected < 1 {
154 state.selected = state.values.len();
155 }
156
157 state.selected -= 1;
158 drop(state);
159 self.notify.emit(());
160 }
161
162 pub fn hide(&self) -> ApiResult<()> {
163 self.state.borrow_mut().target = None;
164 self.notify.emit(());
165 Ok(())
166 }
167}
168
169#[derive(Properties, PartialEq)]
172pub struct ColumnDropDownPortalProps {
173 pub element: ColumnDropDownElement,
174 pub theme: String,
175}
176
177pub struct ColumnDropDownPortal {
178 _sub: Subscription,
179}
180
181impl Component for ColumnDropDownPortal {
182 type Message = ();
183 type Properties = ColumnDropDownPortalProps;
184
185 fn create(ctx: &Context<Self>) -> Self {
186 let link = ctx.link().clone();
187 let sub = ctx
188 .props()
189 .element
190 .notify
191 .add_listener(move |()| link.send_message(()));
192 Self { _sub: sub }
193 }
194
195 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
196 true
197 }
198
199 fn view(&self, ctx: &Context<Self>) -> Html {
200 let state = ctx.props().element.state.borrow();
201 let target = state.target.clone();
202 let on_close = {
203 let element = ctx.props().element.clone();
204 Callback::from(move |()| {
205 let _ = element.hide();
206 })
207 };
208
209 if target.is_some() {
210 let values = state.values.clone();
211 let selected = state.selected;
212 let width = state.width;
213 let on_select = state.on_select.clone();
214 drop(state);
215
216 html! {
217 <PortalModal
218 tag_name="perspective-dropdown"
219 surface={StyleSurface::ColumnDropdown}
220 {target}
221 own_focus=false
222 {on_close}
223 theme={ctx.props().theme.clone()}
224 >
225 <ColumnDropDownView {values} {selected} {width} {on_select} />
226 </PortalModal>
227 }
228 } else {
229 html! {}
230 }
231 }
232}
233
234#[derive(Properties, PartialEq)]
236struct ColumnDropDownViewProps {
237 values: Vec<InPlaceColumn>,
238 selected: usize,
239 width: f64,
240 on_select: Option<Callback<InPlaceColumn>>,
241}
242
243#[function_component]
244fn ColumnDropDownView(props: &ColumnDropDownViewProps) -> 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 let row = match value {
257 InPlaceColumn::Column(col) => html! {
258 <span>{ col }</span>
259 },
260 InPlaceColumn::Expression(col) => html! {
261 <span id="add-expression"><span class="icon" />{ col.name.clone() }</span>
262 },
263 };
264
265 html! {
266 if idx == props.selected {
267 <span onmousedown={click} class="selected">{ row }</span>
268 } else {
269 <span onmousedown={click}>{ row }</span>
270 }
271 }
272 }) }
273 } else {
274 <span class="no-results" />
275 }
276 };
277
278 let position = format!(
279 ":host{{min-width:{}px;max-width:{}px}}",
280 props.width, props.width
281 );
282
283 html! { <><style>{ position }</style>{ body }</> }
284}