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]
32 pub reset_count: u8,
33
34 pub metadata: SessionMetadataRc,
36
37 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}