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