perspective_viewer/components/
editable_header.rs1use 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 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}