using System.Globalization; using Connected.Annotations; using Connected.Utilities; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; namespace Connected.Components; public abstract class InputBase : FormComponent { private bool _isDirty; protected InputBase() : base(new DefaultConverter()) { } /// /// If true, the input element will be disabled. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool Disabled { get; set; } /// /// If true, the input will be read-only. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool ReadOnly { get; set; } /// /// If true, the input will take up the full width of its container. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public bool FullWidth { get; set; } /// /// If true, the input will update the Value immediately on typing. /// If false, the Value is updated only on Enter. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool Immediate { get; set; } /// /// If true, the input will not have an underline. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public bool DisableUnderLine { get; set; } /// /// The HelperText will be displayed below the text field. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string HelperText { get; set; } /// /// If true, the helper text will only be visible on focus. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool HelperTextOnFocus { get; set; } /// /// Icon that will be used if Adornment is set to Start or End. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string AdornmentIcon { get; set; } /// /// Text that will be used if Adornment is set to Start or End, the Text overrides Icon. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string AdornmentText { get; set; } /// /// The Adornment if used. By default, it is set to None. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public Adornment Adornment { get; set; } = Adornment.None; /// /// The validation is only triggered if the user has changed the input value at least once. By default, it is false /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool OnlyValidateIfDirty { get; set; } = false; /// /// The color of the adornment if used. It supports the theme colors. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public ThemeColor AdornmentColor { get; set; } = ThemeColor.Default; /// /// The aria-label of the adornment. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public string AdornmentAriaLabel { get; set; } = string.Empty; /// /// The Icon Size. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public Size IconSize { get; set; } = Size.Medium; /// /// Button click event if set and Adornment used. /// [Parameter] public EventCallback OnAdornmentClick { get; set; } /// /// Variant to use. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public Variant Variant { get; set; } = Variant.Text; /// /// Will adjust vertical spacing. /// [Parameter] [Category(CategoryTypes.FormComponent.Appearance)] public Margin Margin { get; set; } = Margin.None; /// /// The short hint displayed in the input before the user enters a value. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string Placeholder { get; set; } /// /// If set, will display the counter, value 0 will display current count but no stop count. /// [Parameter] [Category(CategoryTypes.FormComponent.Validation)] public int? Counter { get; set; } /// /// Maximum number of characters that the input will accept /// [Parameter] [Category(CategoryTypes.FormComponent.Validation)] public int MaxLength { get; set; } = 524288; /// /// If string has value the label text will be displayed in the input, and scaled down at the top if the input has value. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string Label { get; set; } /// /// If true the input will focus automatically. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool AutoFocus { get; set; } /// /// A multiline input (textarea) will be shown, if set to more than one line. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public int Lines { get; set; } = 1; /// /// The text to be displayed. /// [Parameter] [Category(CategoryTypes.FormComponent.Data)] public string Text { get; set; } /// /// When TextUpdateSuppression is true (which is default) the text can not be updated by bindings while the component is focused in BSS (not WASM). /// This solves issue #1012: Textfield swallowing chars when typing rapidly /// If you need to update the input's text while it is focused you can set this parameter to false. /// Note: on WASM text update suppression is not active, so this parameter has no effect. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool TextUpdateSuppression { get; set; } = true; /// /// Hints at the type of data that might be entered by the user while editing the input /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public virtual InputMode InputMode { get; set; } = InputMode.text; /// /// The pattern attribute, when specified, is a regular expression which the input's value must match in order for the value to pass constraint validation. It must be a valid JavaScript regular expression /// Not Supported in multline input /// [Parameter] [Category(CategoryTypes.FormComponent.Validation)] public virtual string Pattern { get; set; } /// /// Derived classes need to override this if they can be something other than text /// internal virtual InputType GetInputType() { return InputType.Text; } protected virtual async Task SetTextAsync(string text, bool updateValue = true) { if (Text != text) { Text = text; if (!string.IsNullOrWhiteSpace(Text)) Modified = true; if (updateValue) await UpdateValuePropertyAsync(false); await TextChanged.InvokeAsync(Text); } } /// /// Text change hook for descendants. Called when Text needs to be refreshed from current Value property. /// protected virtual Task UpdateTextPropertyAsync(bool updateValue) { return SetTextAsync(Converter.Convert(Value), updateValue); } /// /// Focuses the element /// /// The ValueTask public virtual ValueTask FocusAsync() { return new ValueTask(); } public virtual ValueTask BlurAsync() { return new ValueTask(); } public virtual ValueTask SelectAsync() { return new ValueTask(); } public virtual ValueTask SelectRangeAsync(int pos1, int pos2) { return new ValueTask(); } /// /// Fired when the text value changes. /// [Parameter] public EventCallback TextChanged { get; set; } /// /// Fired when the element loses focus. /// [Parameter] public EventCallback OnBlur { get; set; } /// /// Fired when the element changes internally its text value. /// [Parameter] public EventCallback OnInternalInputChanged { get; set; } protected bool _isFocused; protected internal virtual void OnBlurred(FocusEventArgs obj) { _isFocused = false; if (!OnlyValidateIfDirty || _isDirty) { Modified = true; BeginValidateAfter(OnBlur.InvokeAsync(obj)); } } /// /// Fired on the KeyDown event. /// [Parameter] public EventCallback OnKeyDown { get; set; } protected virtual void InvokeKeyDown(KeyboardEventArgs obj) { _isFocused = true; OnKeyDown.InvokeAsync(obj).AndForget(); } /// /// Prevent the default action for the KeyDown event. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool KeyDownPreventDefault { get; set; } /// /// Fired on the KeyPress event. /// [Parameter] public EventCallback OnKeyPress { get; set; } protected virtual void InvokeKeyPress(KeyboardEventArgs obj) { OnKeyPress.InvokeAsync(obj).AndForget(); } /// /// Prevent the default action for the KeyPress event. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool KeyPressPreventDefault { get; set; } /// /// Fired on the KeyUp event. /// [Parameter] public EventCallback OnKeyUp { get; set; } protected virtual void InvokeKeyUp(KeyboardEventArgs obj) { _isFocused = true; OnKeyUp.InvokeAsync(obj).AndForget(); } /// /// Prevent the default action for the KeyUp event. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public bool KeyUpPreventDefault { get; set; } /// /// Fired when the Value property changes. /// [Parameter] public EventCallback ValueChanged { get; set; } /// /// The value of this input element. /// [Parameter] [Category(CategoryTypes.FormComponent.Data)] public T Value { get => _value; set => _value = value; } protected virtual async Task SetValueAsync(T value, bool updateText = true) { if (!EqualityComparer.Default.Equals(Value, value)) { _isDirty = true; Value = value; if (updateText) await UpdateTextPropertyAsync(false); await ValueChanged.InvokeAsync(Value); BeginValidate(); FieldChanged(Value); } } /// /// Value change hook for descendants. Called when Value needs to be refreshed from current Text property. /// protected virtual Task UpdateValuePropertyAsync(bool updateText) { return SetValueAsync(Converter.ConvertBack(Text), updateText); } protected override bool SetConverter(Converter value) { var changed = base.SetConverter(value); if (changed) UpdateTextPropertyAsync(false).AndForget(); // refresh only Text property from current Value return changed; } protected override bool SetCulture(CultureInfo value) { var changed = base.SetCulture(value); if (changed) UpdateTextPropertyAsync(false).AndForget(); // refresh only Text property from current Value return changed; } /// /// Conversion format parameter for ToString(), can be used for formatting primitive types, DateTimes and TimeSpans /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public string Format { get => ((ToStringConverter)Converter).Format; set => SetFormat(value); } protected virtual bool SetFormat(string value) { var changed = Format != value; if (changed) { ((ToStringConverter)Converter).Format = value; UpdateTextPropertyAsync(false).AndForget(); // refresh only Text property from current Value } return changed; } protected override Task ValidateValue() { if (SubscribeToParentForm) return base.ValidateValue(); return Task.CompletedTask; } protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); // Because the way the Value setter is built, it won't cause an update if the incoming Value is // equal to the initial value. This is why we force an update to the Text property here. if (typeof(T) != typeof(string)) await UpdateTextPropertyAsync(false); if (Label == null && For != null) Label = For.GetLabelString(); } public virtual void ForceRender(bool forceTextUpdate) { _forceTextUpdate = true; UpdateTextPropertyAsync(false).AndForget(); StateHasChanged(); } protected bool _forceTextUpdate; public override async Task SetParametersAsync(ParameterView parameters) { await base.SetParametersAsync(parameters); var hasText = parameters.Contains(nameof(Text)); var hasValue = parameters.Contains(nameof(Value)); // Refresh Value from Text if (hasText && !hasValue) await UpdateValuePropertyAsync(false); // Refresh Text from Value if (hasValue && !hasText) { var updateText = true; if (_isFocused && !_forceTextUpdate) { // Text update suppression, only in BSS (not in WASM). // This is a fix for #1012 if (RuntimeLocation.IsServerSide && TextUpdateSuppression) updateText = false; } if (updateText) { _forceTextUpdate = false; await UpdateTextPropertyAsync(false); } } } protected override async Task OnAfterRenderAsync(bool firstRender) { //Only focus automatically after the first render cycle! if (firstRender && AutoFocus) { await FocusAsync(); } } protected override void OnParametersSet() { if (SubscribeToParentForm) base.OnParametersSet(); } protected override void ResetValue() { SetTextAsync(null, updateValue: true).AndForget(); _isDirty = false; base.ResetValue(); } }