1use chrono::{Datelike, Duration, Month, Months, NaiveDate, Utc};
2use chrono::{Locale, Weekday};
3use chronoutil::shift_months;
4
5use std::convert::TryFrom;
6use std::mem;
7
8use std::str::FromStr;
9use stylist::ast::Sheet;
10use stylist::Style;
11use yew::MouseEvent;
12use yew::{html, Callback, Component, Context, Html, Properties};
13use yew_template::template_html;
14
15pub struct Datepicker {
16 current_month: NaiveDate,
17 locale: Locale,
18 selected_date: Option<NaiveDate>,
19}
20
21#[derive(Properties, PartialEq)]
22pub struct DatepickerProperties {
23 pub on_select: Callback<NaiveDate>,
24 #[prop_or_default]
25 pub locale: Option<Locale>,
26}
27
28impl Default for DatepickerProperties {
29 fn default() -> Self {
30 DatepickerProperties {
31 on_select: Default::default(),
32 locale: Some(Locale::en_US),
33 }
34 }
35}
36
37pub enum DatepickerMessage {
38 CurrentMonthChange(NaiveDate),
39 Select(NaiveDate),
40}
41
42impl Component for Datepicker {
43 type Message = DatepickerMessage;
44 type Properties = DatepickerProperties;
45
46 fn create(ctx: &Context<Self>) -> Self {
47 let current_date = chrono::offset::Local::now()
48 .date_naive()
49 .with_day0(0)
50 .unwrap();
51 let locale = ctx.props().locale.unwrap_or_else(|| Locale::en_US);
52 Datepicker {
53 current_month: current_date,
54 locale,
55 selected_date: None,
56 }
57 }
58
59 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
60 match msg {
61 DatepickerMessage::CurrentMonthChange(date) => self.current_month = date,
62 DatepickerMessage::Select(selected) => {
63 self.selected_date = Some(selected);
64 let _ = ctx.props().on_select.emit(selected);
65 let in_current_mont_selected = selected.year() == self.current_month.year()
66 && selected.month() == self.current_month.month();
67 if !in_current_mont_selected {
68 ctx.link()
69 .send_message(DatepickerMessage::CurrentMonthChange(
70 selected.with_day0(0).unwrap(),
71 ))
72 }
73 }
74 }
75 true
76 }
77
78 fn view(&self, ctx: &Context<Self>) -> Html {
79 const STYLE_FILE: &str = include_str!("styles.css");
80 let sheet = Sheet::from_str(STYLE_FILE).unwrap();
81 let style = Style::new(sheet).unwrap();
82 let days_names = self
83 .current_week()
84 .into_iter()
85 .map(|n: NaiveDate| {
86 html! {
87 <div class="day">{n.format_localized("%a", self.locale).to_string()}</div>
88 }
89 })
90 .collect::<Html>();
91 let columns = html! {
92 <>
93 <div class="day"></div>
94 {days_names}
95 </>
96 };
97
98 let date = self.current_month.clone();
99 let context = ctx.link().clone();
100 let onclick = Callback::from(move |_| {
101 context.send_message(DatepickerMessage::CurrentMonthChange(shift_months(
102 date, -1,
103 )));
104 });
105 let prev = html! {
106 <button class="btn" {onclick} type="button">{"<"}</button>
107 };
108
109 let context = ctx.link().clone();
110 let onclick_next = Callback::from(move |_| {
111 context.send_message(DatepickerMessage::CurrentMonthChange(shift_months(date, 1)));
112 });
113 let next = html! {
114 <button class="btn" onclick={onclick_next} type="button">{">"}</button>
115 };
116
117 let calendarize = calendarize::calendarize_with_offset(self.current_month, 1);
118 let selected_day = self.selected_date;
119 let current_month = self.current_month;
120
121 let previous_month = self
122 .current_month
123 .checked_sub_months(Months::new(1))
124 .unwrap();
125
126 let prev_month = calendarize::calendarize_with_offset(previous_month, 1);
127 let prev_month_last_week = prev_month.last().unwrap();
128
129 let next_month = self
130 .current_month
131 .checked_add_months(Months::new(1))
132 .unwrap();
133 let next_month_calendarize = calendarize::calendarize_with_offset(next_month, 1);
134 let next_month_first_week = next_month_calendarize.first().unwrap();
135
136 let weeks_number = calendarize.len();
137
138 let rows = calendarize
139 .iter()
140 .cloned()
141 .enumerate()
142 .map(|(week_index, n)| {
143 let week_number = week_index + 1;
144 let is_first_week = week_index == 0;
145 let is_last_week = weeks_number == week_number;
146 let cells = n
147 .iter()
148 .cloned()
149 .enumerate()
150 .map(|(day_of_month_index, cl)| {
151 let mut day_class = String::from("day");
152 let context = ctx.link().clone();
153 let current_iter_date: Option<NaiveDate>;
154
155 let number: String;
156 if cl > 0 {
157 number = cl.to_string();
158 current_iter_date = current_month.with_day(cl);
159 } else {
160 if is_first_week {
161 let prev_month_last_week_day =
162 prev_month_last_week.get(day_of_month_index).unwrap();
163 number = prev_month_last_week_day.to_string();
164 day_class.push_str(" day--nc-month");
165 current_iter_date =
166 previous_month.with_day(*prev_month_last_week_day);
167 } else {
168 if is_last_week {
169 let next_month_day =
170 next_month_first_week.get(day_of_month_index).unwrap();
171 number = next_month_day.to_string();
172 day_class.push_str(" day--nc-month");
173 current_iter_date = next_month.with_day(*next_month_day);
174 } else {
175 number = String::new();
176 current_iter_date = None;
177 }
178 }
179 }
180 match selected_day {
181 None => {}
182 Some(s) => match current_iter_date {
183 None => {}
184 Some(sl) => {
185 if s == sl {
186 day_class.push_str(" day--selected");
187 }
188 }
189 },
190 }
191 let onclick: Callback<MouseEvent> =
192 Callback::from(move |event: MouseEvent| {
193 event.prevent_default();
194 match current_iter_date {
195 None => {}
196 Some(s) => {
197 context.send_message(DatepickerMessage::Select(s));
198 }
199 }
200 });
201 html! {
202 <a class={day_class} {onclick} href="#">{number}</a>
203 }
204 })
205 .collect::<Html>();
206 html! {
207 <>
208 <div>{week_number}</div>
209 {cells}
210 </>
211 }
212 })
213 .collect::<Html>();
214 let class = style.get_class_name().to_string();
215 let header = format!(
216 "{} {}",
217 self.current_month_name(),
218 self.current_month.year()
219 );
220 template_html!(
221 "src/templates/default.html",
222 prev = { prev },
223 next = { next },
224 columns = { columns },
225 rows = { rows },
226 class = { class },
227 header = { header }
228 )
229 }
230}
231
232impl Datepicker {
233 fn current_week(&self) -> Vec<NaiveDate> {
234 let current = Utc::now().date_naive();
235 let week = current.week(Weekday::Mon);
236 let first_day = week.first_day();
237 let last_day = week.last_day();
238 let mut result = Vec::new();
239 for day in NaiveDateRange(first_day, last_day) {
240 result.push(day);
241 }
242 result
243 }
244 fn current_month_name(&self) -> String {
245 match self.locale {
246 Locale::ru_RU => {
247 let month = Month::try_from(self.current_month.month() as u8).unwrap();
248 self.russian_month_name(month).to_string()
249 }
250 _ => self
251 .current_month
252 .format_localized("%B", self.locale)
253 .to_string(),
254 }
255 }
256 fn russian_month_name(&self, month: Month) -> &'static str {
257 match month {
258 Month::January => "Январь",
259 Month::February => "Февраль",
260 Month::March => "Март",
261 Month::April => "Апрель",
262 Month::May => "Май",
263 Month::June => "Июнь",
264 Month::July => "Июль",
265 Month::August => "Август",
266 Month::September => "Сентябрь",
267 Month::October => "Октябрь",
268 Month::November => "Ноябрь",
269 Month::December => "Декабрь",
270 }
271 }
272}
273
274struct NaiveDateRange(NaiveDate, NaiveDate);
275
276impl Iterator for NaiveDateRange {
277 type Item = NaiveDate;
278 fn next(&mut self) -> Option<Self::Item> {
279 if self.0 <= self.1 {
280 let next = self.0 + Duration::days(1);
281 Some(mem::replace(&mut self.0, next))
282 } else {
283 None
284 }
285 }
286}