Skip to main content

perspective_viewer/components/
editable_header.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::rc::Rc;
14
15use itertools::Itertools;
16use web_sys::{FocusEvent, HtmlInputElement, KeyboardEvent};
17use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, html};
18
19use super::type_icon::TypeIconType;
20use crate::components::type_icon::TypeIcon;
21use crate::session::Session;
22use crate::*;
23
24#[derive(Clone, PartialEq, Properties, PerspectiveProperties!)]
25pub struct EditableHeaderProps {
26    pub icon_type: Option<TypeIconType>,
27    pub on_change: Callback<(Option<String>, bool)>,
28    pub editable: bool,
29    pub initial_value: Option<String>,
30    pub placeholder: Rc<String>,
31
32    #[prop_or_default]
33    pub reset_count: u8,
34
35    // State
36    pub session: Session,
37}
38
39impl EditableHeaderProps {
40    fn split_placeholder(&self) -> String {
41        let split = self
42            .placeholder
43            .split_once('\n')
44            .map(|(a, _)| a)
45            .unwrap_or(&*self.placeholder);
46
47        match split.char_indices().nth(25) {
48            None => split.to_string(),
49            Some((idx, _)) => split[..idx].to_owned(),
50        }
51    }
52}
53
54pub enum EditableHeaderMsg {
55    SetNewValue(String),
56    OnClick(()),
57}
58
59#[derive(Debug)]
60pub struct EditableHeader {
61    noderef: NodeRef,
62    edited: bool,
63    valid: bool,
64    value: Option<String>,
65    placeholder: String,
66}
67
68impl Component for EditableHeader {
69    type Message = EditableHeaderMsg;
70    type Properties = EditableHeaderProps;
71
72    fn create(ctx: &yew::prelude::Context<Self>) -> Self {
73        Self {
74            value: ctx.props().initial_value.clone(),
75            placeholder: ctx.props().split_placeholder(),
76            valid: true,
77            noderef: NodeRef::default(),
78            edited: false,
79        }
80    }
81
82    fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
83        if ctx.props().reset_count != old_props.reset_count {
84            self.value.clone_from(&ctx.props().initial_value);
85        }
86        if ctx.props().initial_value != old_props.initial_value {
87            self.edited = false;
88            self.value.clone_from(&ctx.props().initial_value);
89        }
90        if !ctx.props().editable {
91            self.edited = false;
92        }
93        self.placeholder = ctx.props().split_placeholder();
94        ctx.props() != old_props
95    }
96
97    fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
98        match msg {
99            EditableHeaderMsg::SetNewValue(new_value) => {
100                let maybe_value = (!new_value.is_empty()).then_some(new_value.clone());
101                self.edited = ctx.props().initial_value != maybe_value;
102
103                self.valid = maybe!({
104                    if maybe_value
105                        .as_ref()
106                        .map(|v| v == &self.placeholder)
107                        .unwrap_or(true)
108                    {
109                        return Some(true);
110                    }
111                    if !self.edited {
112                        return Some(true);
113                    }
114                    let metadata = ctx.props().session.metadata();
115                    let expressions = metadata.get_expression_columns();
116                    let found = metadata
117                        .get_table_columns()?
118                        .iter()
119                        .chain(expressions)
120                        .contains(&new_value);
121                    Some(!found)
122                })
123                .unwrap_or(true);
124
125                self.value.clone_from(&maybe_value);
126                ctx.props().on_change.emit((maybe_value, self.valid));
127                true
128            },
129            EditableHeaderMsg::OnClick(()) => {
130                self.noderef
131                    .cast::<HtmlInputElement>()
132                    .unwrap()
133                    .focus()
134                    .unwrap();
135                false
136            },
137        }
138    }
139
140    fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
141        let mut classes = classes!("sidebar_header_contents");
142        if ctx.props().editable {
143            classes.push("editable");
144        }
145
146        if !self.valid {
147            classes.push("invalid");
148        }
149
150        if self.edited {
151            classes.push("edited");
152        }
153
154        let onkeyup = ctx.link().callback(|e: KeyboardEvent| {
155            let value = e.target_unchecked_into::<HtmlInputElement>().value();
156            EditableHeaderMsg::SetNewValue(value)
157        });
158
159        let onblur = ctx.link().callback(|e: FocusEvent| {
160            let value = e.target_unchecked_into::<HtmlInputElement>().value();
161            EditableHeaderMsg::SetNewValue(value)
162        });
163
164        html! {
165            <div class={classes} onclick={ctx.link().callback(|_| EditableHeaderMsg::OnClick(()))}>
166                if let Some(icon) = ctx.props().icon_type { <TypeIcon ty={icon} /> }
167                <input
168                    ref={self.noderef.clone()}
169                    type="search"
170                    class="sidebar_header_title"
171                    disabled={!ctx.props().editable}
172                    {onblur}
173                    {onkeyup}
174                    value={self.value.clone()}
175                    placeholder={self.placeholder.clone()}
176                />
177            </div>
178        }
179    }
180}
181
182#[derive(Default, Debug, PartialEq, Copy, Clone)]
183pub enum ValueState {
184    #[default]
185    Unedited,
186    Edited,
187}