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