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    /// The active plugin's declared default `date_format`.
42    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    /// When this config has changed, we must signal the wrapper element.
245    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}