using System.Globalization; using Connected.Annotations; using Connected.Extensions; using Connected.Utilities; using Microsoft.AspNetCore.Components; namespace Connected.Components; public abstract partial class DatePickerBase : Picker { private bool _dateFormatTouched; protected DatePickerBase() : base(new DefaultConverter { Format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern, Culture = CultureInfo.CurrentCulture }) { AdornmentAriaLabel = "Open Date Picker"; } [Inject] protected IScrollManager ScrollManager { get; set; } /// /// Max selectable date. /// [Parameter] [Category(CategoryTypes.FormComponent.Validation)] public DateTime? MaxDate { get; set; } /// /// Min selectable date. /// [Parameter] [Category(CategoryTypes.FormComponent.Validation)] public DateTime? MinDate { get; set; } /// /// First view to show in the MudDatePicker. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public OpenTo OpenTo { get; set; } = OpenTo.Date; /// /// String Format for selected date view /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string DateFormat { get { return (Converter as DefaultConverter)?.Format; } set { if (Converter is DefaultConverter defaultConverter) { defaultConverter.Format = value; _dateFormatTouched = true; } DateFormatChanged(value); } } /// /// Date format value change hook for descendants. /// protected virtual Task DateFormatChanged(string newFormat) { return Task.CompletedTask; } protected override bool SetCulture(CultureInfo value) { if (!base.SetCulture(value)) return false; if (!_dateFormatTouched && Converter is DefaultConverter defaultConverter) defaultConverter.Format = value.DateTimeFormat.ShortDatePattern; return true; } /// /// Defines on which day the week starts. Depends on the value of Culture. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public DayOfWeek? FirstDayOfWeek { get; set; } = null; /// /// The current month of the date picker (two-way bindable). This changes when the user browses through the calender. /// The month is represented as a DateTime which is always the first day of that month. You can also set this to define which month is initially shown. If not set, the current month is shown. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public DateTime? PickerMonth { get => _picker_month; set { if (value == _picker_month) return; _picker_month = value; InvokeAsync(StateHasChanged); PickerMonthChanged.InvokeAsync(value); } } private DateTime? _picker_month; /// /// Fired when the date changes. /// [Parameter] public EventCallback PickerMonthChanged { get; set; } /// /// Sets the amount of time in milliseconds to wait before closing the picker. This helps the user see that the date was selected before the popover disappears. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public int ClosingDelay { get; set; } = 100; /// /// Number of months to display in the calendar /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public int DisplayMonths { get; set; } = 1; /// /// Maximum number of months in one row /// [Parameter] [Category(CategoryTypes.FormComponent.PickerAppearance)] public int? MaxMonthColumns { get; set; } /// /// Start month when opening the picker. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public DateTime? StartMonth { get; set; } /// /// Display week numbers according to the Culture parameter. If no culture is defined, CultureInfo.CurrentCulture will be used. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public bool ShowWeekNumbers { get; set; } /// /// Format of the selected date in the title. By default, this is "ddd, dd MMM" which abbreviates day and month names. /// For instance, display the long names like this "dddd, dd. MMMM". /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public string TitleDateFormat { get; set; } = "ddd, dd MMM"; /// /// If AutoClose is set to true and PickerActions are defined, selecting a day will close the MudDatePicker. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public bool AutoClose { get; set; } /// /// Function to determine whether a date is disabled /// [Parameter] [Category(CategoryTypes.FormComponent.Validation)] public Func IsDateDisabledFunc { get => _isDateDisabledFunc; set { _isDateDisabledFunc = value ?? (_ => false); } } private Func _isDateDisabledFunc = _ => false; /// /// Function to conditionally apply new classes to specific days /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public Func AdditionalDateClassesFunc { get; set; } /// /// Custom previous icon. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerAppearance)] public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft; /// /// Custom next icon. /// [Parameter] [Category(CategoryTypes.FormComponent.PickerAppearance)] public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight; /// /// Set a predefined fix year - no year can be selected /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public int? FixYear { get; set; } /// /// Set a predefined fix month - no month can be selected /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public int? FixMonth { get; set; } /// /// Set a predefined fix day - no day can be selected /// [Parameter] [Category(CategoryTypes.FormComponent.PickerBehavior)] public int? FixDay { get; set; } protected virtual bool IsRange { get; } = false; protected OpenTo CurrentView; protected override void OnPickerOpened() { base.OnPickerOpened(); if (Editable == true && Text != null) { DateTime? a = Converter.ConvertBack(Text); if (a.HasValue) { a = new DateTime(a.Value.Year, a.Value.Month, 1); PickerMonth = a; } } if (OpenTo == OpenTo.Date && FixDay.HasValue) { OpenTo = OpenTo.Month; } if (OpenTo == OpenTo.Date && FixDay.HasValue && FixMonth.HasValue) { OpenTo = OpenTo.Year; } CurrentView = OpenTo; if (CurrentView == OpenTo.Year) _scrollToYearAfterRender = true; } /// /// Get the first of the month to display /// /// protected DateTime GetMonthStart(int month) { var monthStartDate = _picker_month ?? DateTime.Today.StartOfMonth(Culture); // Return the min supported datetime of the calendar when this is year 1 and first month! if (_picker_month.HasValue && _picker_month.Value.Year == 1 && _picker_month.Value.Month == 1) { return Culture.Calendar.MinSupportedDateTime; } return Culture.Calendar.AddMonths(monthStartDate, month); } /// /// Get the last of the month to display /// /// protected DateTime GetMonthEnd(int month) { var monthStartDate = _picker_month ?? DateTime.Today.StartOfMonth(Culture); return Culture.Calendar.AddMonths(monthStartDate, month).EndOfMonth(Culture); } protected DayOfWeek GetFirstDayOfWeek() { if (FirstDayOfWeek.HasValue) return FirstDayOfWeek.Value; return Culture.DateTimeFormat.FirstDayOfWeek; } /// /// Gets the n-th week of the currently displayed month. /// /// offset from _picker_month /// between 0 and 4 /// protected IEnumerable GetWeek(int month, int index) { if (index is < 0 or > 5) throw new ArgumentException("Index must be between 0 and 5"); var month_first = GetMonthStart(month); var week_first = month_first.AddDays(index * 7).StartOfWeek(GetFirstDayOfWeek()); for (var i = 0; i < 7; i++) yield return week_first.AddDays(i); } private string GetWeekNumber(int month, int index) { if (index is < 0 or > 5) throw new ArgumentException("Index must be between 0 and 5"); var month_first = GetMonthStart(month); var week_first = month_first.AddDays(index * 7).StartOfWeek(GetFirstDayOfWeek()); //january 1st if (month_first.Month == 1 && index == 0) { week_first = month_first; } if (week_first.Month != month_first.Month && week_first.AddDays(6).Month != month_first.Month) return ""; return Culture.Calendar.GetWeekOfYear(week_first, Culture.DateTimeFormat.CalendarWeekRule, FirstDayOfWeek ?? Culture.DateTimeFormat.FirstDayOfWeek).ToString(); } protected virtual OpenTo? GetNextView() { OpenTo? nextView = CurrentView switch { OpenTo.Year => !FixMonth.HasValue ? OpenTo.Month : !FixDay.HasValue ? OpenTo.Date : null, OpenTo.Month => !FixDay.HasValue ? OpenTo.Date : null, _ => null, }; return nextView; } protected virtual async void SubmitAndClose() { if (PickerActions == null) { Submit(); if (PickerVariant != PickerVariant.Static) { await Task.Delay(ClosingDelay); Close(false); } } } protected abstract string GetDayClasses(int month, DateTime day); /// /// User clicked on a day /// protected abstract void OnDayClicked(DateTime dateTime); /// /// user clicked on a month /// /// protected virtual void OnMonthSelected(DateTime month) { PickerMonth = month; var nextView = GetNextView(); if (nextView != null) { CurrentView = (OpenTo)nextView; } } /// /// user clicked on a year /// /// protected virtual void OnYearClicked(int year) { var current = GetMonthStart(0); PickerMonth = new DateTime(year, current.Month, 1); var nextView = GetNextView(); if (nextView != null) { CurrentView = (OpenTo)nextView; } } /// /// user clicked on a month /// protected virtual void OnMonthClicked(int month) { CurrentView = OpenTo.Month; _picker_month = _picker_month?.AddMonths(month); StateHasChanged(); } /// /// return Mo, Tu, We, Th, Fr, Sa, Su in the right culture /// /// protected IEnumerable GetAbbreviatedDayNames() { var dayNamesNormal = Culture.DateTimeFormat.AbbreviatedDayNames; var dayNamesShifted = Shift(dayNamesNormal, (int)GetFirstDayOfWeek()); return dayNamesShifted; } /// /// Shift array and cycle around from the end /// private static T[] Shift(T[] array, int positions) { var copy = new T[array.Length]; Array.Copy(array, 0, copy, array.Length - positions, positions); Array.Copy(array, positions, copy, 0, array.Length - positions); return copy; } protected string GetMonthName(int month) { return GetMonthStart(month).ToString(Culture.DateTimeFormat.YearMonthPattern, Culture); } protected abstract string GetTitleDateString(); protected string FormatTitleDate(DateTime? date) { return date?.ToString(TitleDateFormat ?? "ddd, dd MMM", Culture) ?? ""; } protected string GetFormattedYearString() { return GetMonthStart(0).ToString("yyyy", Culture); } private void OnPreviousMonthClick() { // It is impossible to go further into the past after the first year and the first month! if (PickerMonth.HasValue && PickerMonth.Value.Year == 1 && PickerMonth.Value.Month == 1) { return; } PickerMonth = GetMonthStart(0).AddDays(-1).StartOfMonth(Culture); } private void OnNextMonthClick() { PickerMonth = GetMonthEnd(0).AddDays(1); } private void OnPreviousYearClick() { PickerMonth = GetMonthStart(0).AddYears(-1); } private void OnNextYearClick() { PickerMonth = GetMonthStart(0).AddYears(1); } private void OnYearClick() { if (!FixYear.HasValue) { CurrentView = OpenTo.Year; StateHasChanged(); _scrollToYearAfterRender = true; } } /// /// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker. /// private string _componentId = Guid.NewGuid().ToString(); /// /// Is set to true to scroll to the actual year after the next render /// private bool _scrollToYearAfterRender = false; public async void ScrollToYear() { _scrollToYearAfterRender = false; var id = $"{_componentId}{GetMonthStart(0).Year}"; await ScrollManager.ScrollToYearAsync(id); StateHasChanged(); } private int GetMinYear() { if (MinDate.HasValue) return MinDate.Value.Year; return DateTime.Today.Year - 100; } private int GetMaxYear() { if (MaxDate.HasValue) return MaxDate.Value.Year; return DateTime.Today.Year + 100; } private string GetYearClasses(int year) { if (year == GetMonthStart(0).Year) return $"mud-picker-year-selected mud-{Color.ToDescriptionString()}-text"; return null; } private string GetCalendarHeaderClasses(int month) { return new CssBuilder("mud-picker-calendar-header") .AddClass($"mud-picker-calendar-header-{month + 1}") .AddClass($"mud-picker-calendar-header-last", month == DisplayMonths - 1) .Build(); } private Typo GetYearTypo(int year) { if (year == GetMonthStart(0).Year) return Typo.h5; return Typo.subtitle1; } private void OnFormattedDateClick() { // todo: raise an event the user can handle } private IEnumerable GetAllMonths() { var current = GetMonthStart(0); var calendarYear = Culture.Calendar.GetYear(current); var firstOfCalendarYear = Culture.Calendar.ToDateTime(calendarYear, 1, 1, 0, 0, 0, 0); for (var i = 0; i < Culture.Calendar.GetMonthsInYear(calendarYear); i++) yield return Culture.Calendar.AddMonths(firstOfCalendarYear, i); } private string GetAbbreviatedMonthName(DateTime month) { var calendarMonth = Culture.Calendar.GetMonth(month); return Culture.DateTimeFormat.AbbreviatedMonthNames[calendarMonth - 1]; } private string GetMonthName(DateTime month) { var calendarMonth = Culture.Calendar.GetMonth(month); return Culture.DateTimeFormat.MonthNames[calendarMonth - 1]; } private string GetMonthClasses(DateTime month) { if (GetMonthStart(0) == month) return $"mud-picker-month-selected mud-{Color.ToDescriptionString()}-text"; return null; } private Typo GetMonthTypo(DateTime month) { if (GetMonthStart(0) == month) return Typo.h5; return Typo.subtitle1; } protected override void OnInitialized() { base.OnInitialized(); CurrentView = OpenTo; } protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (firstRender) { _picker_month ??= GetCalendarStartOfMonth(); } if (firstRender && CurrentView == OpenTo.Year) { ScrollToYear(); return; } if (_scrollToYearAfterRender) ScrollToYear(); } protected abstract DateTime GetCalendarStartOfMonth(); private int GetCalendarDayOfMonth(DateTime date) { return Culture.Calendar.GetDayOfMonth(date); } /// /// Converts gregorian year into whatever year it is in the provided culture /// /// Gregorian year /// Year according to culture protected abstract int GetCalendarYear(int year); }