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