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 crate::session::Session;
26use crate::utils::*;
27use crate::*;
28
29static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/column-dropdown.css"));
30
31#[derive(Default)]
33pub struct ColumnDropDownState {
34 pub values: Vec<InPlaceColumn>,
35 pub selected: usize,
36 pub width: f64,
37 pub on_select: Option<Callback<InPlaceColumn>>,
38 pub target: Option<HtmlElement>,
39 pub no_results: bool,
40}
41
42#[derive(Clone)]
44pub struct ColumnDropDownElement {
45 state: Rc<RefCell<ColumnDropDownState>>,
46 session: Session,
47 notify: Rc<PubSub<()>>,
48}
49
50impl PartialEq for ColumnDropDownElement {
51 fn eq(&self, other: &Self) -> bool {
52 Rc::ptr_eq(&self.state, &other.state)
53 }
54}
55
56impl ImplicitClone for ColumnDropDownElement {}
57
58impl ColumnDropDownElement {
59 pub fn new(session: Session) -> Self {
60 Self {
61 state: Default::default(),
62 session,
63 notify: Rc::default(),
64 }
65 }
66
67 pub fn autocomplete(
68 &self,
69 target: HtmlInputElement,
70 exclude: HashSet<String>,
71 callback: Callback<InPlaceColumn>,
72 ) -> Option<()> {
73 let input = target.value();
74 let metadata = self.session.metadata();
75 let mut values: Vec<InPlaceColumn> = vec![];
76 let small_input = input.to_lowercase();
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 self.session.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 clone!(self.state, self.session, self.notify);
90 let target_elem: HtmlElement = target.clone().into();
91 let width = target.get_bounding_client_rect().width();
92 ApiFuture::spawn(async move {
93 if !exclude.contains(&input) {
94 let is_expr = crate::queries::validate_expr(&session, &input)
95 .await?
96 .is_none();
97 if is_expr {
98 values.push(InPlaceColumn::Expression(Expression::new(
99 None,
100 input.into(),
101 )));
102 }
103 }
104
105 let no_results = values.is_empty();
106 {
107 let mut s = state.borrow_mut();
108 s.values = values;
109 s.selected = 0;
110 s.width = width;
111 s.on_select = Some(callback);
112 s.target = Some(target_elem);
113 s.no_results = no_results;
114 }
115 notify.emit(());
116 Ok(())
117 });
118
119 Some(())
120 }
121
122 pub fn item_select(&self) {
123 let state = self.state.borrow();
124 if let Some(value) = state.values.get(state.selected)
125 && let Some(ref cb) = state.on_select
126 {
127 cb.emit(value.clone());
128 }
129 }
130
131 pub fn item_down(&self) {
132 let mut state = self.state.borrow_mut();
133 state.selected += 1;
134 if state.selected >= state.values.len() {
135 state.selected = 0;
136 }
137
138 drop(state);
139 self.notify.emit(());
140 }
141
142 pub fn item_up(&self) {
143 let mut state = self.state.borrow_mut();
144 if state.selected < 1 {
145 state.selected = state.values.len();
146 }
147
148 state.selected -= 1;
149 drop(state);
150 self.notify.emit(());
151 }
152
153 pub fn hide(&self) -> ApiResult<()> {
154 self.state.borrow_mut().target = None;
155 self.notify.emit(());
156 Ok(())
157 }
158}
159
160#[derive(Properties, PartialEq)]
163pub struct ColumnDropDownPortalProps {
164 pub element: ColumnDropDownElement,
165 pub theme: String,
166}
167
168pub struct ColumnDropDownPortal {
169 _sub: Subscription,
170}
171
172impl Component for ColumnDropDownPortal {
173 type Message = ();
174 type Properties = ColumnDropDownPortalProps;
175
176 fn create(ctx: &Context<Self>) -> Self {
177 let link = ctx.link().clone();
178 let sub = ctx
179 .props()
180 .element
181 .notify
182 .add_listener(move |()| link.send_message(()));
183 Self { _sub: sub }
184 }
185
186 fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
187 true
188 }
189
190 fn view(&self, ctx: &Context<Self>) -> Html {
191 let state = ctx.props().element.state.borrow();
192 let target = state.target.clone();
193 let on_close = {
194 let element = ctx.props().element.clone();
195 Callback::from(move |()| {
196 let _ = element.hide();
197 })
198 };
199
200 if target.is_some() {
201 let values = state.values.clone();
202 let selected = state.selected;
203 let width = state.width;
204 let on_select = state.on_select.clone();
205 drop(state);
206
207 html! {
208 <PortalModal
209 tag_name="perspective-dropdown"
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>{ CSS }</style><style>{ position }</style>{ body }</> }
274}