seed_datepicker/
lib.rs

1#![forbid(unsafe_code)]
2
3use chrono::{prelude::*, Duration};
4use chrono_datepicker_core::{
5    config::{date_constraints::HasDateConstraints, PickerConfig},
6    dialog_view_type::DialogViewType,
7    style_names::*,
8    utils::{create_dialog_title_text, should_display_next_button, should_display_previous_button},
9    viewed_date::{year_group_range, MonthNumber, ViewedDate, YearNumber},
10};
11use num_traits::FromPrimitive;
12use seed::{prelude::*, *};
13
14/// reexport only necessary things for using the seed-datepicker
15pub use chrono_datepicker_core::config;
16pub use chrono_datepicker_core::dialog_view_type;
17
18/// `Model` describes the current datepicker state.
19pub struct Model<T>
20where
21    T: HasDateConstraints + Default + Clone,
22{
23    /// value of the date that is selected
24    selected_date: Option<NaiveDate>,
25
26    /// whether the dialog is shown
27    dialog_opened: bool,
28
29    /// viewed date
30    viewed_date: NaiveDate,
31
32    /// dialog type
33    dialog_view_type: DialogViewType,
34
35    /// dialog position style, describing the position of the dialog
36    dialog_position_style: Option<Style>,
37
38    /// configuration of the picker, should be passed in during init and not modified later
39    config: PickerConfig<T>,
40}
41
42impl<T: HasDateConstraints + Default + Clone> Model<T> {
43    /// selected value of the datepicker
44    pub fn selected_date(&self) -> &Option<NaiveDate> {
45        &self.selected_date
46    }
47
48    pub fn config(&self) -> &PickerConfig<T> {
49        &self.config
50    }
51}
52
53/// `init` describes what should happen when your app started.
54pub fn init<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
55    _: Url,
56    _: &mut impl Orders<Ms>,
57    config: PickerConfig<T>,
58    _to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
59) -> Model<T> {
60    Model {
61        selected_date: *config.initial_date(),
62        dialog_opened: *config.initially_opened(),
63        viewed_date: config.guess_allowed_year_month(),
64        dialog_view_type: *config.initial_view_type(),
65        dialog_position_style: None,
66        config,
67    }
68}
69
70/// `Msg` describes the different events you can modify state with.
71pub enum Msg {
72    DateSelected(NaiveDate),
73    MonthSelected(MonthNumber),
74    YearSelected(YearNumber),
75    /// open the dialog, optionally at the given (left, top) position
76    OpenDialog(Option<(String, String)>),
77    CloseDialog,
78    PreviousButtonClicked,
79    NextButtonClicked,
80
81    /// clicks on the dialog title change the `DialogViewType`
82    DialogTitleClicked,
83}
84
85/// `update` describes how to handle each `Msg`.
86pub fn update<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
87    msg: Msg,
88    model: &mut Model<T>,
89    orders: &mut impl Orders<Ms>,
90    on_change: Ms,
91    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
92) {
93    match msg {
94        Msg::DateSelected(new_date) => {
95            model.selected_date = Some(new_date);
96            model.viewed_date = new_date;
97            orders.send_msg(to_msg(Msg::CloseDialog));
98            orders.send_msg(on_change);
99        }
100        Msg::MonthSelected(new_month) => {
101            model.viewed_date = NaiveDate::from_ymd(model.viewed_date.year(), new_month, 1);
102            if model.config.selection_type() == &DialogViewType::Months {
103                orders.send_msg(to_msg(Msg::DateSelected(model.viewed_date)));
104            } else {
105                model.dialog_view_type = DialogViewType::Days;
106            }
107        }
108        Msg::YearSelected(new_year) => {
109            model.viewed_date = NaiveDate::from_ymd(new_year, 1, 1);
110            if model.config.selection_type() == &DialogViewType::Years {
111                orders.send_msg(to_msg(Msg::DateSelected(model.viewed_date)));
112            } else {
113                model.dialog_view_type = DialogViewType::Months;
114            }
115        }
116        Msg::OpenDialog(position) => {
117            model.dialog_opened = true;
118            if let Some((left, top)) = position {
119                model.dialog_position_style = Some(style! {
120                    St::Left => left,
121                    St::Top => top,
122                });
123            }
124        }
125        Msg::CloseDialog => model.dialog_opened = false,
126        Msg::PreviousButtonClicked => {
127            model.viewed_date = match model.dialog_view_type {
128                DialogViewType::Days => model.viewed_date.previous_month(),
129                DialogViewType::Months => model.viewed_date.previous_year(),
130                DialogViewType::Years => model.viewed_date.previous_year_group(),
131            };
132        }
133        Msg::NextButtonClicked => {
134            model.viewed_date = match model.dialog_view_type {
135                DialogViewType::Days => model.viewed_date.next_month(),
136                DialogViewType::Months => model.viewed_date.next_year(),
137                DialogViewType::Years => model.viewed_date.next_year_group(),
138            };
139        }
140        Msg::DialogTitleClicked => {
141            if let Some(new_dialog_type) = model.dialog_view_type.larger_type() {
142                model.dialog_view_type = new_dialog_type;
143            }
144        }
145    };
146}
147
148/// `view` describes what to display.
149pub fn view<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
150    model: &Model<T>,
151    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
152) -> Node<Ms> {
153    IF!(model.dialog_opened => div![
154        C![DATEPICKER_ROOT],
155        model.dialog_position_style.as_ref(),
156        view_dialog_header(model, to_msg.clone()),
157        view_dialog_body(model, to_msg),
158    ])
159    .unwrap_or(empty![])
160}
161
162fn view_dialog_header<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
163    model: &Model<T>,
164    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
165) -> Node<Ms> {
166    div![
167        C![HEADER],
168        button![
169            C![BUTTON, PREVIOUS],
170            style! {
171                St::Visibility => if should_display_previous_button(&model.dialog_view_type, &model.viewed_date, &model.config) { "visible" } else {"hidden"},
172            },
173            "«",
174            ev(Ev::Click, {
175                let to_msg = to_msg.clone();
176                |_| to_msg(Msg::PreviousButtonClicked)
177            }),
178        ],
179        span![
180            C![TITLE],
181            attrs! {
182                At::from("role") => "heading",
183            },
184            create_dialog_title_text(
185                &model.dialog_view_type,
186                &model.viewed_date,
187                model.config.month_title_format()
188            ),
189            ev(Ev::Click, {
190                let to_msg = to_msg.clone();
191                |_| to_msg(Msg::DialogTitleClicked)
192            }),
193        ],
194        button![
195            C![BUTTON, NEXT],
196            style! {
197                St::Visibility => if should_display_next_button(&model.dialog_view_type, &model.viewed_date, &model.config) { "visible" } else { "hidden" },
198            },
199            "»",
200            ev(Ev::Click, {
201                let to_msg = to_msg.clone();
202                |_| to_msg(Msg::NextButtonClicked)
203            }),
204        ],
205        button![
206            C![BUTTON, CLOSE],
207            "x",
208            ev(Ev::Click, |_| to_msg(Msg::CloseDialog)),
209        ],
210    ]
211}
212
213fn view_dialog_body<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
214    model: &Model<T>,
215    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
216) -> Node<Ms> {
217    match model.dialog_view_type {
218        DialogViewType::Days => view_dialog_days(model, to_msg),
219        DialogViewType::Months => view_dialog_months(model, to_msg),
220        DialogViewType::Years => view_dialog_years(model, to_msg),
221    }
222}
223
224fn view_dialog_years<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
225    model: &Model<T>,
226    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
227) -> Node<Ms> {
228    let years: Vec<Node<Ms>> = year_group_range(model.viewed_date.year())
229        .map(|year| view_year_cell(year, model, to_msg.clone()))
230        .collect();
231
232    div![
233        C![BODY],
234        style! {
235            St::GridTemplateColumns => "1fr ".repeat(4),
236        },
237        years,
238    ]
239}
240
241fn view_year_cell<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
242    year: i32,
243    model: &Model<T>,
244    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
245) -> Node<Ms> {
246    let is_year_forbidden = model.config.is_year_forbidden(year);
247    let is_year_selected = model
248        .selected_date
249        .map_or(false, |optval| optval.year() == year);
250
251    span![
252        year.to_string(),
253        C![
254            if is_year_forbidden {
255                UNAVAILABLE
256            } else {
257                SELECTABLE
258            },
259            IF!(is_year_selected => SELECTED),
260        ],
261        attrs! {
262            At::from("role") => "gridcell",
263            At::AriaSelected => is_year_selected.as_at_value(),
264        },
265        IF!(!is_year_forbidden => ev(Ev::Click, move |_| to_msg(Msg::YearSelected(year)))),
266    ]
267}
268
269fn view_dialog_months<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
270    model: &Model<T>,
271    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
272) -> Node<Ms> {
273    let months: Vec<Node<Ms>> = (1..=12u32)
274        .map(|month| {
275            view_month_cell(
276                NaiveDate::from_ymd(model.viewed_date.year(), month, 1),
277                model,
278                to_msg.clone(),
279            )
280        })
281        .collect();
282
283    div![
284        C![BODY],
285        style! {
286            St::GridTemplateColumns => "1fr ".repeat(3),
287        },
288        months
289    ]
290}
291
292fn view_month_cell<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
293    month_to_display: NaiveDate,
294    model: &Model<T>,
295    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
296) -> Node<Ms> {
297    let is_month_forbidden = model.config.is_month_forbidden(&month_to_display);
298    let is_month_selected = model.selected_date.map_or(false, |optval| {
299        month_to_display.contains(&model.dialog_view_type, &optval)
300    });
301
302    span![
303        Month::from_u32(month_to_display.month()).unwrap().name(),
304        C![
305            if is_month_forbidden {
306                UNAVAILABLE
307            } else {
308                SELECTABLE
309            },
310            IF!(is_month_selected => SELECTED),
311        ],
312        attrs! {
313            At::from("role") => "gridcell",
314            At::AriaSelected => is_month_selected.as_at_value(),
315        },
316        IF!(!is_month_forbidden => ev(Ev::Click, move |_| to_msg(Msg::MonthSelected(month_to_display.month())))),
317    ]
318}
319
320fn view_dialog_days<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
321    model: &Model<T>,
322    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
323) -> Node<Ms> {
324    let first_day_of_month = model.viewed_date.first_day_of_month();
325    let first_day_of_calendar = first_day_of_month
326        - Duration::days(first_day_of_month.weekday().num_days_from_monday().into());
327
328    let day_nodes: Vec<Node<Ms>> = first_day_of_calendar
329        .iter_days()
330        .take(7 * 6)
331        .map(|day| view_day_cell(day, model, to_msg.clone()))
332        .collect();
333
334    div![
335        C!["body"],
336        style! {
337            St::GridTemplateColumns => "1fr ".repeat(7),
338        },
339        view_weekday_name(Weekday::Mon),
340        view_weekday_name(Weekday::Tue),
341        view_weekday_name(Weekday::Wed),
342        view_weekday_name(Weekday::Thu),
343        view_weekday_name(Weekday::Fri),
344        view_weekday_name(Weekday::Sat),
345        view_weekday_name(Weekday::Sun),
346        day_nodes,
347    ]
348}
349
350fn view_weekday_name<Ms: 'static>(day: Weekday) -> Node<Ms> {
351    span![
352        day.to_string(),
353        C![GRID_HEADER],
354        attrs! {
355            At::from("role") => "columnheader",
356        },
357    ]
358}
359
360fn view_day_cell<Ms: 'static, T: HasDateConstraints + std::default::Default + Clone>(
361    date: NaiveDate,
362    model: &Model<T>,
363    to_msg: impl FnOnce(Msg) -> Ms + Clone + 'static,
364) -> Node<Ms> {
365    let is_day_forbidden = model.config.is_day_forbidden(&date);
366    let is_date_selected = model.selected_date.map_or(false, |optval| optval == date);
367
368    span![
369        date.day().to_string(),
370        C![
371            if is_day_forbidden {
372                UNAVAILABLE
373            } else {
374                SELECTABLE
375            },
376            IF!(date.month() != model.viewed_date.month() => OTHER_MONTH),
377            IF!(is_date_selected => SELECTED),
378        ],
379        attrs! {
380            At::from("role") => "gridcell",
381            At::AriaSelected => is_date_selected.as_at_value(),
382        },
383        IF!(!is_day_forbidden => ev(Ev::Click, move |_| to_msg(Msg::DateSelected(date)))),
384    ]
385}