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