@using System.Globalization @implements IAsyncDisposable @inject IJSRuntime JS
@if (isOpen && !Disabled) {
@visibleMonth.ToDateTime(TimeOnly.MinValue).ToString("MMMM yyyy", ItalianCulture)
@foreach (var weekday in mondayFirstWeekdays) {
@weekday
}
@foreach (var day in calendarDays) { }
@if (AllowEmpty) {
}
}
@code { private static readonly CultureInfo ItalianCulture = CultureInfo.GetCultureInfo("it-IT"); private static readonly string[] SupportedFormats = ["dd/MM/yyyy", "d/M/yyyy", "dd/M/yyyy", "d/MM/yyyy"]; private static readonly string[] mondayFirstWeekdays = BuildMondayFirstWeekdays(); [Parameter] public DateOnly? Value { get; set; } [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public bool Disabled { get; set; } [Parameter] public bool AllowEmpty { get; set; } [Parameter] public string? InputId { get; set; } [Parameter] public string? TestId { get; set; } private DateOnly? lastValue; private string displayValue = string.Empty; private bool isOpen; private bool outsideClickListenerActive; private DateOnly visibleMonth; private IReadOnlyList calendarDays = []; private ElementReference rootElement; private DotNetObjectReference? dotNetReference; protected override async Task OnAfterRenderAsync(bool firstRender) { if (isOpen && !outsideClickListenerActive) { dotNetReference ??= DotNetObjectReference.Create(this); await JS.InvokeVoidAsync("workTrackerDateInput.registerOutsideClick", rootElement, dotNetReference); outsideClickListenerActive = true; } else if (!isOpen && outsideClickListenerActive) { await JS.InvokeVoidAsync("workTrackerDateInput.unregisterOutsideClick", rootElement); outsideClickListenerActive = false; } await base.OnAfterRenderAsync(firstRender); } protected override void OnParametersSet() { if (visibleMonth == default) { visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today)); calendarDays = BuildCalendarDays(visibleMonth); } if (Value != lastValue) { lastValue = Value; displayValue = FormatValue(Value); visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today)); calendarDays = BuildCalendarDays(visibleMonth); } } private void OpenPopup(FocusEventArgs _) { if (Disabled) { return; } isOpen = true; visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today)); calendarDays = BuildCalendarDays(visibleMonth); } private void TogglePopup() { if (Disabled) { return; } isOpen = !isOpen; if (isOpen) { visibleMonth = ToFirstOfMonth(Value ?? DateOnly.FromDateTime(DateTime.Today)); calendarDays = BuildCalendarDays(visibleMonth); } } private async Task OnTextChangedAsync(ChangeEventArgs e) { displayValue = e.Value?.ToString() ?? string.Empty; if (TryParseInput(displayValue, out var parsedDate)) { await SetValueAsync(parsedDate, closePopup: false); return; } if (AllowEmpty && string.IsNullOrWhiteSpace(displayValue)) { await SetValueAsync(null, closePopup: false); return; } displayValue = FormatValue(Value); } private async Task SelectDateAsync(DateOnly date) { await SetValueAsync(date, closePopup: true); } private async Task ClearAsync() { await SetValueAsync(null, closePopup: true); } private async Task SetValueAsync(DateOnly? date, bool closePopup) { lastValue = date; Value = date; displayValue = FormatValue(date); if (date.HasValue) { visibleMonth = ToFirstOfMonth(date.Value); calendarDays = BuildCalendarDays(visibleMonth); } if (closePopup) { isOpen = false; } await ValueChanged.InvokeAsync(date); } private void ShowPreviousMonth() { visibleMonth = visibleMonth.AddMonths(-1); calendarDays = BuildCalendarDays(visibleMonth); } private void ShowNextMonth() { visibleMonth = visibleMonth.AddMonths(1); calendarDays = BuildCalendarDays(visibleMonth); } private void HandleKeyDown(KeyboardEventArgs e) { if (e.Key == "Escape") { isOpen = false; } } [JSInvokable] public Task ClosePopupFromOutsideClickAsync() { if (!isOpen) { return Task.CompletedTask; } isOpen = false; return InvokeAsync(StateHasChanged); } public async ValueTask DisposeAsync() { if (outsideClickListenerActive) { try { await JS.InvokeVoidAsync("workTrackerDateInput.unregisterOutsideClick", rootElement); } catch (JSDisconnectedException) { } } dotNetReference?.Dispose(); } private string GetInputTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-input" : $"{TestId}-input"; private string GetPopoverTestId() => string.IsNullOrWhiteSpace(TestId) ? "localized-date-popover" : $"{TestId}-popover"; private string GetDayTestId(DateOnly date) => string.IsNullOrWhiteSpace(TestId) ? $"localized-date-day-{date:yyyy-MM-dd}" : $"{TestId}-day-{date:yyyy-MM-dd}"; private static string FormatValue(DateOnly? date) { return date?.ToString("dd/MM/yyyy", ItalianCulture) ?? string.Empty; } private static bool TryParseInput(string? value, out DateOnly date) { foreach (var format in SupportedFormats) { if (DateOnly.TryParseExact(value, format, ItalianCulture, DateTimeStyles.None, out date)) { return true; } } return DateOnly.TryParse(value, ItalianCulture, DateTimeStyles.None, out date); } private static DateOnly ToFirstOfMonth(DateOnly date) => new(date.Year, date.Month, 1); private static IReadOnlyList BuildCalendarDays(DateOnly month) { var firstDayOfMonth = ToFirstOfMonth(month); var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1); var gridStart = firstDayOfMonth.AddDays(-(((int)firstDayOfMonth.DayOfWeek + 6) % 7)); var gridEnd = lastDayOfMonth.AddDays((7 - (((int)lastDayOfMonth.DayOfWeek + 6) % 7) - 1 + 7) % 7); var days = new List(); for (var date = gridStart; date <= gridEnd; date = date.AddDays(1)) { days.Add(new CalendarDayCell(date, date.Month == month.Month && date.Year == month.Year)); } return days; } private static string[] BuildMondayFirstWeekdays() { return Enumerable.Range(0, 7) .Select(index => ItalianCulture.DateTimeFormat.AbbreviatedDayNames[(index + 1) % 7]) .Select(dayName => ItalianCulture.TextInfo.ToTitleCase(dayName)) .ToArray(); } private sealed record CalendarDayCell(DateOnly Date, bool IsCurrentMonth); }