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
14pub use chrono_datepicker_core::config;
16pub use chrono_datepicker_core::dialog_view_type;
17
18pub struct Model<T>
20where
21 T: HasDateConstraints + Default + Clone,
22{
23 selected_date: Option<NaiveDate>,
25
26 dialog_opened: bool,
28
29 viewed_date: NaiveDate,
31
32 dialog_view_type: DialogViewType,
34
35 dialog_position_style: Option<Style>,
37
38 config: PickerConfig<T>,
40}
41
42impl<T: HasDateConstraints + Default + Clone> Model<T> {
43 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
53pub 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
70pub enum Msg {
72 DateSelected(NaiveDate),
73 MonthSelected(MonthNumber),
74 YearSelected(YearNumber),
75 OpenDialog(Option<(String, String)>),
77 CloseDialog,
78 PreviousButtonClicked,
79 NextButtonClicked,
80
81 DialogTitleClicked,
83}
84
85pub 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
148pub 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}