perspective_viewer/components/
editable_header.rs1use std::rc::Rc;
14
15use itertools::Itertools;
16use web_sys::{FocusEvent, HtmlInputElement, KeyboardEvent};
17use yew::prelude::*;
18
19use super::type_icon::TypeIconType;
20use crate::components::type_icon::TypeIcon;
21use crate::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
29 pub value: Option<String>,
31
32 pub initial_value: Option<String>,
34 pub placeholder: Rc<String>,
35
36 #[prop_or_default]
37 pub update_on_input: bool,
38
39 pub metadata: SessionMetadataRc,
41}
42
43impl EditableHeaderProps {
44 fn split_placeholder(&self) -> String {
45 let split = self
46 .placeholder
47 .split_once('\n')
48 .map(|(a, _)| a)
49 .unwrap_or(&*self.placeholder);
50
51 match split.char_indices().nth(25) {
52 None => split.to_string(),
53 Some((idx, _)) => split[..idx].to_owned(),
54 }
55 }
56
57 fn is_valid(&self, value: &Option<String>, placeholder: &str) -> bool {
58 let Some(value) = value else {
59 return true;
60 };
61
62 if value == placeholder || Some(value) == self.initial_value.as_ref() {
63 return true;
64 }
65
66 let metadata = &self.metadata;
67 let Some(table_columns) = metadata.get_table_columns() else {
68 return true;
69 };
70
71 !table_columns
72 .iter()
73 .chain(metadata.get_expression_columns())
74 .chain(metadata.get_window_columns())
75 .contains(value)
76 }
77}
78
79#[function_component(EditableHeader)]
80pub fn editable_header(props: &EditableHeaderProps) -> Html {
81 let noderef = use_node_ref();
82 let placeholder = props.split_placeholder();
83 let edited = props.value != props.initial_value;
84 let valid = props.is_valid(&props.value, &placeholder);
85 let mut classes = classes!("sidebar_header_contents");
86 if props.editable {
87 classes.push("editable");
88 }
89
90 if !valid {
91 classes.push("invalid");
92 }
93
94 if edited {
95 classes.push("edited");
96 }
97
98 let on_value = {
99 let props = props.clone();
100 let placeholder = placeholder.clone();
101 Callback::from(move |value: String| {
102 let value = (!value.is_empty()).then_some(value);
103 let valid = props.is_valid(&value, &placeholder);
104 props.on_change.emit((value, valid));
105 })
106 };
107
108 let onkeyup =
109 on_value.reform(|e: KeyboardEvent| e.target_unchecked_into::<HtmlInputElement>().value());
110 let onblur =
111 on_value.reform(|e: FocusEvent| e.target_unchecked_into::<HtmlInputElement>().value());
112 let oninput = {
113 let update_on_input = props.update_on_input;
114 let on_value = on_value.clone();
115 Callback::from(move |e: InputEvent| {
116 if update_on_input {
117 on_value.emit(e.target_unchecked_into::<HtmlInputElement>().value());
118 }
119 })
120 };
121
122 let onclick = {
123 let noderef = noderef.clone();
124 Callback::from(move |_: MouseEvent| {
125 if let Some(input) = noderef.cast::<HtmlInputElement>() {
126 let _ = input.focus();
127 }
128 })
129 };
130
131 html! {
132 <div class={classes} {onclick}>
133 if let Some(icon) = props.icon_type { <TypeIcon ty={icon} /> }
134 <input
135 ref={noderef}
136 type="search"
137 class="sidebar_header_title"
138 disabled={!props.editable}
139 {onblur}
140 {onkeyup}
141 {oninput}
142 value={props.value.clone()}
143 {placeholder}
144 />
145 </div>
146 }
147}