use crate::prelude::{
Button, ButtonType, ButtonVariant, Icon, InputGroup, InputGroupItem, SimpleSelect, TextInput,
TextInputType,
};
use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday};
use num_traits::cast::FromPrimitive;
use std::str::FromStr;
use yew::{
Callback, Html, Properties, classes, function_component, html, use_callback, use_state_eq,
};
use super::select::SelectItemRenderer;
#[derive(Clone, PartialEq, Properties)]
pub struct CalendarMonthProperties {
#[prop_or(Local::now().date_naive())]
pub date: NaiveDate,
#[prop_or_default]
pub onchange: Callback<NaiveDate>,
#[prop_or_default]
pub rangestart: Option<NaiveDate>,
#[prop_or(Weekday::Mon)]
pub weekday_start: Weekday,
}
fn build_calendar(date: NaiveDate, weekday_start: Weekday) -> Vec<Vec<NaiveDate>> {
const ONE_DAY: Days = Days::new(1);
let mut ret: Vec<Vec<NaiveDate>> = Vec::new();
let mut first_day = date.with_day(1).unwrap();
let mut day = first_day.week(weekday_start).first_day();
let mut week: Vec<NaiveDate>;
while first_day.month() == date.month() {
week = Vec::new();
while first_day.week(weekday_start).days().contains(&day) {
week.push(day);
day = day + ONE_DAY;
}
first_day = first_day.week(weekday_start).last_day() + ONE_DAY;
ret.push(week);
}
ret
}
#[function_component(CalendarView)]
pub fn calendar(props: &CalendarMonthProperties) -> Html {
let date = use_state_eq(|| props.date);
let show_date = use_state_eq(|| props.date);
let weeks = build_calendar(*show_date, props.weekday_start);
let month = use_state_eq(|| Month::from_u32(props.date.month()).unwrap());
let callback_month_select = use_callback(
(show_date.clone(), month.clone()),
move |new_month: MonthLocal, (show_date, month)| {
if let Some(d) = NaiveDate::from_ymd_opt(
show_date.year(),
new_month.0.number_from_month(),
show_date.day(),
) {
show_date.set(d);
month.set(new_month.0);
}
},
);
let callback_years = use_callback(show_date.clone(), move |new_year: String, show_date| {
if let Ok(y) = i32::from_str(&new_year)
&& let Some(d) = NaiveDate::from_ymd_opt(y, show_date.month(), show_date.day())
{
show_date.set(d)
}
});
let callback_prev = use_callback(
(show_date.clone(), month.clone()),
move |_, (show_date, month)| {
if let Some(d) = show_date.checked_sub_months(Months::new(1)) {
show_date.set(d);
month.set(month.pred());
}
},
);
let callback_next = use_callback(
(show_date.clone(), month.clone()),
move |_, (show_date, month)| {
if let Some(d) = show_date.checked_add_months(Months::new(1)) {
show_date.set(d);
month.set(month.succ());
}
},
);
html! {
<div class="pf-v6-c-calendar-month">
<div class="pf-v6-c-calendar-month__header">
<div class="pf-v6-c-calendar-month__header-nav-control pf-m-prev-month">
<Button
variant={ButtonVariant::Plain}
aria_label="Previous month"
onclick={callback_prev}
>
{ Icon::AngleLeft.as_html() }
</Button>
</div>
<InputGroup>
<InputGroupItem>
<div class="pf-v6-c-calendar-month__header-month">
<SimpleSelect<MonthLocal>
entries={vec![
MonthLocal(Month::January),
MonthLocal(Month::February),
MonthLocal(Month::March),
MonthLocal(Month::April),
MonthLocal(Month::May),
MonthLocal(Month::June),
MonthLocal(Month::July),
MonthLocal(Month::August),
MonthLocal(Month::September),
MonthLocal(Month::October),
MonthLocal(Month::November),
MonthLocal(Month::December)
]}
selected={MonthLocal(*month)}
onselect={callback_month_select}
/>
</div>
</InputGroupItem>
<InputGroupItem>
<div class="pf-v6-c-calendar-month__header-year">
<TextInput
value={show_date.year().to_string()}
r#type={TextInputType::Number}
onchange={callback_years}
/>
</div>
</InputGroupItem>
</InputGroup>
<div class="pf-v6-c-calendar-month__header-nav-control pf-m-next-month">
<Button
variant={ButtonVariant::Plain}
aria_label="Next month"
onclick={callback_next}
>
{ Icon::AngleRight.as_html() }
</Button>
</div>
</div>
<table class="pf-v6-c-calendar-month__calendar">
<thead class="pf-v6-c-calendar-month__days">
<tr class="pf-v6-c-calendar-month__days-row">
{ weeks[0].clone().into_iter().map(|day| {
html!{
<th class="pf-v6-c-calendar-month__day">
<span class="pf-v6-screen-reader">{weekday_name(day.weekday())}</span>
<span aria-hidden="true">{weekday_name(day.weekday())}</span>
</th>
}
}).collect::<Html>() }
</tr>
</thead>
<tbody class="pf-v6-c-calendar-month__dates">
{ weeks.into_iter().map(|week| {
html!{
<>
<tr class="pf-v6-c-calendar-month__dates-row">
{
week.into_iter().map(|day| {
let callback_date = {
let date = date.clone();
let month = month.clone();
let show_date = show_date.clone();
let onchange = props.onchange.clone();
move |day: NaiveDate| {
Callback::from(move |_| {
let new = NaiveDate::from_ymd_opt(day.year(), day.month(), day.day()).unwrap();
date.set(new);
show_date.set(new);
month.set(Month::from_u32(day.month()).unwrap());
onchange.emit(new);
})
}
};
let mut classes = classes!("pf-v6-c-calendar-month__dates-cell");
if day == *date {
classes.extend(classes!("pf-m-selected"));
}
if day.month() != show_date.month() {
classes.extend(classes!("pf-m-adjacent-month"));
}
let before_range = if let Some(range_start) = props.rangestart {
if day < range_start {
classes.extend(classes!("pf-m-disabled"));
}
if day == range_start {
classes.extend(classes!("pf-m-start-range"));
classes.extend(classes!("pf-m-selected"));
}
if day >= range_start && day <= *date {
classes.extend(classes!("pf-m-in-range"));
}
if day == *date {
classes.extend(classes!("pf-m-end-range"));
}
day < range_start
} else { false };
html!{
<>
<td class={classes}>
<Button
class="pf-v6-c-calendar-month__date"
r#type={ButtonType::Button}
variant={if before_range {
ButtonVariant::Plain
} else {
ButtonVariant::None
}}
onclick={callback_date(day)}
disabled={before_range}
>
{day.day()}
</Button>
</td>
</>
}
}).collect::<Html>()
}
</tr>
</>
}
}).collect::<Html>() }
</tbody>
</table>
</div>
}
}
#[derive(Clone, PartialEq, Eq)]
struct MonthLocal(Month);
impl SelectItemRenderer for MonthLocal {
type Item = String;
#[cfg(feature = "localization")]
fn label(&self) -> Self::Item {
self.0.localized_name()
}
#[cfg(not(feature = "localization"))]
fn label(&self) -> Self::Item {
self.0.name().to_string()
}
}
#[cfg(feature = "localization")]
trait Localized {
fn localized_name(&self) -> String;
}
#[cfg(feature = "localization")]
impl Localized for Month {
fn localized_name(&self) -> String {
let date = NaiveDate::from_ymd_opt(2024, self.number_from_month(), 1).unwrap();
date.format_localized("%B", current_locale()).to_string()
}
}
#[cfg(feature = "localization")]
fn weekday_name(weekday: Weekday) -> String {
localized_weekday_name(weekday)
}
#[cfg(not(feature = "localization"))]
fn weekday_name(weekday: Weekday) -> String {
weekday.to_string()
}
#[cfg(feature = "localization")]
fn localized_weekday_name(weekday: Weekday) -> String {
let today = chrono::Local::now().naive_local();
let days_until_weekday = (7 + weekday.num_days_from_monday() as i64
- today.weekday().num_days_from_monday() as i64)
% 7;
let one_day = today + chrono::Duration::days(days_until_weekday);
one_day
.date()
.format_localized("%a", current_locale())
.to_string()
}
#[cfg(feature = "localization")]
static CURRENT_LOCALE_CELL: std::sync::OnceLock<chrono::Locale> = std::sync::OnceLock::new();
#[cfg(feature = "localization")]
fn current_locale() -> chrono::Locale {
CURRENT_LOCALE_CELL
.get_or_init(|| {
let current_locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
let current_locale_snake_case = current_locale
.as_str()
.split('.')
.next()
.map(|s| s.replace('-', "_"))
.unwrap_or("en_US".to_string());
chrono::Locale::try_from(current_locale_snake_case.as_str())
.unwrap_or(chrono::Locale::POSIX)
})
.to_owned()
}