Skip to main content

perspective_viewer/components/
datetime_column_style.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13mod 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/// Format-only widget for `datetime` columns. Renders the `date_format`
32/// hierarchy (Simple|Custom + timezone); color/color-mode UI is provided
33/// externally as primitive `Enum` + `Color` schema fields.
34#[derive(Properties, Derivative)]
35#[derivative(Debug)]
36pub struct DatetimeColumnStyleProps {
37    pub enable_time_config: bool,
38    pub config: Option<DatetimeColumnStyleConfig>,
39
40    /// The active plugin's declared default `date_format`.
41    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    /// When this config has changed, we must signal the wrapper element.
244    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}