|
|
@ -2,11 +2,191 @@
|
|
|
|
using Connected.Utilities;
|
|
|
|
using Connected.Utilities;
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
|
|
|
|
|
|
using System.Numerics;
|
|
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
|
|
using System.Timers;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Connected.Components;
|
|
|
|
namespace Connected.Components;
|
|
|
|
|
|
|
|
|
|
|
|
public partial class Input<T> : InputBase<T>
|
|
|
|
public partial class Input<T> : InputBase<T>
|
|
|
|
{
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
* Debounce
|
|
|
|
|
|
|
|
*
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
|
|
/// The current character counter, displayed below the text field.
|
|
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
|
|
[Parameter] public string CounterText { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private System.Timers.Timer _timer;
|
|
|
|
|
|
|
|
private double _debounceInterval;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
|
|
/// Interval to be awaited in MILLISECONDS before changing the Text value
|
|
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
|
|
[Parameter]
|
|
|
|
|
|
|
|
public double TextChangeDelay
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
get => _debounceInterval;
|
|
|
|
|
|
|
|
set
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (NumericConverter<double>.AreEqual(_debounceInterval, value))
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
_debounceInterval = value;
|
|
|
|
|
|
|
|
if (_debounceInterval == 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// not debounced, dispose timer if any
|
|
|
|
|
|
|
|
ClearTimer(suppressTick: false);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
SetTimer();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
|
|
/// callback to be called when the debounce interval has elapsed
|
|
|
|
|
|
|
|
/// receives the Text as a parameter
|
|
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
|
|
[Parameter] public EventCallback<string> OnDebounceIntervalElapsed { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected Task OnDebounceChange()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (TextChangeDelay > 0 && _timer != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
_timer.Stop();
|
|
|
|
|
|
|
|
return base.UpdateValuePropertyAsync(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected override Task UpdateValuePropertyAsync(bool updateText)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// This method is called when Value property needs to be refreshed from the current Text property, so typically because Text property has changed.
|
|
|
|
|
|
|
|
// We want to debounce only text-input, not a value being set, so the debouncing is only done when updateText==false (because that indicates the
|
|
|
|
|
|
|
|
// change came from a Text setter)
|
|
|
|
|
|
|
|
if (updateText)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// we have a change coming not from the Text setter, no debouncing is needed
|
|
|
|
|
|
|
|
return base.UpdateValuePropertyAsync(updateText);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// if debounce interval is 0 we update immediately
|
|
|
|
|
|
|
|
if (TextChangeDelay <= 0 || _timer == null)
|
|
|
|
|
|
|
|
return base.UpdateValuePropertyAsync(updateText);
|
|
|
|
|
|
|
|
// If a debounce interval is defined, we want to delay the update of Value property.
|
|
|
|
|
|
|
|
_timer.Stop();
|
|
|
|
|
|
|
|
// restart the timer while user is typing
|
|
|
|
|
|
|
|
_timer.Start();
|
|
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnParametersSet()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
base.OnParametersSet();
|
|
|
|
|
|
|
|
// if input is to be debounced, makes sense to bind the change of the text to oninput
|
|
|
|
|
|
|
|
// so we set Immediate to true
|
|
|
|
|
|
|
|
if (TextChangeDelay > 0)
|
|
|
|
|
|
|
|
Immediate = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void SetTimer()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (_timer == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
_timer = new System.Timers.Timer();
|
|
|
|
|
|
|
|
_timer.Elapsed += OnTimerTick;
|
|
|
|
|
|
|
|
_timer.AutoReset = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
_timer.Interval = TextChangeDelay;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void OnTimerTick(object sender, ElapsedEventArgs e)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
InvokeAsync(OnTimerTickGuiThread).AndForget();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private async Task OnTimerTickGuiThread()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
await base.UpdateValuePropertyAsync(false);
|
|
|
|
|
|
|
|
await OnDebounceIntervalElapsed.InvokeAsync(Text);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void ClearTimer(bool suppressTick = false)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (_timer == null)
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
var wasEnabled = _timer.Enabled;
|
|
|
|
|
|
|
|
_timer.Stop();
|
|
|
|
|
|
|
|
_timer.Elapsed -= OnTimerTick;
|
|
|
|
|
|
|
|
_timer.Dispose();
|
|
|
|
|
|
|
|
_timer = null;
|
|
|
|
|
|
|
|
if (wasEnabled && !suppressTick)
|
|
|
|
|
|
|
|
OnTimerTickGuiThread().AndForget();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
* Debounce end
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected CssBuilder CompiledHelperContainerClassList
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
get
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return new CssBuilder("input-control-helper-container")
|
|
|
|
|
|
|
|
.AddClass($"px-1", Variant == Variant.Filled)
|
|
|
|
|
|
|
|
.AddClass($"px-2", Variant == Variant.Outlined)
|
|
|
|
|
|
|
|
.AddClass($"px-1", Variant == Variant.Text)
|
|
|
|
|
|
|
|
.AddClass(HelperContainerClassList);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
|
|
/// A space separated list of class names, added on top of the default helper container class list.
|
|
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
|
|
[Parameter]
|
|
|
|
|
|
|
|
public string? HelperContainerClassList { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected CssBuilder CompiledHelperClassList
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
get
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return new CssBuilder("input-helper-text")
|
|
|
|
|
|
|
|
.AddClass("input-helper-onfocus", HelperTextOnFocus)
|
|
|
|
|
|
|
|
.AddClass(HelperClassList);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
|
|
/// A space separated list of class names, added on top of the default helper class list.
|
|
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
|
|
[Parameter]
|
|
|
|
|
|
|
|
public string? HelperClassList { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*protected string HelperContainer =>
|
|
|
|
|
|
|
|
new CssBuilder("input-control-helper-container")
|
|
|
|
|
|
|
|
.AddClass($"px-1", Variant == Variant.Filled)
|
|
|
|
|
|
|
|
.AddClass($"px-2", Variant == Variant.Outlined)
|
|
|
|
|
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected string HelperClass =>
|
|
|
|
|
|
|
|
new CssBuilder("input-helper-text")
|
|
|
|
|
|
|
|
.AddClass("input-helper-onfocus", HelperTextOnFocus)
|
|
|
|
|
|
|
|
.Build();*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private string GetCounterText()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
string result = Text.Length.ToString();
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(Text)) result = "0";
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected string Classname => InputCssHelper.GetClassname(this,
|
|
|
|
protected string Classname => InputCssHelper.GetClassname(this,
|
|
|
|
() => HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder));
|
|
|
|
() => HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(Text) || Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder));
|
|
|
|
|
|
|
|
|
|
|
@ -31,15 +211,79 @@ public partial class Input<T> : InputBase<T>
|
|
|
|
|
|
|
|
|
|
|
|
protected string InputTypeString => InputType.ToDescription();
|
|
|
|
protected string InputTypeString => InputType.ToDescription();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsDecimalNumber(string type)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
switch (type.ToLower())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
case "double":
|
|
|
|
|
|
|
|
case "float":
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsNumber(string s)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
bool result = false;
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
double d;
|
|
|
|
|
|
|
|
result= double.TryParse(s, out d);
|
|
|
|
|
|
|
|
} catch
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
result = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private string ValidateInput(string value)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
string result = value;
|
|
|
|
|
|
|
|
if (value is not null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var expectedType = typeof(T).Name;
|
|
|
|
|
|
|
|
if (IsNumericType(expectedType))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (IsNumber(value.ToString()))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
result = value.ToString();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (IsDecimalNumber(value.ToString()))
|
|
|
|
|
|
|
|
result = Regex.Replace(value.ToString(), "[^0-9.]", "");
|
|
|
|
|
|
|
|
else result = Regex.Replace(value.ToString(), "[^0-9]", "");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
result = value;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
}*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected Task OnInput(ChangeEventArgs args)
|
|
|
|
protected Task OnInput(ChangeEventArgs args)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (!Immediate)
|
|
|
|
if (!Immediate)
|
|
|
|
return Task.CompletedTask;
|
|
|
|
return Task.CompletedTask;
|
|
|
|
_isFocused = true;
|
|
|
|
_isFocused = true;
|
|
|
|
return SetTextAsync(args?.Value as string);
|
|
|
|
return SetTextAsync(args?.Value as string);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected async Task OnChange(ChangeEventArgs args)
|
|
|
|
protected async Task OnChange(ChangeEventArgs args)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (TextChangeDelay > 0 && _timer != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
_timer.Stop();
|
|
|
|
|
|
|
|
await UpdateValuePropertyAsync(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_internalText = args?.Value as string;
|
|
|
|
_internalText = args?.Value as string;
|
|
|
|
await OnInternalInputChanged.InvokeAsync(args);
|
|
|
|
await OnInternalInputChanged.InvokeAsync(args);
|
|
|
@ -49,6 +293,8 @@ public partial class Input<T> : InputBase<T>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// Paste hook for descendants.
|
|
|
|
/// Paste hook for descendants.
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
@ -164,12 +410,12 @@ public partial class Input<T> : InputBase<T>
|
|
|
|
UpdateClearable(Text);
|
|
|
|
UpdateClearable(Text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override async Task UpdateValuePropertyAsync(bool updateText)
|
|
|
|
/*protected override async Task UpdateValuePropertyAsync(bool updateText)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
await base.UpdateValuePropertyAsync(updateText);
|
|
|
|
await base.UpdateValuePropertyAsync(updateText);
|
|
|
|
if (Clearable)
|
|
|
|
if (Clearable)
|
|
|
|
UpdateClearable(Value);
|
|
|
|
UpdateClearable(Value);
|
|
|
|
}
|
|
|
|
}*/
|
|
|
|
|
|
|
|
|
|
|
|
protected virtual async Task ClearButtonClickHandlerAsync(MouseEventArgs e)
|
|
|
|
protected virtual async Task ClearButtonClickHandlerAsync(MouseEventArgs e)
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -196,6 +442,33 @@ public partial class Input<T> : InputBase<T>
|
|
|
|
{
|
|
|
|
{
|
|
|
|
// in WASM (or in BSS with TextUpdateSuppression==false) we always update
|
|
|
|
// in WASM (or in BSS with TextUpdateSuppression==false) we always update
|
|
|
|
_internalText = Text;
|
|
|
|
_internalText = Text;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string baseTypeName = typeof(T).Name;
|
|
|
|
|
|
|
|
if (IsNumericType(baseTypeName))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
InputType = InputType.Number;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsNumericType(string type)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
switch (type.ToLower())
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
case "uint16":
|
|
|
|
|
|
|
|
case "uint32":
|
|
|
|
|
|
|
|
case "uint64":
|
|
|
|
|
|
|
|
case "int16":
|
|
|
|
|
|
|
|
case "int32":
|
|
|
|
|
|
|
|
case "int64":
|
|
|
|
|
|
|
|
case "int":
|
|
|
|
|
|
|
|
case "double":
|
|
|
|
|
|
|
|
case "decimal":
|
|
|
|
|
|
|
|
case "float":
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|