perspective_viewer/components/
empty_row.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::collections::HashSet;
14use std::rc::Rc;
15
16use derivative::Derivative;
17use perspective_client::clone;
18use wasm_bindgen::JsCast;
19use web_sys::*;
20use yew::prelude::*;
21
22use crate::components::style::LocalStyle;
23use crate::css;
24use crate::custom_elements::FilterDropDownElement;
25
26#[derive(Default)]
27pub struct EmptyRow {
28    input_ref: NodeRef,
29}
30
31#[derive(Properties, Derivative)]
32#[derivative(Debug)]
33pub struct EmptyRowProps {
34    #[derivative(Debug = "ignore")]
35    pub dropdown: Rc<FilterDropDownElement>,
36    pub exclude: HashSet<String>,
37    pub on_select: Callback<String>,
38    pub focused: bool,
39    pub index: usize,
40    pub set_focused_index: Callback<Option<usize>>,
41    pub value: String,
42    pub column_name: String,
43}
44
45impl PartialEq for EmptyRowProps {
46    fn eq(&self, _other: &Self) -> bool {
47        false
48    }
49}
50
51pub enum EmptyRowMsg {
52    KeyDown(u32),
53    Blur,
54    Input(String),
55    Focus,
56}
57
58use EmptyRowMsg::*;
59
60impl Component for EmptyRow {
61    type Message = EmptyRowMsg;
62    type Properties = EmptyRowProps;
63
64    fn view(&self, ctx: &Context<Self>) -> Html {
65        let onblur = ctx.link().callback(|_| Blur);
66        let oninput = ctx.link().callback(|e: InputEvent| {
67            let value = e
68                .target()
69                .unwrap()
70                .unchecked_into::<HtmlInputElement>()
71                .value();
72            Input(value)
73        });
74        let onkeydown = ctx
75            .link()
76            .callback(|event: KeyboardEvent| KeyDown(event.key_code()));
77
78        if ctx.props().focused {
79            // do this on the next render cycle so we know the ref is there
80            ctx.link()
81                .send_message_batch(vec![Focus, Input(ctx.props().value.clone())]);
82        }
83        let onfocus = {
84            clone!(ctx.props().value);
85            ctx.link().callback(move |_| Input(value.clone()))
86        };
87
88        html! {
89            <div class="pivot-column column-empty">
90                <LocalStyle href={css!("empty-column")} />
91                <input
92                    spellcheck="false"
93                    ref={self.input_ref.clone()}
94                    {onblur}
95                    {onkeydown}
96                    {oninput}
97                    {onfocus}
98                    class="column-empty-input"
99                />
100            </div>
101        }
102    }
103
104    fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
105        if let Some(elem) = self.input_ref.cast::<HtmlInputElement>() {
106            elem.blur().unwrap();
107        }
108        false
109    }
110
111    fn destroy(&mut self, ctx: &Context<Self>) {
112        ctx.props().dropdown.hide().unwrap();
113    }
114
115    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
116        let p = ctx.props();
117        match msg {
118            Focus => {
119                if let Some(elem) = self.input_ref.cast::<HtmlInputElement>() {
120                    elem.set_value(&ctx.props().value);
121                    elem.focus().unwrap();
122                }
123
124                false
125            },
126            Blur => {
127                p.dropdown.hide().unwrap();
128                if let Some(elem) = self.input_ref.cast::<HtmlInputElement>() {
129                    elem.set_value("");
130                }
131                p.set_focused_index.emit(Some(p.index + 1));
132
133                false
134            },
135            KeyDown(40) => {
136                p.dropdown.item_down();
137                false
138            },
139            KeyDown(38) => {
140                p.dropdown.item_up();
141                false
142            },
143            KeyDown(13) => {
144                p.dropdown.item_select();
145                p.dropdown.hide().unwrap();
146                p.set_focused_index.emit(Some(p.index + 1));
147                true
148            },
149            KeyDown(_) => false,
150            Input(value) => {
151                if let Some(elem) = self.input_ref.cast::<HtmlElement>() {
152                    ctx.props().dropdown.autocomplete(
153                        (ctx.props().index, ctx.props().column_name.clone()),
154                        value,
155                        ctx.props().exclude.clone(),
156                        elem,
157                        ctx.props().on_select.clone(),
158                    );
159                }
160
161                false
162            },
163        }
164    }
165
166    fn create(_ctx: &Context<Self>) -> Self {
167        Self::default()
168    }
169}