patternfly_yew/components/
calendar.rs1use crate::prelude::{
2 Button, ButtonType, ButtonVariant, Icon, InputGroup, InputGroupItem, SimpleSelect, TextInput,
3 TextInputType,
4};
5use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday};
6use num_traits::cast::FromPrimitive;
7use std::str::FromStr;
8
9use yew::{
10 classes, function_component, html, use_callback, use_state_eq, Callback, Html, Properties,
11};
12
13use super::select::SelectItemRenderer;
14
15#[derive(Clone, PartialEq, Properties)]
16pub struct CalendarMonthProperties {
17 #[prop_or(Local::now().date_naive())]
18 pub date: NaiveDate,
19 #[prop_or_default]
20 pub onchange: Callback<NaiveDate>,
21 #[prop_or_default]
22 pub rangestart: Option<NaiveDate>,
23 #[prop_or(Weekday::Mon)]
24 pub weekday_start: Weekday,
25}
26
27fn build_calendar(date: NaiveDate, weekday_start: Weekday) -> Vec<Vec<NaiveDate>> {
32 const ONE_DAY: Days = Days::new(1);
33 let mut ret: Vec<Vec<NaiveDate>> = Vec::new();
34 let mut first_day = date.with_day(1).unwrap();
36 let mut day = first_day.week(weekday_start).first_day();
37 let mut week: Vec<NaiveDate>;
38
39 while first_day.month() == date.month() {
40 week = Vec::new();
41 while first_day.week(weekday_start).days().contains(&day) {
42 week.push(day);
43 day = day + ONE_DAY;
44 }
45
46 first_day = first_day.week(weekday_start).last_day() + ONE_DAY;
47 ret.push(week);
48 }
49
50 ret
51}
52
53#[function_component(CalendarView)]
54pub fn calendar(props: &CalendarMonthProperties) -> Html {
55 let date = use_state_eq(|| props.date);
57 let show_date = use_state_eq(|| props.date);
59 let weeks = build_calendar(*show_date, props.weekday_start);
61 let month = use_state_eq(|| Month::from_u32(props.date.month()).unwrap());
63
64 let callback_month_select = use_callback(
65 (show_date.clone(), month.clone()),
66 move |new_month: MonthLocal, (show_date, month)| {
67 if let Some(d) = NaiveDate::from_ymd_opt(
68 show_date.year(),
69 new_month.0.number_from_month(),
70 show_date.day(),
71 ) {
72 show_date.set(d);
73 month.set(new_month.0);
74 }
75 },
76 );
77
78 let callback_years = use_callback(show_date.clone(), move |new_year: String, show_date| {
79 if let Ok(y) = i32::from_str(&new_year) {
80 if let Some(d) = NaiveDate::from_ymd_opt(y, show_date.month(), show_date.day()) {
81 show_date.set(d)
82 }
83 }
84 });
85
86 let callback_prev = use_callback(
87 (show_date.clone(), month.clone()),
88 move |_, (show_date, month)| {
89 if let Some(d) = show_date.checked_sub_months(Months::new(1)) {
90 show_date.set(d);
91 month.set(month.pred());
92 }
93 },
94 );
95
96 let callback_next = use_callback(
97 (show_date.clone(), month.clone()),
98 move |_, (show_date, month)| {
99 if let Some(d) = show_date.checked_add_months(Months::new(1)) {
100 show_date.set(d);
101 month.set(month.succ());
102 }
103 },
104 );
105
106 html! {
107 <div class="pf-v5-c-calendar-month">
108 <div class="pf-v5-c-calendar-month__header">
109 <div class="pf-v5-c-calendar-month__header-nav-control pf-m-prev-month">
110 <Button
111 variant={ButtonVariant::Plain}
112 aria_label="Previous month"
113 onclick={callback_prev}
114 >
115 {Icon::AngleLeft.as_html()}
116 </Button>
117 </div>
118 <InputGroup>
119 <InputGroupItem>
120 <div class="pf-v5-c-calendar-month__header-month">
121 <SimpleSelect<MonthLocal>
122 entries={vec![
123 MonthLocal(Month::January),
124 MonthLocal(Month::February),
125 MonthLocal(Month::March),
126 MonthLocal(Month::April),
127 MonthLocal(Month::May),
128 MonthLocal(Month::June),
129 MonthLocal(Month::July),
130 MonthLocal(Month::August),
131 MonthLocal(Month::September),
132 MonthLocal(Month::October),
133 MonthLocal(Month::November),
134 MonthLocal(Month::December)
135 ]}
136 selected={MonthLocal(*month)}
137 onselect={callback_month_select}
138 />
139 </div>
140 </InputGroupItem>
141 <InputGroupItem>
142 <div class="pf-v5-c-calendar-month__header-year">
143 <TextInput
144 value={show_date.year().to_string()}
145 r#type={TextInputType::Number}
146 onchange={callback_years}
147 >
148 </TextInput>
149 </div>
150 </InputGroupItem>
151 </InputGroup>
152 <div class="pf-v5-c-calendar-month__header-nav-control pf-m-next-month">
153 <Button
154 variant={ButtonVariant::Plain}
155 aria_label="Next month"
156 onclick={callback_next}
157 >
158 {Icon::AngleRight.as_html()}
159 </Button>
160 </div>
161 </div>
162 <table class="pf-v5-c-calendar-month__calendar">
163 <thead class="pf-v5-c-calendar-month__days">
164 <tr class="pf-v5-c-calendar-month__days-row">
165 {
166 weeks[0].clone().into_iter().map(|day| {
167 html!{
168 <th class="pf-v5-c-calendar-month__day">
169 <span class="pf-v5-screen-reader">{weekday_name(day.weekday())}</span>
170 <span aria-hidden="true">{weekday_name(day.weekday())}</span>
171 </th>
172 }
173 }).collect::<Html>()
174 }
175 </tr>
176 </thead>
177 <tbody class="pf-v5-c-calendar-month__dates">
178 {
179 weeks.into_iter().map(|week| {
180 html!{
181 <>
182 <tr class="pf-v5-c-calendar-month__dates-row">
183 {
184 week.into_iter().map(|day| {
185 let callback_date = {
186 let date = date.clone();
187 let month = month.clone();
188 let show_date = show_date.clone();
189 let onchange = props.onchange.clone();
190 move |day: NaiveDate| {
191 Callback::from(move |_| {
192 let new = NaiveDate::from_ymd_opt(day.year(), day.month(), day.day()).unwrap();
193 date.set(new);
194 show_date.set(new);
195 month.set(Month::from_u32(day.month()).unwrap());
196 onchange.emit(new);
197 })
198 }
199 };
200
201 let mut classes = classes!("pf-v5-c-calendar-month__dates-cell");
202
203 if day == *date {
204 classes.extend(classes!("pf-m-selected"));
205 }
206
207 if day.month() != show_date.month() {
208 classes.extend(classes!("pf-m-adjacent-month"));
209 }
210
211 let before_range = if let Some(range_start) = props.rangestart {
212 if day < range_start {
213 classes.extend(classes!("pf-m-disabled"));
214 }
215
216 if day == range_start {
217 classes.extend(classes!("pf-m-start-range"));
218 classes.extend(classes!("pf-m-selected"));
219 }
220
221 if day >= range_start && day <= *date {
222 classes.extend(classes!("pf-m-in-range"));
223 }
224
225 if day == *date {
226 classes.extend(classes!("pf-m-end-range"));
227 }
228
229 day < range_start
230 } else { false };
231
232 html!{
233 <>
234 <td class={classes}>
235 <Button
236 class="pf-v5-c-calendar-month__date"
237 r#type={ButtonType::Button}
238 variant={if before_range {
239 ButtonVariant::Plain
240 } else {
241 ButtonVariant::None
242 }}
243 onclick={callback_date(day)}
244 disabled={before_range}
245 >
246 {day.day()}
247 </Button>
248 </td>
249 </>
250 }
251 }).collect::<Html>()
252 }
253 </tr>
254 </>
255 }
256 }).collect::<Html>()
257 }
258 </tbody>
259 </table>
260 </div>
261 }
262}
263
264#[derive(Clone, PartialEq, Eq)]
266struct MonthLocal(Month);
267
268impl SelectItemRenderer for MonthLocal {
269 type Item = String;
270
271 #[cfg(feature = "localization")]
272 fn label(&self) -> Self::Item {
273 self.0.localized_name()
274 }
275
276 #[cfg(not(feature = "localization"))]
277 fn label(&self) -> Self::Item {
278 self.0.name().to_string()
279 }
280}
281
282#[cfg(feature = "localization")]
283trait Localized {
284 fn localized_name(&self) -> String;
285}
286
287#[cfg(feature = "localization")]
288impl Localized for Month {
289 fn localized_name(&self) -> String {
291 let date = NaiveDate::from_ymd_opt(2024, self.number_from_month(), 1).unwrap();
293
294 date.format_localized("%B", current_locale()).to_string()
296 }
297}
298
299#[cfg(feature = "localization")]
300fn weekday_name(weekday: Weekday) -> String {
301 localized_weekday_name(weekday)
302}
303
304#[cfg(not(feature = "localization"))]
305fn weekday_name(weekday: Weekday) -> String {
306 weekday.to_string()
307}
308
309#[cfg(feature = "localization")]
310fn localized_weekday_name(weekday: Weekday) -> String {
311 let today = chrono::Local::now().naive_local();
313
314 let days_until_weekday = (7 + weekday.num_days_from_monday() as i64
316 - today.weekday().num_days_from_monday() as i64)
317 % 7;
318
319 let one_day = today + chrono::Duration::days(days_until_weekday);
321
322 one_day
324 .date()
325 .format_localized("%a", current_locale())
326 .to_string()
327}
328
329#[cfg(feature = "localization")]
330static CURRENT_LOCALE_CELL: std::sync::OnceLock<chrono::Locale> = std::sync::OnceLock::new();
331
332#[cfg(feature = "localization")]
333fn current_locale() -> chrono::Locale {
334 CURRENT_LOCALE_CELL
335 .get_or_init(|| {
336 let current_locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
338
339 let current_locale_snake_case = current_locale
341 .as_str()
342 .split('.')
343 .next()
344 .map(|s| s.replace('-', "_"))
345 .unwrap_or("en_US".to_string());
346
347 chrono::Locale::try_from(current_locale_snake_case.as_str())
349 .unwrap_or(chrono::Locale::POSIX)
350 })
351 .to_owned()
352}