perspective_viewer/components/
editable_header.rs1use std::rc::Rc;
14
15use derivative::Derivative;
16use itertools::Itertools;
17use web_sys::{FocusEvent, HtmlInputElement, KeyboardEvent};
18use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, html};
19
20use super::type_icon::TypeIconType;
21use crate::components::type_icon::TypeIcon;
22use crate::maybe;
23use crate::session::Session;
24
25#[derive(PartialEq, Properties, Derivative, Clone)]
26#[derivative(Debug)]
27pub struct EditableHeaderProps {
28 pub icon_type: Option<TypeIconType>,
29 pub on_change: Callback<(Option<String>, bool)>,
30 pub editable: bool,
31 pub initial_value: Option<String>,
32 pub placeholder: Rc<String>,
33 #[prop_or_default]
34 pub reset_count: u8,
35 #[derivative(Debug = "ignore")]
36 pub session: Session,
37}
38impl EditableHeaderProps {
39 fn split_placeholder(&self) -> String {
40 let split = self
41 .placeholder
42 .split_once('\n')
43 .map(|(a, _)| a)
44 .unwrap_or(&*self.placeholder);
45 match split.char_indices().nth(25) {
46 None => split.to_string(),
47 Some((idx, _)) => split[..idx].to_owned(),
48 }
49 }
50}
51
52#[derive(Default, Debug, PartialEq, Copy, Clone)]
53pub enum ValueState {
54 #[default]
55 Unedited,
56 Edited,
57}
58
59pub enum EditableHeaderMsg {
60 SetNewValue(String),
61 OnClick(()),
62}
63
64#[derive(Default, Debug)]
65pub struct EditableHeader {
66 noderef: NodeRef,
67 edited: bool,
68 valid: bool,
69 value: Option<String>,
70 placeholder: String,
71}
72impl Component for EditableHeader {
73 type Message = EditableHeaderMsg;
74 type Properties = EditableHeaderProps;
75
76 fn create(ctx: &yew::prelude::Context<Self>) -> Self {
77 Self {
78 value: ctx.props().initial_value.clone(),
79 placeholder: ctx.props().split_placeholder(),
80 valid: true,
81 ..Self::default()
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().session.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}