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.
677 lines
15 KiB
677 lines
15 KiB
using System.Diagnostics.CodeAnalysis;
|
|
using System.Dynamic;
|
|
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
using Connected.Annotations;
|
|
using Connected.Extensions;
|
|
using Connected.Utilities;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
|
|
namespace Connected.Components;
|
|
|
|
public partial class TimePicker : Picker<TimeSpan?>
|
|
{
|
|
private const string format24Hours = "HH:mm";
|
|
private const string format12Hours = "hh:mm tt";
|
|
|
|
|
|
public TimePicker() : base()
|
|
{
|
|
_timeFormat = format24Hours;
|
|
|
|
Converter = new LambdaConverter<TimeSpan?, string>((e) => OnSet(e), (e) => OnGet(e));
|
|
|
|
AdornmentIcon = Icons.Material.Filled.AccessTime;
|
|
AdornmentAriaLabel = "Open Time Picker";
|
|
}
|
|
|
|
private string OnSet(TimeSpan? timespan)
|
|
{
|
|
if (timespan == null)
|
|
return string.Empty;
|
|
|
|
var time = DateTime.Today.Add(timespan.Value);
|
|
|
|
return time.ToString(_timeFormat, Culture);
|
|
}
|
|
|
|
private TimeSpan? OnGet(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return null;
|
|
|
|
if (DateTime.TryParseExact(value, _timeFormat, Culture, DateTimeStyles.None, out var time))
|
|
{
|
|
return time.TimeOfDay;
|
|
}
|
|
else
|
|
{
|
|
var m = Regex.Match(value, "AM|PM", RegexOptions.IgnoreCase);
|
|
if (m.Success)
|
|
{
|
|
return DateTime.ParseExact(value, format12Hours, CultureInfo.InvariantCulture).TimeOfDay;
|
|
}
|
|
else
|
|
{
|
|
return DateTime.ParseExact(value, format24Hours, CultureInfo.InvariantCulture).TimeOfDay;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool _amPm = false;
|
|
private OpenTo _currentView;
|
|
private string _timeFormat = string.Empty;
|
|
|
|
internal TimeSpan? TimeIntermediate { get; private set; }
|
|
|
|
/// <summary>
|
|
/// First view to show in the MudDatePicker.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.PickerBehavior)]
|
|
public OpenTo OpenTo { get; set; } = OpenTo.Hours;
|
|
|
|
/// <summary>
|
|
/// Choose the edition mode. By default, you can edit hours and minutes.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.PickerBehavior)]
|
|
public TimeEditMode TimeEditMode { get; set; } = TimeEditMode.Normal;
|
|
|
|
/// <summary>
|
|
/// Sets the amount of time in milliseconds to wait before closing the picker. This helps the user see that the time was selected before the popover disappears.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.PickerBehavior)]
|
|
public int ClosingDelay { get; set; } = 200;
|
|
|
|
/// <summary>
|
|
/// If AutoClose is set to true and PickerActions are defined, the hour and the minutes can be defined without any action.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.PickerBehavior)]
|
|
public bool AutoClose { get; set; }
|
|
|
|
/// <summary>
|
|
/// If true, sets 12 hour selection clock.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.Behavior)]
|
|
public bool AmPm
|
|
{
|
|
get => _amPm;
|
|
set
|
|
{
|
|
if (value == _amPm)
|
|
return;
|
|
|
|
_amPm = value;
|
|
|
|
_timeFormat = AmPm ? format12Hours : format24Hours;
|
|
|
|
Modified = true;
|
|
SetTextAsync(Converter.Convert(_value), false).AndForget();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// String Format for selected time view
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.Behavior)]
|
|
public string TimeFormat
|
|
{
|
|
get => _timeFormat;
|
|
set
|
|
{
|
|
if (_timeFormat == value)
|
|
return;
|
|
|
|
_timeFormat = value;
|
|
|
|
Modified = true;
|
|
SetTextAsync(Converter.Convert(_value), false).AndForget();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The currently selected time (two-way bindable). If null, then nothing was selected.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Category(CategoryTypes.FormComponent.Data)]
|
|
public TimeSpan? Time
|
|
{
|
|
get => _value;
|
|
set => SetTimeAsync(value, true).AndForget();
|
|
}
|
|
|
|
protected async Task SetTimeAsync(TimeSpan? time, bool updateValue)
|
|
{
|
|
if (_value != time)
|
|
{
|
|
TimeIntermediate = time;
|
|
_value = time;
|
|
if (updateValue)
|
|
await SetTextAsync(Converter.Convert(_value), false);
|
|
UpdateTimeSetFromTime();
|
|
await TimeChanged.InvokeAsync(_value);
|
|
BeginValidate();
|
|
FieldChanged(_value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fired when the date changes.
|
|
/// </summary>
|
|
[Parameter] public EventCallback<TimeSpan?> TimeChanged { get; set; }
|
|
|
|
protected override Task StringValueChanged(string value)
|
|
{
|
|
Modified = true;
|
|
// Update the time property (without updating back the Value property)
|
|
return SetTimeAsync(Converter.ConvertBack(value), false);
|
|
}
|
|
|
|
//The last line cannot be tested
|
|
[ExcludeFromCodeCoverage]
|
|
protected override void OnPickerOpened()
|
|
{
|
|
base.OnPickerOpened();
|
|
_currentView = TimeEditMode switch
|
|
{
|
|
TimeEditMode.Normal => OpenTo,
|
|
TimeEditMode.OnlyHours => OpenTo.Hours,
|
|
TimeEditMode.OnlyMinutes => OpenTo.Minutes,
|
|
_ => _currentView
|
|
};
|
|
}
|
|
|
|
protected internal override void Submit()
|
|
{
|
|
if (ReadOnly)
|
|
return;
|
|
Time = TimeIntermediate;
|
|
}
|
|
|
|
public override async void Clear(bool close = true)
|
|
{
|
|
TimeIntermediate = null;
|
|
await SetTimeAsync(null, true);
|
|
|
|
if (AutoClose == true)
|
|
{
|
|
Close(false);
|
|
}
|
|
}
|
|
|
|
private string GetHourString()
|
|
{
|
|
if (TimeIntermediate == null)
|
|
return "--";
|
|
var h = AmPm ? TimeIntermediate.Value.ToAmPmHour() : TimeIntermediate.Value.Hours;
|
|
return Math.Min(23, Math.Max(0, h)).ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private string GetMinuteString()
|
|
{
|
|
if (TimeIntermediate == null)
|
|
return "--";
|
|
return $"{Math.Min(59, Math.Max(0, TimeIntermediate.Value.Minutes)):D2}";
|
|
}
|
|
|
|
private void UpdateTime()
|
|
{
|
|
TimeIntermediate = new TimeSpan(_timeSet.Hour, _timeSet.Minute, 0);
|
|
if ((PickerVariant == PickerVariant.Static && PickerActions == null) || (PickerActions != null && AutoClose))
|
|
{
|
|
Submit();
|
|
}
|
|
}
|
|
|
|
private void OnHourClick()
|
|
{
|
|
_currentView = OpenTo.Hours;
|
|
FocusAsync().AndForget();
|
|
}
|
|
|
|
private void OnMinutesClick()
|
|
{
|
|
_currentView = OpenTo.Minutes;
|
|
FocusAsync().AndForget();
|
|
}
|
|
|
|
private void OnAmClicked()
|
|
{
|
|
_timeSet.Hour %= 12; // "12:-- am" is "00:--" in 24h
|
|
UpdateTime();
|
|
FocusAsync().AndForget();
|
|
}
|
|
|
|
private void OnPmClicked()
|
|
{
|
|
if (_timeSet.Hour <= 12) // "12:-- pm" is "12:--" in 24h
|
|
_timeSet.Hour += 12;
|
|
_timeSet.Hour %= 24;
|
|
UpdateTime();
|
|
FocusAsync().AndForget();
|
|
}
|
|
|
|
protected string ToolbarClass =>
|
|
new CssBuilder("mud-picker-timepicker-toolbar")
|
|
.AddClass($"mud-picker-timepicker-toolbar-landscape", Orientation == Orientation.Landscape && PickerVariant == PickerVariant.Static)
|
|
.AddClass(Class)
|
|
.Build();
|
|
|
|
protected string HoursButtonClass =>
|
|
new CssBuilder("mud-timepicker-button")
|
|
.AddClass($"mud-timepicker-toolbar-text", _currentView == OpenTo.Minutes)
|
|
.Build();
|
|
|
|
protected string MinuteButtonClass =>
|
|
new CssBuilder("mud-timepicker-button")
|
|
.AddClass($"mud-timepicker-toolbar-text", _currentView == OpenTo.Hours)
|
|
.Build();
|
|
|
|
protected string AmButtonClass =>
|
|
new CssBuilder("mud-timepicker-button")
|
|
.AddClass($"mud-timepicker-toolbar-text", !IsAm) // gray it out
|
|
.Build();
|
|
|
|
protected string PmButtonClass =>
|
|
new CssBuilder("mud-timepicker-button")
|
|
.AddClass($"mud-timepicker-toolbar-text", !IsPm) // gray it out
|
|
.Build();
|
|
|
|
private string HourDialClass =>
|
|
new CssBuilder("mud-time-picker-hour")
|
|
.AddClass($"mud-time-picker-dial")
|
|
.AddClass($"mud-time-picker-dial-out", _currentView != OpenTo.Hours)
|
|
.AddClass($"mud-time-picker-dial-hidden", _currentView != OpenTo.Hours)
|
|
.Build();
|
|
|
|
private string MinuteDialClass =>
|
|
new CssBuilder("mud-time-picker-minute")
|
|
.AddClass($"mud-time-picker-dial")
|
|
.AddClass($"mud-time-picker-dial-out", _currentView != OpenTo.Minutes)
|
|
.AddClass($"mud-time-picker-dial-hidden", _currentView != OpenTo.Minutes)
|
|
.Build();
|
|
|
|
private bool IsAm => _timeSet.Hour >= 00 && _timeSet.Hour < 12; // am is 00:00 to 11:59
|
|
private bool IsPm => _timeSet.Hour >= 12 && _timeSet.Hour < 24; // pm is 12:00 to 23:59
|
|
|
|
private string GetClockPinColor()
|
|
{
|
|
return $"mud-picker-time-clock-pin mud-{Color.ToDescriptionString()}";
|
|
}
|
|
|
|
private string GetClockPointerColor()
|
|
{
|
|
if (MouseDown)
|
|
return $"mud-picker-time-clock-pointer mud-{Color.ToDescriptionString()}";
|
|
else
|
|
return $"mud-picker-time-clock-pointer mud-picker-time-clock-pointer-animation mud-{Color.ToDescriptionString()}";
|
|
}
|
|
|
|
private string GetClockPointerThumbColor()
|
|
{
|
|
var deg = GetDeg();
|
|
if (deg % 30 == 0)
|
|
return $"mud-picker-time-clock-pointer-thumb mud-onclock-text mud-onclock-primary mud-{Color.ToDescriptionString()}";
|
|
else
|
|
return $"mud-picker-time-clock-pointer-thumb mud-onclock-minute mud-{Color.ToDescriptionString()}-text";
|
|
}
|
|
|
|
private string GetNumberColor(int value)
|
|
{
|
|
if (_currentView == OpenTo.Hours)
|
|
{
|
|
var h = _timeSet.Hour;
|
|
if (AmPm)
|
|
{
|
|
h = _timeSet.Hour % 12;
|
|
if (_timeSet.Hour % 12 == 0)
|
|
h = 12;
|
|
}
|
|
if (h == value)
|
|
return $"mud-clock-number mud-theme-{Color.ToDescriptionString()}";
|
|
}
|
|
else if (_currentView == OpenTo.Minutes && _timeSet.Minute == value)
|
|
{
|
|
return $"mud-clock-number mud-theme-{Color.ToDescriptionString()}";
|
|
}
|
|
return $"mud-clock-number";
|
|
}
|
|
|
|
private double GetDeg()
|
|
{
|
|
double deg = 0;
|
|
if (_currentView == OpenTo.Hours)
|
|
deg = (_timeSet.Hour * 30) % 360;
|
|
if (_currentView == OpenTo.Minutes)
|
|
deg = (_timeSet.Minute * 6) % 360;
|
|
return deg;
|
|
}
|
|
|
|
private string GetTransform(double angle, double radius, double offsetX, double offsetY)
|
|
{
|
|
angle = angle / 180 * Math.PI;
|
|
var x = (Math.Sin(angle) * radius + offsetX).ToString("F3", CultureInfo.InvariantCulture);
|
|
var y = ((Math.Cos(angle) + 1) * radius + offsetY).ToString("F3", CultureInfo.InvariantCulture);
|
|
return $"transform: translate({x}px, {y}px);";
|
|
}
|
|
|
|
private string GetPointerRotation()
|
|
{
|
|
double deg = 0;
|
|
if (_currentView == OpenTo.Hours)
|
|
deg = (_timeSet.Hour * 30) % 360;
|
|
if (_currentView == OpenTo.Minutes)
|
|
deg = (_timeSet.Minute * 6) % 360;
|
|
return $"rotateZ({deg}deg);";
|
|
}
|
|
|
|
private string GetPointerHeight()
|
|
{
|
|
var height = 40;
|
|
if (_currentView == OpenTo.Minutes)
|
|
height = 40;
|
|
if (_currentView == OpenTo.Hours)
|
|
{
|
|
if (!AmPm && _timeSet.Hour > 0 && _timeSet.Hour < 13)
|
|
height = 26;
|
|
else
|
|
height = 40;
|
|
}
|
|
return $"{height}%;";
|
|
}
|
|
|
|
private readonly SetTime _timeSet = new();
|
|
private int _initialHour;
|
|
private int _initialMinute;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
base.OnInitialized();
|
|
UpdateTimeSetFromTime();
|
|
_currentView = OpenTo;
|
|
_initialHour = _timeSet.Hour;
|
|
_initialMinute = _timeSet.Minute;
|
|
}
|
|
|
|
|
|
private void UpdateTimeSetFromTime()
|
|
{
|
|
if (TimeIntermediate == null)
|
|
{
|
|
_timeSet.Hour = 0;
|
|
_timeSet.Minute = 0;
|
|
return;
|
|
}
|
|
_timeSet.Hour = TimeIntermediate.Value.Hours;
|
|
_timeSet.Minute = TimeIntermediate.Value.Minutes;
|
|
}
|
|
|
|
public bool MouseDown { get; set; }
|
|
|
|
/// <summary>
|
|
/// Sets Mouse Down bool to true if mouse is inside the clock mask.
|
|
/// </summary>
|
|
private void OnMouseDown(MouseEventArgs e)
|
|
{
|
|
MouseDown = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets Mouse Down bool to false if mouse is inside the clock mask.
|
|
/// </summary>
|
|
private void OnMouseUp(MouseEventArgs e)
|
|
{
|
|
if (MouseDown && _currentView == OpenTo.Minutes && _timeSet.Minute != _initialMinute || _currentView == OpenTo.Hours && _timeSet.Hour != _initialHour && TimeEditMode == TimeEditMode.OnlyHours)
|
|
{
|
|
MouseDown = false;
|
|
SubmitAndClose();
|
|
}
|
|
|
|
MouseDown = false;
|
|
|
|
if (_currentView == OpenTo.Hours && _timeSet.Hour != _initialHour && TimeEditMode == TimeEditMode.Normal)
|
|
{
|
|
_currentView = OpenTo.Minutes;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If MouseDown is true enables "dragging" effect on the clock pin/stick.
|
|
/// </summary>
|
|
private void OnMouseOverHour(int value)
|
|
{
|
|
if (MouseDown)
|
|
{
|
|
_timeSet.Hour = value;
|
|
UpdateTime();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// On click for the hour "sticks", sets the hour.
|
|
/// </summary>
|
|
private void OnMouseClickHour(int value)
|
|
{
|
|
var h = value;
|
|
if (AmPm)
|
|
{
|
|
if (IsAm && value == 12)
|
|
h = 0;
|
|
else if (IsPm && value < 12)
|
|
h = value + 12;
|
|
}
|
|
_timeSet.Hour = h;
|
|
UpdateTime();
|
|
|
|
if (TimeEditMode == TimeEditMode.Normal)
|
|
{
|
|
_currentView = OpenTo.Minutes;
|
|
}
|
|
else if (TimeEditMode == TimeEditMode.OnlyHours)
|
|
{
|
|
SubmitAndClose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// On mouse over for the minutes "sticks", sets the minute.
|
|
/// </summary>
|
|
private void OnMouseOverMinute(int value)
|
|
{
|
|
if (MouseDown)
|
|
{
|
|
_timeSet.Minute = value;
|
|
UpdateTime();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// On click for the minute "sticks", sets the minute.
|
|
/// </summary>
|
|
private void OnMouseClickMinute(int value)
|
|
{
|
|
_timeSet.Minute = value;
|
|
UpdateTime();
|
|
SubmitAndClose();
|
|
}
|
|
|
|
protected async void SubmitAndClose()
|
|
{
|
|
if (PickerActions == null || AutoClose)
|
|
{
|
|
Submit();
|
|
|
|
if (PickerVariant != PickerVariant.Static)
|
|
{
|
|
await Task.Delay(ClosingDelay);
|
|
Close(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected internal override void HandleKeyDown(KeyboardEventArgs obj)
|
|
{
|
|
if (Disabled || ReadOnly)
|
|
return;
|
|
base.HandleKeyDown(obj);
|
|
switch (obj.Key)
|
|
{
|
|
case "ArrowRight":
|
|
if (IsOpen)
|
|
{
|
|
if (obj.CtrlKey == true)
|
|
{
|
|
ChangeHour(1);
|
|
}
|
|
else if (obj.ShiftKey == true)
|
|
{
|
|
if (_timeSet.Minute > 55)
|
|
{
|
|
ChangeHour(1);
|
|
}
|
|
ChangeMinute(5);
|
|
}
|
|
else
|
|
{
|
|
if (_timeSet.Minute == 59)
|
|
{
|
|
ChangeHour(1);
|
|
}
|
|
ChangeMinute(1);
|
|
}
|
|
}
|
|
break;
|
|
case "ArrowLeft":
|
|
if (IsOpen)
|
|
{
|
|
if (obj.CtrlKey == true)
|
|
{
|
|
ChangeHour(-1);
|
|
}
|
|
else if (obj.ShiftKey == true)
|
|
{
|
|
if (_timeSet.Minute < 5)
|
|
{
|
|
ChangeHour(-1);
|
|
}
|
|
ChangeMinute(-5);
|
|
}
|
|
else
|
|
{
|
|
if (_timeSet.Minute == 0)
|
|
{
|
|
ChangeHour(-1);
|
|
}
|
|
ChangeMinute(-1);
|
|
}
|
|
}
|
|
break;
|
|
case "ArrowUp":
|
|
if (IsOpen == false && Editable == false)
|
|
{
|
|
IsOpen = true;
|
|
}
|
|
else if (obj.AltKey == true)
|
|
{
|
|
IsOpen = false;
|
|
}
|
|
else if (obj.ShiftKey == true)
|
|
{
|
|
ChangeHour(5);
|
|
}
|
|
else
|
|
{
|
|
ChangeHour(1);
|
|
}
|
|
break;
|
|
case "ArrowDown":
|
|
if (IsOpen == false && Editable == false)
|
|
{
|
|
IsOpen = true;
|
|
}
|
|
else if (obj.ShiftKey == true)
|
|
{
|
|
ChangeHour(-5);
|
|
}
|
|
else
|
|
{
|
|
ChangeHour(-1);
|
|
}
|
|
break;
|
|
case "Escape":
|
|
ReturnTimeBackUp();
|
|
break;
|
|
case "Enter":
|
|
case "NumpadEnter":
|
|
if (!IsOpen)
|
|
{
|
|
Open();
|
|
}
|
|
else
|
|
{
|
|
Submit();
|
|
Close();
|
|
_inputReference?.SetText(Text);
|
|
}
|
|
break;
|
|
case " ":
|
|
if (!Editable)
|
|
{
|
|
if (!IsOpen)
|
|
{
|
|
Open();
|
|
}
|
|
else
|
|
{
|
|
Submit();
|
|
Close();
|
|
_inputReference?.SetText(Text);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected void ChangeMinute(int val)
|
|
{
|
|
_currentView = OpenTo.Minutes;
|
|
_timeSet.Minute = (_timeSet.Minute + val + 60) % 60;
|
|
UpdateTime();
|
|
}
|
|
|
|
protected void ChangeHour(int val)
|
|
{
|
|
_currentView = OpenTo.Hours;
|
|
_timeSet.Hour = (_timeSet.Hour + val + 24) % 24;
|
|
UpdateTime();
|
|
}
|
|
|
|
protected void ReturnTimeBackUp()
|
|
{
|
|
if (Time == null)
|
|
{
|
|
TimeIntermediate = null;
|
|
}
|
|
else
|
|
{
|
|
_timeSet.Hour = Time.Value.Hours;
|
|
_timeSet.Minute = Time.Value.Minutes;
|
|
UpdateTime();
|
|
}
|
|
}
|
|
|
|
private class SetTime
|
|
{
|
|
public int Hour { get; set; }
|
|
|
|
public int Minute { get; set; }
|
|
|
|
}
|
|
}
|