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