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.
601 lines
16 KiB
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.Get(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);
|
|
}
|