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 super::modal::{ModalLink, SetModalLink};
26use super::style::LocalStyle;
27use crate::components::datetime_column_style::custom::DatetimeStyleCustom;
28use crate::components::datetime_column_style::simple::DatetimeStyleSimple;
29use crate::components::form::select_value_field::SelectValueField;
30use crate::config::*;
31use crate::css;
32use crate::utils::WeakScope;
33
34/// Format-only widget for `datetime` columns. Renders the `date_format`
35/// hierarchy (Simple|Custom + timezone); color/color-mode UI is provided
36/// externally as primitive `Enum` + `Color` schema fields.
37#[derive(Properties, Derivative)]
38#[derivative(Debug)]
39pub struct DatetimeColumnStyleProps {
40    pub enable_time_config: bool,
41    pub config: Option<DatetimeColumnStyleConfig>,
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 && self.config == other.config
63    }
64}
65
66pub enum DatetimeColumnStyleMsg {
67    SimpleDatetimeStyleConfigChanged(SimpleDatetimeStyleConfig),
68    CustomDatetimeStyleConfigChanged(CustomDatetimeStyleConfig),
69    TimezoneChanged(Option<String>),
70}
71
72#[derive(Debug)]
73pub struct DatetimeColumnStyle {
74    config: DatetimeColumnStyleConfig,
75}
76
77impl Component for DatetimeColumnStyle {
78    type Message = DatetimeColumnStyleMsg;
79    type Properties = DatetimeColumnStyleProps;
80
81    fn create(ctx: &Context<Self>) -> Self {
82        ctx.set_modal_link();
83        Self {
84            config: ctx.props().config.clone().unwrap_or_default(),
85        }
86    }
87
88    fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
89        let mut rerender = false;
90        let mut new_config = ctx.props().config.clone().unwrap_or_default();
91        if self.config != new_config {
92            std::mem::swap(&mut self.config, &mut new_config);
93            rerender = true;
94        }
95        if old.enable_time_config != ctx.props().enable_time_config {
96            rerender = true;
97        }
98        rerender
99    }
100
101    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
102        match msg {
103            DatetimeColumnStyleMsg::TimezoneChanged(val) => {
104                if Some(&*USER_TIMEZONE) != val.as_ref() {
105                    *self.config.date_format.time_zone_mut() = val;
106                } else {
107                    *self.config.date_format.time_zone_mut() = None;
108                }
109
110                self.dispatch_config(ctx);
111                true
112            },
113            DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(simple) => {
114                self.config.date_format = DatetimeFormatType::Simple(simple);
115                self.dispatch_config(ctx);
116                true
117            },
118            DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(custom) => {
119                self.config.date_format = DatetimeFormatType::Custom(custom);
120                self.dispatch_config(ctx);
121                true
122            },
123        }
124    }
125
126    fn view(&self, ctx: &Context<Self>) -> Html {
127        html! {
128            <>
129                <LocalStyle href={css!("column-style")} />
130                <div id="column-style-container" class="datetime-column-style-container">
131                    if ctx.props().enable_time_config {
132                        <SelectValueField<String>
133                            label="timezone"
134                            values={ALL_TIMEZONES.with(|x| (*x).clone())}
135                            default_value={(*USER_TIMEZONE).clone()}
136                            on_change={ctx.link().callback(DatetimeColumnStyleMsg::TimezoneChanged)}
137                            current_value={self.config.date_format.time_zone().as_ref().unwrap_or(&*USER_TIMEZONE).clone()}
138                        />
139                    }
140                    if let DatetimeFormatType::Simple(config) = &self.config.date_format {
141                        if ctx.props().enable_time_config {
142                            <div class="row">
143                                <button
144                                    id="datetime_format"
145                                    data-title="Simple"
146                                    data-title-hover="Switch to Custom"
147                                    onclick={ctx.link().callback(|_| DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged(CustomDatetimeStyleConfig::default()))}
148                                />
149                            </div>
150                        }
151                        <DatetimeStyleSimple
152                            enable_time_config={ctx.props().enable_time_config}
153                            on_change={ctx.link().callback(DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged)}
154                            config={config.clone()}
155                        />
156                    } else if let DatetimeFormatType::Custom(config) = &self.config.date_format {
157                        if ctx.props().enable_time_config {
158                            <div class="row">
159                                <button
160                                    id="datetime_format"
161                                    data-title="Custom"
162                                    data-title-hover="Switch to Simple"
163                                    onclick={ctx.link().callback(|_| DatetimeColumnStyleMsg::SimpleDatetimeStyleConfigChanged(SimpleDatetimeStyleConfig::default()))}
164                                />
165                            </div>
166                        }
167                        <DatetimeStyleCustom
168                            enable_time_config={ctx.props().enable_time_config}
169                            on_change={ctx.link().callback(DatetimeColumnStyleMsg::CustomDatetimeStyleConfigChanged)}
170                            config={config.clone()}
171                        />
172                    }
173                </div>
174            </>
175        }
176    }
177}
178
179#[wasm_bindgen]
180extern "C" {
181    #[wasm_bindgen(js_name = supportedValuesOf, js_namespace = Intl)]
182    pub fn supported_values_of(s: &JsValue) -> js_sys::Array;
183}
184
185thread_local! {
186    static ALL_TIMEZONES: LazyLock<Rc<Vec<String>>> = LazyLock::new(|| {
187        Rc::new(
188            supported_values_of(&JsValue::from("timeZone"))
189                .iter()
190                .map(|x| x.as_string().unwrap())
191                .collect(),
192        )
193    });
194}
195
196static USER_TIMEZONE: LazyLock<String> = LazyLock::new(|| {
197    js_sys::Reflect::get(
198        &js_sys::Intl::DateTimeFormat::new(&navigator().languages(), &json!({})).resolved_options(),
199        &JsValue::from("timeZone"),
200    )
201    .unwrap()
202    .as_string()
203    .unwrap()
204});
205
206impl DatetimeColumnStyle {
207    /// When this config has changed, we must signal the wrapper element.
208    fn dispatch_config(&self, ctx: &Context<Self>) {
209        let value = if self.config == DatetimeColumnStyleConfig::default() {
210            serde_json::Map::new()
211        } else {
212            match serde_json::to_value(&self.config) {
213                Ok(serde_json::Value::Object(m)) => m,
214                _ => serde_json::Map::new(),
215            }
216        };
217
218        ctx.props().on_change.emit(ColumnConfigFieldUpdate {
219            keys: ctx.props().keys.clone(),
220            value,
221        });
222    }
223}