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