You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Components/DatePicker/DatePickerBase.razor.cs

601 lines
16 KiB

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<DateTime?>
{
private bool _dateFormatTouched;
protected DatePickerBase() : base(new DefaultConverter<DateTime?>
{
Format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern,
Culture = CultureInfo.CurrentCulture
})
{
AdornmentAriaLabel = "Open Date Picker";
}
[Inject] protected IScrollManager ScrollManager { get; set; }
/// <summary>
/// Max selectable date.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public DateTime? MaxDate { get; set; }
/// <summary>
/// Min selectable date.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public DateTime? MinDate { get; set; }
/// <summary>
/// First view to show in the MudDatePicker.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public OpenTo OpenTo { get; set; } = OpenTo.Date;
/// <summary>
/// String Format for selected date view
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Behavior)]
public string DateFormat
{
get
{
return (Converter as DefaultConverter<DateTime?>)?.Format;
}
set
{
if (Converter is DefaultConverter<DateTime?> defaultConverter)
{
defaultConverter.Format = value;
_dateFormatTouched = true;
}
DateFormatChanged(value);
}
}
/// <summary>
/// Date format value change hook for descendants.
/// </summary>
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<DateTime?> defaultConverter)
defaultConverter.Format = value.DateTimeFormat.ShortDatePattern;
return true;
}
/// <summary>
/// Defines on which day the week starts. Depends on the value of Culture.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public DayOfWeek? FirstDayOfWeek { get; set; } = null;
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>
/// Fired when the date changes.
/// </summary>
[Parameter] public EventCallback<DateTime?> PickerMonthChanged { get; set; }
/// <summary>
/// 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.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public int ClosingDelay { get; set; } = 100;
/// <summary>
/// Number of months to display in the calendar
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public int DisplayMonths { get; set; } = 1;
/// <summary>
/// Maximum number of months in one row
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerAppearance)]
public int? MaxMonthColumns { get; set; }
/// <summary>
/// Start month when opening the picker.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public DateTime? StartMonth { get; set; }
/// <summary>
/// Display week numbers according to the Culture parameter. If no culture is defined, CultureInfo.CurrentCulture will be used.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public bool ShowWeekNumbers { get; set; }
/// <summary>
/// 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".
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public string TitleDateFormat { get; set; } = "ddd, dd MMM";
/// <summary>
/// If AutoClose is set to true and PickerActions are defined, selecting a day will close the MudDatePicker.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public bool AutoClose { get; set; }
/// <summary>
/// Function to determine whether a date is disabled
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Validation)]
public Func<DateTime, bool> IsDateDisabledFunc
{
get => _isDateDisabledFunc;
set
{
_isDateDisabledFunc = value ?? (_ => false);
}
}
private Func<DateTime, bool> _isDateDisabledFunc = _ => false;
/// <summary>
/// Function to conditionally apply new classes to specific days
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.Appearance)]
public Func<DateTime, string> AdditionalDateClassesFunc { get; set; }
/// <summary>
/// Custom previous icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerAppearance)]
public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft;
/// <summary>
/// Custom next icon.
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerAppearance)]
public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight;
/// <summary>
/// Set a predefined fix year - no year can be selected
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public int? FixYear { get; set; }
/// <summary>
/// Set a predefined fix month - no month can be selected
/// </summary>
[Parameter]
[Category(CategoryTypes.FormComponent.PickerBehavior)]
public int? FixMonth { get; set; }
/// <summary>
/// Set a predefined fix day - no day can be selected
/// </summary>
[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;
}
/// <summary>
/// Get the first of the month to display
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Get the last of the month to display
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Gets the n-th week of the currently displayed month.
/// </summary>
/// <param name="month">offset from _picker_month</param>
/// <param name="index">between 0 and 4</param>
/// <returns></returns>
protected IEnumerable<DateTime> 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);
/// <summary>
/// User clicked on a day
/// </summary>
protected abstract void OnDayClicked(DateTime dateTime);
/// <summary>
/// user clicked on a month
/// </summary>
/// <param name="month"></param>
protected virtual void OnMonthSelected(DateTime month)
{
PickerMonth = month;
var nextView = GetNextView();
if (nextView != null)
{
CurrentView = (OpenTo)nextView;
}
}
/// <summary>
/// user clicked on a year
/// </summary>
/// <param name="year"></param>
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;
}
}
/// <summary>
/// user clicked on a month
/// </summary>
protected virtual void OnMonthClicked(int month)
{
CurrentView = OpenTo.Month;
_picker_month = _picker_month?.AddMonths(month);
StateHasChanged();
}
/// <summary>
/// return Mo, Tu, We, Th, Fr, Sa, Su in the right culture
/// </summary>
/// <returns></returns>
protected IEnumerable<string> GetAbbreviatedDayNames()
{
var dayNamesNormal = Culture.DateTimeFormat.AbbreviatedDayNames;
var dayNamesShifted = Shift(dayNamesNormal, (int)GetFirstDayOfWeek());
return dayNamesShifted;
}
/// <summary>
/// Shift array and cycle around from the end
/// </summary>
private static T[] Shift<T>(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;
}
}
/// <summary>
/// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker.
/// </summary>
private string _componentId = Guid.NewGuid().ToString();
/// <summary>
/// Is set to true to scroll to the actual year after the next render
/// </summary>
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<DateTime> 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);
}
/// <summary>
/// Converts gregorian year into whatever year it is in the provided culture
/// </summary>
/// <param name="year">Gregorian year</param>
/// <returns>Year according to culture</returns>
protected abstract int GetCalendarYear(int year);
}