diff --git a/TeslaSolarCharger/Client/Components/GenericInput.razor b/TeslaSolarCharger/Client/Components/GenericInput.razor index e2a899aa6..940bb435c 100644 --- a/TeslaSolarCharger/Client/Components/GenericInput.razor +++ b/TeslaSolarCharger/Client/Components/GenericInput.razor @@ -2,6 +2,7 @@ @using System.Reflection @using System.ComponentModel.DataAnnotations @using System.ComponentModel +@using Lysando.LabStorageV2.UiHelper.Wrapper.Contracts @using MudExtensions @using TeslaSolarCharger.Shared.Attributes @using TeslaSolarCharger.Shared.Helper.Contracts @@ -9,30 +10,36 @@ @inject IConstants Constants @inject IStringHelper StringHelper -@* ReSharper disable once InconsistentNaming *@ -@inject IJSRuntime JSRuntime @typeparam T @if (!EqualityComparer.Default.Equals(Value, default(T)) || !IsReadOnly) {
-
+
@if (typeof(T) == typeof(DateTime?)) { - + Margin="InputMargin" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())" /> } else if (DropDownOptions != default && typeof(T) == typeof(int?)) { - + ItemCollection="@DropDownOptions.Keys.Select(k => (int?)k).ToList()" + Immediate="@ImmediateValueUpdate" + Virtualize="true" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())"> } else if (DropDownOptions != default && typeof(T) == typeof(HashSet)) { - + ItemCollection="@DropDownOptions.Keys.ToList()" + Immediate="@ImmediateValueUpdate" + Virtualize="true" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())"> + + + } + else if (LongIdDropDownOptions != default && typeof(T) == typeof(long?)) + { + } + else if (LongIdDropDownOptions != default && typeof(T) == typeof(HashSet)) + { + + + + } else if (StringIdDropDownOptions != default && typeof(T) == typeof(string)) { @* Even though compiler says ?? string.Empty is not needed in ToStringFunc, it is needed. *@ - + ToStringFunc="@(new Func(x => StringIdDropDownOptions.TryGetValue(x, out var value) ? value : string.Empty))" + ItemCollection="@StringIdDropDownOptions.Keys.ToList()" + Immediate="@ImmediateValueUpdate" + Virtualize="true" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())"> } else if (StringIdDropDownOptions != default && typeof(T) == typeof(HashSet)) { //ToDo: For label is missing @* Even though compiler says ?? string.Empty is not needed in ToStringFunc, it is needed. *@ - + MultiSelectionTextFunc="@(GetMultiSelectionText)" + ToStringFunc="@(new Func(x => StringIdDropDownOptions.TryGetValue(x, out var value) ? value : string.Empty))" + ItemCollection="@StringIdDropDownOptions.Keys.ToList()" + Immediate="@ImmediateValueUpdate" + Virtualize="true" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())"> } else if (typeof(T) == typeof(short) - || typeof(T) == typeof(short?) - || typeof(T) == typeof(ushort) - || typeof(T) == typeof(ushort?) - || typeof(T) == typeof(int) - || typeof(T) == typeof(int?) - || typeof(T) == typeof(uint) - || typeof(T) == typeof(uint?) - || typeof(T) == typeof(long) - || typeof(T) == typeof(long?) - || typeof(T) == typeof(ulong) - || typeof(T) == typeof(ulong?) - || typeof(T) == typeof(float) - || typeof(T) == typeof(float?) - || typeof(T) == typeof(double) - || typeof(T) == typeof(double?) - || typeof(T) == typeof(decimal) - || typeof(T) == typeof(decimal?)) + || typeof(T) == typeof(short?) + || typeof(T) == typeof(ushort) + || typeof(T) == typeof(ushort?) + || typeof(T) == typeof(int) + || typeof(T) == typeof(int?) + || typeof(T) == typeof(uint) + || typeof(T) == typeof(uint?) + || typeof(T) == typeof(long) + || typeof(T) == typeof(long?) + || typeof(T) == typeof(ulong) + || typeof(T) == typeof(ulong?) + || typeof(T) == typeof(float) + || typeof(T) == typeof(float?) + || typeof(T) == typeof(double) + || typeof(T) == typeof(double?) + || typeof(T) == typeof(decimal) + || typeof(T) == typeof(decimal?)) { - + Margin="InputMargin" + Immediate="@ImmediateValueUpdate" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())" /> } else if (IsNormalText()) { if (IsPassword) { - + Margin="InputMargin" + Immediate="@ImmediateValueUpdate" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())" /> } else { - + Margin="InputMargin" + Immediate="@ImmediateValueUpdate" + Clearable="@(Clearable && !IsReadOnly && !IsDisabled)" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())" /> } } else if (typeof(T) == typeof(bool) - || typeof(T) == typeof(bool?)) + || typeof(T) == typeof(bool?)) { - + Dense="InputMargin == Margin.Dense" + @attributes="@(ShouldBeInErrorState.HasValue ? new Dictionary + { + { "Error", ShouldBeInErrorState.Value }, + { "ErrorText", ErrorMessage ?? string.Empty }, + } :new())"> } else @@ -179,7 +307,7 @@ throw new ArgumentOutOfRangeException(); }
- @if(!string.IsNullOrEmpty(HelperText)) + @if (!string.IsNullOrEmpty(HelperText)) {
@HelperText @@ -188,7 +316,7 @@
@if (!string.IsNullOrEmpty(PostfixButtonStartIcon)) { -
+
Constants.DefaultMargin; private Margin InputMargin => Constants.InputMargin; @@ -230,12 +367,8 @@ } } - private string _inputId = Guid.NewGuid().ToString(); - [Parameter] - public int TextAreaMinimumLines { get; set; } = 1; - - private int TextAreaLines { get; set; } = 1; + private bool? _isReadOnlyParameter; private Expression>? ForDateTime { @@ -261,49 +394,9 @@ private int MultiSelectValue { get; set; } = 0; - private string MultiSelectStringValue { get; set; } = string.Empty; - - private async Task GetVisibleLineBreaksCount() - { - return await JSRuntime.InvokeAsync("countVisibleLineBreaks", _inputId); - } - - private async Task IsTextCutOff() - { - return await JSRuntime.InvokeAsync("isInputTextCutOff", _inputId); - } - - private async Task SetFocusToCurrentInput() - { - await JSRuntime.InvokeVoidAsync("setFocusToInput", _inputId); - } + private long MultiSelectLongValue { get; set; } = 0; - private async Task UpdateLineCount(bool shouldSetFocus) - { - var textFieldReplacedByTextarea = false; - if (IsNormalText() && !IsPassword) - { - if (TextAreaLines < 2) - { - if (!await IsTextCutOff()) - { - return; - } - textFieldReplacedByTextarea = true; - } - var lineCount = await GetVisibleLineBreaksCount(); - TextAreaLines = lineCount > TextAreaMinimumLines ? lineCount : TextAreaMinimumLines; - this.StateHasChanged(); - if (shouldSetFocus && textFieldReplacedByTextarea) - { - await SetFocusToCurrentInput(); - } - if (textFieldReplacedByTextarea) - { - await UpdateLineCount(false); - } - } - } + private string MultiSelectStringValue { get; set; } = string.Empty; private Expression>> ForMultiSelectValues { @@ -328,7 +421,7 @@ } throw new InvalidCastException(); } - set => throw new NotImplementedException($"{nameof(ForMultiSelectValues)} can not be set."); + set => throw new NotImplementedException($"{nameof(ForNullableString)} can not be set."); } private Expression> ForNullableInt @@ -341,12 +434,28 @@ } throw new InvalidCastException(); } - set => throw new NotImplementedException($"{nameof(ForMultiSelectValues)} can not be set."); + set => throw new NotImplementedException($"{nameof(ForNullableInt)} can not be set."); + } + + private Expression> ForNullableLong + { + get + { + if (typeof(T) == typeof(long?) && For != null) + { + return (Expression>)(object)For; + } + throw new InvalidCastException(); + } + set => throw new NotImplementedException($"{nameof(ForNullableLong)} can not be set."); } [Parameter] public Dictionary? DropDownOptions { get; set; } + [Parameter] + public Dictionary? LongIdDropDownOptions { get; set; } + [Parameter] public Dictionary? StringIdDropDownOptions { get; set; } @@ -384,17 +493,40 @@ public bool? IsRequiredParameter { get; set; } [Parameter] - public bool? IsReadOnlyParameter { get; set; } + public bool? IsReadOnlyParameter + { + get => _isReadOnlyParameter; + set + { + if (_isReadOnlyParameter != value && _componentRenderedCounter > 0) + { + _isReadOnlyParameter = value; + OnAfterRender(true); + } + else + { + _isReadOnlyParameter = value; + } + } + } [Parameter] public string? HelperText { get; set; } + [Parameter] + public bool ImmediateValueUpdate { get; set; } + + [Parameter] + public bool Clearable { get; set; } + private string? AdornmentText { get; set; } private bool IsRequired { get; set; } private bool IsDisabled { get; set; } private bool IsReadOnly { get; set; } private Adornment Adornment { get; set; } + private int _componentRenderedCounter = 0; + private IEnumerable SelectedMultiSelectValues { get @@ -411,6 +543,22 @@ set => Value = (T)value; } + private IEnumerable SelectedMultiSelectLongValues + { + get + { + if (Value is HashSet selectedValues) + { + return selectedValues; + } + else + { + throw new NotImplementedException(); + } + } + set => Value = (T)value; + } + private IEnumerable SelectedMultiSelectStringValues { get @@ -481,6 +629,33 @@ } } + private long? NullableLongValue + { + get + { + if (typeof(T) == typeof(long?) && Value != null) + { + return (long?)(object)Value; + } + if (Value == null) + { + return null; + } + throw new NotImplementedException(); + } + set + { + if (value != default) + { + Value = (T)(object)value; + } + else + { + Value = default; + } + } + } + private DateTime? DateValue { get @@ -539,17 +714,13 @@ } } - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override void OnAfterRender(bool firstRender) { - if (firstRender && IsNormalText() && !IsPassword) - { - await UpdateLineCount(false); - } + _componentRenderedCounter++; } protected override void OnParametersSet() { - TextAreaLines = TextAreaMinimumLines; if (For == default) { throw new ArgumentException("Expression body is null"); @@ -587,7 +758,7 @@ var postfixAttribute = propertyInfo.GetCustomAttributes(false).SingleOrDefault(); var prefixAttribute = propertyInfo.GetCustomAttributes(false).SingleOrDefault(); - + if (postfixAttribute != default) { @@ -612,30 +783,55 @@ else { Adornment = Adornment.None; - } + } + StateHasChanged(); } - protected override void OnInitialized() + private string GetIntMultiSelectionText(List selectedValues) { - + if (DisplayMultiSelectValues && selectedValues.Count > 0) + { + if (DropDownOptions != null) + { + try + { + return string.Join("; ", selectedValues.Select(x => DropDownOptions[Convert.ToInt32(x)])); + } + catch (Exception) + { + // ignored + } + } + } + + return GetMultiSelectionTextWithoutValues(selectedValues.Count, DropDownOptions?.Count); } - private string GetMultiSelectionText(List selectedValues) + private string GetLongMultiSelectionText(List selectedValues) { if (DisplayMultiSelectValues && selectedValues.Count > 0) { - if (DropDownOptions != null) + if (LongIdDropDownOptions != null) { try { - return string.Join("; ", selectedValues.Select(x => DropDownOptions[Convert.ToInt32(x)])); + return string.Join("; ", selectedValues.Select(x => LongIdDropDownOptions[Convert.ToInt64(x)])); } catch (Exception) { // ignored } } - else if(StringIdDropDownOptions != null) + } + + return GetMultiSelectionTextWithoutValues(selectedValues.Count, LongIdDropDownOptions?.Count); + } + + private string GetMultiSelectionText(List selectedValues) + { + if (DisplayMultiSelectValues && selectedValues.Count > 0) + { + if (StringIdDropDownOptions != null) { try { @@ -647,7 +843,13 @@ } } } - return $"{selectedValues.Count} item{(selectedValues.Count == 1 ? " has" : "s have")} been selected"; + return GetMultiSelectionTextWithoutValues(selectedValues.Count, StringIdDropDownOptions?.Count); + } + + private string GetMultiSelectionTextWithoutValues(int selectedValues, int? availableOptions) + { + var ofText = availableOptions == null ? string.Empty : $"/{availableOptions}"; + return $"{selectedValues}{ofText} item{(selectedValues == 1 ? " has" : "s have")} been selected"; } private void InvokeOnButtonClicked() diff --git a/TeslaSolarCharger/Client/Helper/Contracts/IJavaScriptWrapper.cs b/TeslaSolarCharger/Client/Helper/Contracts/IJavaScriptWrapper.cs new file mode 100644 index 000000000..047b7632c --- /dev/null +++ b/TeslaSolarCharger/Client/Helper/Contracts/IJavaScriptWrapper.cs @@ -0,0 +1,9 @@ +namespace Lysando.LabStorageV2.UiHelper.Wrapper.Contracts; + +public interface IJavaScriptWrapper +{ + Task SetFocusToElementById(string elementId); + Task RemoveFocusFromElementById(string elementId); + Task OpenUrlInNewTab(string url); + Task IsIosDevice(); +} \ No newline at end of file diff --git a/TeslaSolarCharger/Client/Helper/JavaScriptWrapper.cs b/TeslaSolarCharger/Client/Helper/JavaScriptWrapper.cs new file mode 100644 index 000000000..2cd6a7539 --- /dev/null +++ b/TeslaSolarCharger/Client/Helper/JavaScriptWrapper.cs @@ -0,0 +1,54 @@ +using Lysando.LabStorageV2.UiHelper.Wrapper.Contracts; +using Microsoft.JSInterop; + +namespace Lysando.LabStorageV2.UiHelper.Wrapper; + +public class JavaScriptWrapper(IJSRuntime jsRuntime) : IJavaScriptWrapper +{ + /// + /// Sets the focus to an element with a specific ID + /// + /// ID to set the focus on + /// Was the ID set successfully + public async Task SetFocusToElementById(string elementId) + { + try + { + return await jsRuntime.InvokeAsync("setFocus", elementId); + } + catch (Exception) + { + return false; + } + } + + public async Task RemoveFocusFromElementById(string elementId) + { + try + { + return await jsRuntime.InvokeAsync("removeFocus", elementId); + } + catch (Exception) + { + return false; + } + } + + public async Task OpenUrlInNewTab(string url) + { + await jsRuntime.InvokeVoidAsync("openInNewTab", url); + } + + public async Task IsIosDevice() + { + try + { + var device = await jsRuntime.InvokeAsync("detectDevice"); + return device == "iOS"; + } + catch (Exception) + { + return false; + } + } +} \ No newline at end of file diff --git a/TeslaSolarCharger/Client/Program.cs b/TeslaSolarCharger/Client/Program.cs index 8e8d173d0..6c2f2bbcb 100644 --- a/TeslaSolarCharger/Client/Program.cs +++ b/TeslaSolarCharger/Client/Program.cs @@ -1,3 +1,5 @@ +using Lysando.LabStorageV2.UiHelper.Wrapper; +using Lysando.LabStorageV2.UiHelper.Wrapper.Contracts; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using MudBlazor; @@ -21,6 +23,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSharedDependencies(); diff --git a/TeslaSolarCharger/Client/wwwroot/index.html b/TeslaSolarCharger/Client/wwwroot/index.html index edb707476..3301a3b53 100644 --- a/TeslaSolarCharger/Client/wwwroot/index.html +++ b/TeslaSolarCharger/Client/wwwroot/index.html @@ -37,6 +37,7 @@ + diff --git a/TeslaSolarCharger/Client/wwwroot/js/javaScriptWrapperFunctions.js b/TeslaSolarCharger/Client/wwwroot/js/javaScriptWrapperFunctions.js new file mode 100644 index 000000000..4843b48f8 --- /dev/null +++ b/TeslaSolarCharger/Client/wwwroot/js/javaScriptWrapperFunctions.js @@ -0,0 +1,30 @@ +function setFocus(elementId) { + const element = document.getElementById(elementId); + if (element) { + element.focus(); + return true; + } + return false; +} + +function removeFocus(elementId) { + const element = document.getElementById(elementId); + if (element) { + element.blur(); + return true; + } + return false; +} + +function openInNewTab(url) { + window.open(url, '_blank'); +} + +function detectDevice() { + var ua = navigator.userAgent || navigator.vendor || window.opera; + // iOS detection + if (/iPad|iPhone|iPod/.test(ua) && !window.MSStream) { + return "iOS"; + } + return "Other"; +}