perspective_viewer/components/
datetime_column_style.rs1mod custom;
14mod simple;
15
16use std::rc::Rc;
17use std::sync::LazyLock;
18
19use derivative::Derivative;
20use perspective_js::json;
21use perspective_js::utils::global::navigator;
22use wasm_bindgen::prelude::*;
23use yew::prelude::*;
24
25use crate::components::datetime_column_style::custom::DatetimeStyleCustom;
26use crate::components::datetime_column_style::simple::DatetimeStyleSimple;
27use crate::config::*;
28use crate::ui::{ModalLink, SelectValueField, SetModalLink};
29use crate::utils::WeakScope;
30
31#[derive(Properties, Derivative)]
35#[derivative(Debug)]
36pub struct DatetimeColumnStyleProps {
37 pub enable_time_config: bool,
38 pub config: Option<DatetimeColumnStyleConfig>,
39
40 pub default_format: Rc<DatetimeFormatType>,
42
43 #[prop_or_default]
44 pub on_change: Callback<ColumnConfigFieldUpdate>,
45
46 #[prop_or_default]
47 pub keys: Vec<String>,
48
49 #[prop_or_default]
50 #[derivative(Debug = "ignore")]
51 weak_link: WeakScope<DatetimeColumnStyle>,
52}
53
54impl ModalLink<DatetimeColumnStyle> for DatetimeColumnStyleProps {
55 fn weak_link(&self) -> &'_ WeakScope<DatetimeColumnStyle> {
56 &self.weak_link
57 }
58}
59
60impl PartialEq for DatetimeColumnStyleProps {
61 fn eq(&self, other: &Self) -> bool {
62 self.enable_time_config == other.enable_time_config
63 && self.config == other.config
64 && self.default_format == other.default_format
65 }
66}
67
68pub enum DatetimeColumnStyleMsg {
69 SimpleDatetimeStyleConfigChanged(SimpleDatetimeStyleConfig),
70 CustomDatetimeStyleConfigChanged(CustomDatetimeStyleConfig),
71 TimezoneChanged(Option<String>),
72}
73
74#[derive(Debug)]
75pub struct DatetimeColumnStyle {
76 config: DatetimeColumnStyleConfig,
77}
78
79impl Component for DatetimeColumnStyle {
80 type Message = DatetimeColumnStyleMsg;
81 type Properties = DatetimeColumnStyleProps;
82
83 fn create(ctx: &Context<Self>) -> Self {
84 ctx.set_modal_link();
85 Self {
86 config: ctx.props().config.clone().unwrap_or_default(),
87 }
88 }
89
90 fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
91 let mut rerender = false;
92 let mut new_config = ctx.props().config.clone().unwrap_or_default();
93 if self.config != new_config {
94 std::mem::swap(&mut self.config, &mut new_config);
95 rerender = true;
96 }
97 if old.enable_time_config != ctx.props().enable_time_config
98 || old.default_format != ctx.props().default_format
99 {
100 rerender = true;
101 }
102 rerender
103 }
104
105 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
106 match msg {
107 DatetimeColumnStyleMsg::TimezoneChanged(val) => {
108 let mut format = self.effective_format(ctx);
109 if Some(&*USER_TIMEZONE) != val.as_ref() {
110 *format.time_zone_mut() = val;
111 } else {
112 *format.time_zone_mut() = None;
113 }
114
115 self.set_format(ctx, format);
116 self.dispatch_config(ctx);
117 true
118 },
119 DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(simple) => {
120 self.set_format(ctx, DatetimeFormatType::Simple(simple));
121 self.dispatch_config(ctx);
122 true
123 },
124 DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(custom) => {
125 self.set_format(ctx, DatetimeFormatType::Custom(custom));
126 self.dispatch_config(ctx);
127 true
128 },
129 }
130 }
131
132 fn view(&self, ctx: &Context<Self>) -> Html {
133 let format = self.effective_format(ctx);
134 let (date_style_default, time_style_default) = match &*ctx.props().default_format {
135 DatetimeFormatType::Simple(simple) => (simple.date_style, simple.time_style),
136 DatetimeFormatType::Custom(_) => {
137 (SimpleDatetimeFormat::Short, SimpleDatetimeFormat::Medium)
138 },
139 };
140
141 let simple_reset = {
142 let default_format = ctx.props().default_format.clone();
143 ctx.link().callback(move |_| {
144 DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(match &*default_format {
145 DatetimeFormatType::Simple(simple) => simple.clone(),
146 DatetimeFormatType::Custom(_) => SimpleDatetimeStyleConfig::default(),
147 })
148 })
149 };
150
151 html! {
152 <>
153 <div id="column-style-container" class="datetime-column-style-container">
154 if ctx.props().enable_time_config {
155 <SelectValueField<String>
156 label="timezone"
157 values={ALL_TIMEZONES.with(|x| (*x).clone())}
158 default_value={(*USER_TIMEZONE).clone()}
159 on_change={ctx.link().callback(DatetimeColumnStyleMsg::TimezoneChanged)}
160 current_value={format.time_zone().as_ref().unwrap_or(&*USER_TIMEZONE).clone()}
161 />
162 }
163 if let DatetimeFormatType::Simple(config) = &format {
164 if ctx.props().enable_time_config {
165 <div class="row">
166 <button
167 id="datetime_format"
168 data-title="Simple"
169 data-title-hover="Switch to Custom"
170 onclick={ctx.link().callback(|_| DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(CustomDatetimeStyleConfig::default()))}
171 />
172 </div>
173 }
174 <DatetimeStyleSimple
175 enable_time_config={ctx.props().enable_time_config}
176 on_change={ctx.link().callback(DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged)}
177 config={config.clone()}
178 {date_style_default}
179 {time_style_default}
180 />
181 } else if let DatetimeFormatType::Custom(config) = &format {
182 if ctx.props().enable_time_config {
183 <div class="row">
184 <button
185 id="datetime_format"
186 data-title="Custom"
187 data-title-hover="Switch to Simple"
188 onclick={simple_reset}
189 />
190 </div>
191 }
192 <DatetimeStyleCustom
193 enable_time_config={ctx.props().enable_time_config}
194 on_change={ctx.link().callback(DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged)}
195 config={config.clone()}
196 />
197 }
198 </div>
199 </>
200 }
201 }
202}
203
204#[wasm_bindgen]
205extern "C" {
206 #[wasm_bindgen(js_name = supportedValuesOf, js_namespace = Intl)]
207 pub fn supported_values_of(s: &JsValue) -> js_sys::Array;
208}
209
210thread_local! {
211 static ALL_TIMEZONES: LazyLock<Rc<Vec<String>>> = LazyLock::new(|| {
212 Rc::new(
213 supported_values_of(&JsValue::from("timeZone"))
214 .iter()
215 .map(|x| x.as_string().unwrap())
216 .collect(),
217 )
218 });
219}
220
221static USER_TIMEZONE: LazyLock<String> = LazyLock::new(|| {
222 js_sys::Reflect::get(
223 &js_sys::Intl::DateTimeFormat::new(&navigator().languages(), &json!({})).resolved_options(),
224 &JsValue::from("timeZone"),
225 )
226 .unwrap()
227 .as_string()
228 .unwrap()
229});
230
231impl DatetimeColumnStyle {
232 fn effective_format(&self, ctx: &Context<Self>) -> DatetimeFormatType {
233 self.config
234 .date_format
235 .clone()
236 .unwrap_or_else(|| (*ctx.props().default_format).clone())
237 }
238
239 fn set_format(&mut self, ctx: &Context<Self>, format: DatetimeFormatType) {
240 self.config.date_format = (format != *ctx.props().default_format).then_some(format);
241 }
242
243 fn dispatch_config(&self, ctx: &Context<Self>) {
245 let value = if self.config == DatetimeColumnStyleConfig::default() {
246 serde_json::Map::new()
247 } else {
248 match serde_json::to_value(&self.config) {
249 Ok(serde_json::Value::Object(m)) => m,
250 _ => serde_json::Map::new(),
251 }
252 };
253
254 ctx.props().on_change.emit(ColumnConfigFieldUpdate {
255 keys: ctx.props().keys.clone(),
256 value,
257 });
258 }
259}