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 metadata = self.session.metadata();
74 let mut values: Vec<InPlaceColumn> = vec![];
75 let small_input = input.to_lowercase();
76 for col in metadata.get_table_columns()? {
77 if !exclude.contains(col) && col.to_lowercase().contains(&small_input) {
78 values.push(InPlaceColumn::Column(col.to_owned()));
79 }
80 }
81
82 for col in self.session.metadata().get_expression_columns() {
83 if !exclude.contains(col) && col.to_lowercase().contains(&small_input) {
84 values.push(InPlaceColumn::Column(col.to_owned()));
85 }
86 }
87
88 clone!(self.state, self.session, self.notify);
89 let target_elem: HtmlElement = target.clone().into();
90 let width = target.get_bounding_client_rect().width();
91 ApiFuture::spawn(async move {
92 if !exclude.contains(&input) {
93 let is_expr = crate::queries::validate_expr(&session, &input)
94 .await?
95 .is_none();
96 if is_expr {
97 values.push(InPlaceColumn::Expression(Expression::new(
98 None,
99 input.into(),
100 )));
101 }
102 }
103
104 let no_results = values.is_empty();
105 {
106 let mut s = state.borrow_mut();
107 s.values = values;
108 s.selected = 0;
109 s.width = width;
110 s.on_select = Some(callback);
111 s.target = Some(target_elem);
112 s.no_results = no_results;
113 }
114 notify.emit(());
115 Ok(())
116 });
117
118 Some(())
119 }
120
121 pub fn item_select(&self) {
122 let state = self.state.borrow();
123 if let Some(value) = state.values.get(state.selected)
124 && let Some(ref cb) = state.on_select
125 {
126 cb.emit(value.clone());
127 }
128 }
129
130 pub fn item_down(&self) {
131 let mut state = self.state.borrow_mut();
132 state.selected += 1;
133 if state.selected >= state.values.len() {
134 state.selected = 0;
135 }
136
137 drop(state);
138 self.notify.emit(());
139 }
140
141 pub fn item_up(&self) {
142 let mut state = self.state.borrow_mut();
143 if state.selected < 1 {
144 state.selected = state.values.len();
145 }
146
147 state.selected -= 1;
148 drop(state);
149 self.notify.emit(());
150 }
151
152 pub fn hide(&self) -> ApiResult<()> {
153 self.state.borrow_mut().target = None;
154 self.notify.emit(());
155 Ok(())
156 }
157}
158
159#[derive(Properties, PartialEq)]
162pub struct ColumnDropDownPortalProps {
163 pub element: ColumnDropDownElement,
164 pub theme: String,
165}
166
167pub struct ColumnDropDownPortal {
168 _sub: Subscription,
169}
170
171impl Component for ColumnDropDownPortal {
172 type Message = ();
173 type Properties = ColumnDropDownPortalProps;
174
175 fn create(ctx: &Context<Self>) -> Self {
176 let link = ctx.link().clone();
177 let sub = ctx
178 .props()
179 .element
180 .notify
181 .add_listener(move |()| link.send_message(()));
182 Self { _sub: sub }
183 }
184
185 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
186 true
187 }
188
189 fn view(&self, ctx: &Context<Self>) -> Html {
190 let state = ctx.props().element.state.borrow();
191 let target = state.target.clone();
192 let on_close = {
193 let element = ctx.props().element.clone();
194 Callback::from(move |()| {
195 let _ = element.hide();
196 })
197 };
198
199 if target.is_some() {
200 let values = state.values.clone();
201 let selected = state.selected;
202 let width = state.width;
203 let on_select = state.on_select.clone();
204 drop(state);
205
206 html! {
207 <PortalModal
208 tag_name="perspective-dropdown"
209 surface={StyleSurface::ColumnDropdown}
210 {target}
211 own_focus=false
212 {on_close}
213 theme={ctx.props().theme.clone()}
214 >
215 <ColumnDropDownView {values} {selected} {width} {on_select} />
216 </PortalModal>
217 }
218 } else {
219 html! {}
220 }
221 }
222}
223
224#[derive(Properties, PartialEq)]
226struct ColumnDropDownViewProps {
227 values: Vec<InPlaceColumn>,
228 selected: usize,
229 width: f64,
230 on_select: Option<Callback<InPlaceColumn>>,
231}
232
233#[function_component]
234fn ColumnDropDownView(props: &ColumnDropDownViewProps) -> Html {
235 let body = html! {
236 if !props.values.is_empty() {
237 { for props.values
238 .iter()
239 .enumerate()
240 .map(|(idx, value)| {
241 let click = props.on_select.as_ref().unwrap().reform({
242 let value = value.clone();
243 move |_: MouseEvent| value.clone()
244 });
245
246 let row = match value {
247 InPlaceColumn::Column(col) => html! {
248 <span>{ col }</span>
249 },
250 InPlaceColumn::Expression(col) => html! {
251 <span id="add-expression"><span class="icon" />{ col.name.clone() }</span>
252 },
253 };
254
255 html! {
256 if idx == props.selected {
257 <span onmousedown={click} class="selected">{ row }</span>
258 } else {
259 <span onmousedown={click}>{ row }</span>
260 }
261 }
262 }) }
263 } else {
264 <span class="no-results" />
265 }
266 };
267
268 let position = format!(
269 ":host{{min-width:{}px;max-width:{}px}}",
270 props.width, props.width
271 );
272
273 html! { <><style>{ position }</style>{ body }</> }
274}