From c3b267dfc440c1591ce5f3ba31b6d32cdcc6a810 Mon Sep 17 00:00:00 2001 From: stm Date: Fri, 20 Jan 2023 11:10:16 +0100 Subject: [PATCH] Progress - Categorizing variables and methods --- .../Annotations/CategoryAttribute.cs | 2 +- .../Components/AppBar/AppBar.razor.cs | 61 +- .../Autocomplete/Autocomplete.razor.cs | 1376 +++++++++-------- .../Components/Avatar/Avatar.razor.cs | 59 +- .../Components/Avatar/AvatarGroup.razor.cs | 124 +- .../Components/Avatar/AvatarKind.cs | 8 +- .../Components/Badge/Badge.razor.cs | 139 +- .../Breadcrumbs/BreadcrumbLink.razor.cs | 7 + .../Breadcrumbs/BreadcrumbSeparator.razor.cs | 2 + .../Breadcrumbs/Breadcrumbs.razor.cs | 70 +- .../BreakpointProvider.razor.cs | 37 +- .../Components/Button/Button.razor.cs | 28 +- .../Components/Button/ButtonBase.cs | 18 +- .../Components/Button/Fab.razor.cs | 63 +- .../Components/Button/IconButton.razor.cs | 44 +- .../Components/Button/ToggleIconButton.razor | 4 +- .../Button/ToggleIconButton.razor.cs | 60 +- .../ButtonGroup/ButtonGroup.razor.cs | 118 +- .../Components/Card/Card.razor.cs | 17 +- .../Components/Card/CardActions.razor.cs | 7 + .../Components/Card/CardContent.razor.cs | 7 + .../Components/Card/CardHeader.razor.cs | 17 +- .../Components/Card/CardMedia.razor.cs | 20 +- .../Components/Carousel/Carousel.razor.cs | 272 ++-- .../Components/Carousel/CarouselItem.razor.cs | 28 +- .../Components/Chart/Chart.razor.cs | 113 +- .../Components/Chart/Charts/Bar.razor.cs | 5 + .../Components/Chart/Charts/Donut.razor.cs | 7 + .../Components/Chart/Charts/Line.razor.cs | 8 + .../Components/Chart/Charts/Pie.razor.cs | 6 + .../Components/CheckBox/CheckBox.razor.cs | 215 +-- .../Components/Chip/Chip.razor.cs | 266 ++-- .../Components/ChipSet/ChipSet.razor.cs | 671 ++++---- .../Components/Collapse/Collapse.razor.cs | 96 +- .../ColorPicker/ColorPicker.razor.cs | 1158 +++++++------- .../Components/Container/Container.razor.cs | 3 - .../DatePicker/DatePickerBase.razor.cs | 517 ++++--- .../DatePicker/DateRangePicker.razor.cs | 594 +++---- .../Components/Input/Input.razor | 2 +- .../Components/Input/Input.razor.cs | 23 +- 40 files changed, 3289 insertions(+), 2983 deletions(-) diff --git a/src/Connected.Components/Annotations/CategoryAttribute.cs b/src/Connected.Components/Annotations/CategoryAttribute.cs index 063d1d3..ef7b589 100644 --- a/src/Connected.Components/Annotations/CategoryAttribute.cs +++ b/src/Connected.Components/Annotations/CategoryAttribute.cs @@ -126,7 +126,7 @@ public static class CategoryTypes public const string Common = "Common"; } - /// Used in: , all components inheriting from it, and . + /// Used in: , all components inheriting from it, and . public static class Button { public const string Behavior = "Behavior"; diff --git a/src/Connected.Components/Components/AppBar/AppBar.razor.cs b/src/Connected.Components/Components/AppBar/AppBar.razor.cs index 416b35a..1465e43 100644 --- a/src/Connected.Components/Components/AppBar/AppBar.razor.cs +++ b/src/Connected.Components/Components/AppBar/AppBar.razor.cs @@ -1,12 +1,25 @@ -using Connected.Extensions; -using Connected.Utilities; +using Connected.Utilities; using Microsoft.AspNetCore.Components; -using System.Security; namespace Connected.Components; public partial class AppBar : UIComponent { + + #region Event callbacks + #endregion + + #region Content placeholders + + /// + /// Child content of the component. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + #endregion + + #region Styling properties /// /// The classlist determining the toolbar render. /// @@ -18,6 +31,12 @@ public partial class AppBar : UIComponent .AddClass(ToolbarClassList); } } + /// + /// Class names for the nested toolbar. In case of several, separate by spaces. + /// + [Parameter] + public string? ToolbarClassList { get; set; } + /// /// The classlist determining the header render. @@ -32,19 +51,6 @@ public partial class AppBar : UIComponent .AddClass(HeaderClassList); } } - - /// - /// Child content of the component. - /// - [Parameter] - public RenderFragment? ChildContent { get; set; } - - /// - /// If true, appbar will be fixed. - /// - [Parameter] - public bool Fixed { get; set; } = true; - /// /// Class names for the AppBar header. In case of several, separate by spaces. /// @@ -52,14 +58,29 @@ public partial class AppBar : UIComponent public string? HeaderClassList { get; set; } /// - /// Class names for the nested toolbar. In case of several, separate by spaces. + /// Determines the vertical alignment of the AppBar. /// [Parameter] - public string? ToolbarClassList { get; set; } + public VerticalAlignment VerticalAlignment { get; set; } /// - /// Determines the vertical alignment of the AppBar. + /// If true, appbar will be fixed. /// [Parameter] - public VerticalAlignment VerticalAlignment { get; set; } + public bool Fixed { get; set; } = true; + + #endregion + + #region Lifecycle + #endregion + + + + + + + + + + } diff --git a/src/Connected.Components/Components/Autocomplete/Autocomplete.razor.cs b/src/Connected.Components/Components/Autocomplete/Autocomplete.razor.cs index 9d3eea9..f943830 100644 --- a/src/Connected.Components/Components/Autocomplete/Autocomplete.razor.cs +++ b/src/Connected.Components/Components/Autocomplete/Autocomplete.razor.cs @@ -6,687 +6,727 @@ namespace Connected.Components; public partial class Autocomplete : InputBase, IDisposable { - private Func? _toStringFunc; - private Task _currentSearchTask; - private CancellationTokenSource _cancellationTokenSrc; - private bool _isOpen; - private Timer _timer; - private T[] _items; - private int _selectedListItemIndex = 0; - private IList _enabledItemIndices = new List(); - private int _itemsReturned; //the number of items returned by the search function - int _elementKey = 0; - /// - /// This boolean will keep track if the clear function is called too keep the set text function to be called. - /// - private bool _isCleared; - private Input _elementReference; - /// - /// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker. - /// - private readonly string _componentId = Guid.NewGuid().ToString(); - - public Autocomplete() - { - Adornment = Adornment.End; - IconSize = Size.Medium; - } - - [Inject] - IScrollManager ScrollManager { get; set; } - /// - /// User class names for the popover, separated by space - /// - [Parameter] - public string PopoverClass { get; set; } - /// - /// Set the anchor origin point to determen where the popover will open from. - /// - [Parameter] - public Origin AnchorOrigin { get; set; } = Origin.BottomCenter; - /// - /// Sets the transform origin point for the popover. - /// - [Parameter] - public Origin TransformOrigin { get; set; } = Origin.TopCenter; - /// - /// If true, compact vertical padding will be applied to all Autocomplete items. - /// - [Parameter] - public bool Dense { get; set; } - /// - /// The Open Autocomplete Glyph - /// - [Parameter] - public string OpenIcon { get; set; } = Icons.Material.Filled.ArrowDropDown; - /// - /// The Close Autocomplete Glyph - /// - [Parameter] - public string CloseIcon { get; set; } = Icons.Material.Filled.ArrowDropUp; - /// - /// The maximum height of the Autocomplete when it is open. - /// - [Parameter] - public int MaxHeight { get; set; } = 300; - /// - /// Defines how values are displayed in the drop-down list - /// - [Parameter] - public Func? ToStringFunc - { - get => _toStringFunc; - set - { - if (_toStringFunc == value) - return; - - _toStringFunc = value; - - SetConverter(new LambdaConverter(_toStringFunc ?? (x => x?.ToString()), null)); - } - } - /// - /// Whether to show the progress indicator. - /// - [Parameter] - public bool ShowProgressIndicator { get; set; } = false; - /// - /// The color of the progress indicator. - /// - [Parameter] - public ThemeColor ProgressIndicatorColor { get; set; } = ThemeColor.Default; - private bool IsLoading => _currentSearchTask is not null && !_currentSearchTask.IsCompleted; - /// - /// Func that returns a list of items matching the typed text. Provides a cancellation token that - /// is marked as cancelled when the user changes the search text or selects a value from the list. - /// This can be used to cancel expensive asynchronous work occuring within the SearchFunc itself. - /// - [Parameter] - public Func>> SearchFuncWithCancel { get; set; } - /// - /// The SearchFunc returns a list of items matching the typed text - /// - [Parameter] - public Func>> SearchFunc { get; set; } - /// - /// Maximum items to display, defaults to 10. - /// A null value will display all items. - /// - [Parameter] - public int? MaxItems { get; set; } = 10; - /// - /// Minimum characters to initiate a search - /// - [Parameter] - public int MinCharacters { get; set; } = 0; - /// - /// Reset value if user deletes the text - /// - [Parameter] - public bool ResetValueOnEmptyText { get; set; } = false; - /// - /// If true, clicking the text field will select (highlight) its contents. - /// - [Parameter] - public bool SelectOnClick { get; set; } = true; - /// - /// Debounce interval in milliseconds. - /// - [Parameter] - public int DebounceInterval { get; set; } = 100; - /// - /// Optional presentation template for unselected items - /// - [Parameter] - public RenderFragment ItemTemplate { get; set; } - /// - /// Optional presentation template for the selected item - /// - [Parameter] - public RenderFragment ItemSelectedTemplate { get; set; } - /// - /// Optional presentation template for disabled item - /// - [Parameter] - public RenderFragment ItemDisabledTemplate { get; set; } - /// - /// Optional presentation template for when more items were returned from the Search function than the MaxItems limit - /// - [Parameter] - public RenderFragment MoreItemsTemplate { get; set; } - /// - /// Optional presentation template for when no items were returned from the Search function - /// - [Parameter] - public RenderFragment NoItemsTemplate { get; set; } - /// - /// Optional template for progress indicator - /// - [Parameter] - public RenderFragment ProgressIndicatorTemplate { get; set; } - /// - /// Optional template for showing progress indicator inside the popover - /// - [Parameter] - public RenderFragment ProgressIndicatorInPopoverTemplate { get; set; } - /// - /// On drop-down close override Text with selected Value. This makes it clear to the user - /// which list value is currently selected and disallows incomplete values in Text. - /// - [Parameter] - public bool CoerceText { get; set; } = true; - /// - /// If user input is not found by the search func and CoerceValue is set to true the user input - /// will be applied to the Value which allows to validate it and display an error message. - /// - [Parameter] - public bool CoerceValue { get; set; } - /// - /// Function to be invoked when checking whether an item should be disabled or not - /// - [Parameter] - public Func ItemDisabledFunc { get; set; } - /// - /// Returns the open state of the drop-down. - /// - public bool IsOpen - { - get => _isOpen; - // Note: the setter is protected because it was needed by a user who derived his own autocomplete from this class. - // Note: setting IsOpen will not open or close it. Use ToggleMenu() for that. - protected set - { - if (value == _isOpen) - return; - - _isOpen = value; - - IsOpenChanged.InvokeAsync(_isOpen).AndForget(); - } - } - /// - /// An event triggered when the state of IsOpen has changed - /// - [Parameter] - public EventCallback IsOpenChanged { get; set; } - /// - /// If true, the currently selected item from the drop-down (if it is open) is selected. - /// - [Parameter] - public bool SelectValueOnTab { get; set; } = false; - /// - /// Show clear button. - /// - [Parameter] - public bool Clearable { get; set; } = false; - /// - /// Button click event for clear button. Called after text and value has been cleared. - /// - [Parameter] - public EventCallback OnClearButtonClick { get; set; } - - private string CurrentIcon => !string.IsNullOrWhiteSpace(AdornmentIcon) ? AdornmentIcon : _isOpen ? CloseIcon : OpenIcon; - - protected string ClassList() - { - return new CssBuilder("select") - .Build(); - } - - protected string AutocompleteClassList() - { - return new CssBuilder("select") - .AddClass("autocomplete") - .AddClass("width-full", FullWidth) - .AddClass("autocomplete--with-progress", ShowProgressIndicator && IsLoading) - .Build(); - } - - protected string CircularProgressClassList() - { - return new CssBuilder("progress-indicator-circular") - .AddClass("progress-indicator-circular--with-adornment", Adornment == Adornment.End) - .Build(); - } - - public async Task SelectOption(T value) - { - await SetValueAsync(value); - - if (_items is not null) - _selectedListItemIndex = Array.IndexOf(_items, value); - - var optionText = GetItemString(value); - - if (!_isCleared) - await SetTextAsync(optionText, false); - - _timer?.Dispose(); - - IsOpen = false; - - BeginValidate(); - - if (!_isCleared) - _elementReference?.SetText(optionText); - - _elementReference?.FocusAsync().AndForget(); - - StateHasChanged(); - } - /// - /// Toggle the menu (if not disabled or not readonly, and is opened). - /// - public async Task ToggleMenu() - { - if ((Disabled || ReadOnly) && !IsOpen) - return; - - await ChangeMenu(!IsOpen); - } - - private async Task ChangeMenu(bool open) - { - if (open) - { - if (SelectOnClick) - await _elementReference.SelectAsync(); - - await OnSearchAsync(); - } - else - { - _timer?.Dispose(); - - RestoreScrollPosition(); - - await CoerceTextToValue(); - - IsOpen = false; - - StateHasChanged(); - } - } - - protected override void OnInitialized() - { - var text = GetItemString(Value); - - if (!string.IsNullOrWhiteSpace(text)) - Text = text; - } - - protected override void OnAfterRender(bool firstRender) - { - _isCleared = false; - - base.OnAfterRender(firstRender); - } - - protected override Task UpdateTextPropertyAsync(bool updateValue) - { - _timer?.Dispose(); - // This keeps the text from being set when clear() was called - if (_isCleared) - return Task.CompletedTask; - - return base.UpdateTextPropertyAsync(updateValue); - } - - protected override async Task UpdateValuePropertyAsync(bool updateText) - { - _timer?.Dispose(); - - if (ResetValueOnEmptyText && string.IsNullOrWhiteSpace(Text)) - await SetValueAsync(default, updateText); - - if (DebounceInterval <= 0) - await OnSearchAsync(); - else - _timer = new Timer(OnTimerComplete, null, DebounceInterval, Timeout.Infinite); - } - - private void OnTimerComplete(object stateInfo) - { - InvokeAsync(OnSearchAsync); - } - private void CancelToken() - { - try - { - _cancellationTokenSrc?.Cancel(); - } - catch - { - } - - _cancellationTokenSrc = new CancellationTokenSource(); - } - /// - /// This async method needs to return a task and be awaited in order for - /// unit tests that trigger this method to work correctly. - /// - private async Task OnSearchAsync() - { - if (MinCharacters > 0 && (string.IsNullOrWhiteSpace(Text) || Text.Length < MinCharacters)) - { - IsOpen = false; - StateHasChanged(); - return; - } - - IEnumerable searchedItems = Array.Empty(); - CancelToken(); - try - { - if (ProgressIndicatorInPopoverTemplate is not null) - IsOpen = true; - - var searchTask = SearchFuncWithCancel is not null ? SearchFuncWithCancel(Text, _cancellationTokenSrc.Token) : SearchFunc(Text); - - _currentSearchTask = searchTask; + #region Variables + + [Inject] + IScrollManager ScrollManager { get; set; } + + private Func? _toStringFunc; + private Task _currentSearchTask; + private CancellationTokenSource _cancellationTokenSrc; + private bool _isOpen; + private Timer _timer; + private T[] _items; + private int _selectedListItemIndex = 0; + private IList _enabledItemIndices = new List(); + private int _itemsReturned; //the number of items returned by the search function + int _elementKey = 0; + /// + /// This boolean will keep track if the clear function is called too keep the set text function to be called. + /// + private bool _isCleared; + private Input _elementReference; + /// + /// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker. + /// + private readonly string _componentId = Guid.NewGuid().ToString(); + + #endregion + + #region Event callbacks + + /// + /// Function to be invoked when checking whether an item should be disabled or not + /// + [Parameter] + public Func ItemDisabledFunc { get; set; } + + /// + /// If true, the currently selected item from the drop-down (if it is open) is selected. + /// + [Parameter] + public bool SelectValueOnTab { get; set; } = false; + + /// + /// Func that returns a list of items matching the typed text. Provides a cancellation token that + /// is marked as cancelled when the user changes the search text or selects a value from the list. + /// This can be used to cancel expensive asynchronous work occuring within the SearchFunc itself. + /// + [Parameter] + public Func>> SearchFuncWithCancel { get; set; } + /// + /// The SearchFunc returns a list of items matching the typed text + /// + [Parameter] + public Func>> SearchFunc { get; set; } + + + // + /// An event triggered when the state of IsOpen has changed + /// + [Parameter] + public EventCallback IsOpenChanged { get; set; } + + /// + /// Button click event for clear button. Called after text and value has been cleared. + /// + [Parameter] + public EventCallback OnClearButtonClick { get; set; } + + internal virtual async Task OnInputKeyUp(KeyboardEventArgs args) + { + switch (args.Key) + { + case "Enter": + case "NumpadEnter": + if (!IsOpen) + await ToggleMenu(); + else + await OnEnterKey(); + + break; + case "ArrowDown": + if (!IsOpen) + await ToggleMenu(); + else + { + var increment = _enabledItemIndices.ElementAtOrDefault(_enabledItemIndices.IndexOf(_selectedListItemIndex) + 1) - _selectedListItemIndex; - StateHasChanged(); + await SelectNextItem(increment < 0 ? 1 : increment); + } - searchedItems = await searchTask ?? Array.Empty(); - } - catch (TaskCanceledException) - { - } - catch (OperationCanceledException) - { - } - catch (Exception e) - { - Console.WriteLine($"The search function failed to return results: {e.Message}"); - } + break; + case "ArrowUp": + if (args.AltKey) + await ChangeMenu(false); + else if (!IsOpen) + await ToggleMenu(); + else + { + var decrement = _selectedListItemIndex - _enabledItemIndices.ElementAtOrDefault(_enabledItemIndices.IndexOf(_selectedListItemIndex) - 1); + await SelectNextItem(-(decrement < 0 ? 1 : decrement)); + } - _itemsReturned = searchedItems.Count(); + break; + case "Escape": + await ChangeMenu(false); - if (MaxItems.HasValue) - searchedItems = searchedItems.Take(MaxItems.Value); + break; + case "Tab": + await Task.Delay(1); - _items = searchedItems.ToArray(); - _enabledItemIndices = _items.Select((item, idx) => (item, idx)).Where(tuple => ItemDisabledFunc?.Invoke(tuple.item) != true).Select(tuple => tuple.idx).ToList(); - _selectedListItemIndex = _enabledItemIndices.Any() ? _enabledItemIndices.First() : -1; + if (!IsOpen) + return; - IsOpen = true; + if (SelectValueOnTab) + await OnEnterKey(); + else + await ToggleMenu(); - if (_items?.Length == 0) - { - await CoerceValueToText(); + break; + case "Backspace": + if (args.CtrlKey && args.ShiftKey) + Reset(); + break; + } - StateHasChanged(); + base.InvokeKeyUp(args); + } + internal virtual async Task OnInputKeyDown(KeyboardEventArgs args) + { + switch (args.Key) + { + case "Tab": + // NOTE: We need to catch Tab in Keydown because a tab will move focus to the next element and thus + // in OnInputKeyUp we'd never get the tab key + if (!IsOpen) + return; + if (SelectValueOnTab) + await OnEnterKey(); + else + IsOpen = false; + break; + } + } + + public async Task SelectOption(T value) + { + await SetValueAsync(value); + + if (_items is not null) + _selectedListItemIndex = Array.IndexOf(_items, value); + + var optionText = GetItemString(value); + + if (!_isCleared) + await SetTextAsync(optionText, false); + + _timer?.Dispose(); + + IsOpen = false; + + BeginValidate(); + + if (!_isCleared) + _elementReference?.SetText(optionText); + + _elementReference?.FocusAsync().AndForget(); + + StateHasChanged(); + } + /// + /// Toggle the menu (if not disabled or not readonly, and is opened). + /// + public async Task ToggleMenu() + { + if ((Disabled || ReadOnly) && !IsOpen) + return; + + await ChangeMenu(!IsOpen); + } + + private async Task ChangeMenu(bool open) + { + if (open) + { + if (SelectOnClick) + await _elementReference.SelectAsync(); + + await OnSearchAsync(); + } + else + { + _timer?.Dispose(); + + RestoreScrollPosition(); + + await CoerceTextToValue(); + + IsOpen = false; + + StateHasChanged(); + } + } + + + + protected override Task UpdateTextPropertyAsync(bool updateValue) + { + _timer?.Dispose(); + // This keeps the text from being set when clear() was called + if (_isCleared) + return Task.CompletedTask; + + return base.UpdateTextPropertyAsync(updateValue); + } + + protected override async Task UpdateValuePropertyAsync(bool updateText) + { + _timer?.Dispose(); + + if (ResetValueOnEmptyText && string.IsNullOrWhiteSpace(Text)) + await SetValueAsync(default, updateText); + + if (DebounceInterval <= 0) + await OnSearchAsync(); + else + _timer = new Timer(OnTimerComplete, null, DebounceInterval, Timeout.Infinite); + } + + private void OnTimerComplete(object stateInfo) + { + InvokeAsync(OnSearchAsync); + } + private void CancelToken() + { + try + { + _cancellationTokenSrc?.Cancel(); + } + catch + { + } + + _cancellationTokenSrc = new CancellationTokenSource(); + } + /// + /// This async method needs to return a task and be awaited in order for + /// unit tests that trigger this method to work correctly. + /// + private async Task OnSearchAsync() + { + if (MinCharacters > 0 && (string.IsNullOrWhiteSpace(Text) || Text.Length < MinCharacters)) + { + IsOpen = false; + StateHasChanged(); + return; + } + + IEnumerable searchedItems = Array.Empty(); + CancelToken(); + + try + { + if (ProgressIndicatorInPopoverTemplate is not null) + IsOpen = true; + + var searchTask = SearchFuncWithCancel is not null ? SearchFuncWithCancel(Text, _cancellationTokenSrc.Token) : SearchFunc(Text); + + _currentSearchTask = searchTask; + + StateHasChanged(); + + searchedItems = await searchTask ?? Array.Empty(); + } + catch (TaskCanceledException) + { + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + Console.WriteLine($"The search function failed to return results: {e.Message}"); + } + + _itemsReturned = searchedItems.Count(); + + if (MaxItems.HasValue) + searchedItems = searchedItems.Take(MaxItems.Value); + + _items = searchedItems.ToArray(); + _enabledItemIndices = _items.Select((item, idx) => (item, idx)).Where(tuple => ItemDisabledFunc?.Invoke(tuple.item) != true).Select(tuple => tuple.idx).ToList(); + _selectedListItemIndex = _enabledItemIndices.Any() ? _enabledItemIndices.First() : -1; + + IsOpen = true; + + if (_items?.Length == 0) + { + await CoerceValueToText(); + + StateHasChanged(); + + return; + } + + StateHasChanged(); + } + + /// + /// Clears the autocomplete's text + /// + public async Task Clear() + { + _isCleared = true; + IsOpen = false; + + await SetTextAsync(string.Empty, updateValue: false); + await CoerceValueToText(); + + if (_elementReference is not null) + await _elementReference.SetText(string.Empty); + + _timer?.Dispose(); + + StateHasChanged(); + } + + protected override async void ResetValue() + { + await Clear(); + base.ResetValue(); + } + + private string GetItemString(T item) + { + if (item is null) + return string.Empty; + try + { + return Converter.Convert(item); + } + catch (NullReferenceException) { } + + return "null"; + } + + private ValueTask SelectNextItem(int increment) + { + if (increment == 0 || _items is null || !_items.Any() || !_enabledItemIndices.Any()) + return ValueTask.CompletedTask; + // if we are at the end, or the beginning we just do an rollover + _selectedListItemIndex = Math.Clamp(value: (10 * _items.Length + _selectedListItemIndex + increment) % _items.Length, min: 0, max: _items.Length - 1); + + return ScrollToListItem(_selectedListItemIndex); + } + /// + /// Scroll to a specific item index in the Autocomplete list of items. + /// + /// the index to scroll to + public ValueTask ScrollToListItem(int index) + { + var id = GetListItemId(index); + //id of the scrolled element + return ScrollManager.ScrollToListItemAsync(id); + } + /* + * This restores the scroll position after closing the menu and element being 0 + */ + private void RestoreScrollPosition() + { + if (_selectedListItemIndex != 0) + return; + + ScrollManager.ScrollToListItemAsync(GetListItemId(0)); + } + + private string GetListItemId(in int index) + { + return $"{_componentId}_item{index}"; + } + + internal Task OnEnterKey() + { + if (!IsOpen) + return Task.CompletedTask; + + if (_items is null || !_items.Any()) + return Task.CompletedTask; + + if (_selectedListItemIndex >= 0 && _selectedListItemIndex < _items.Length) + return SelectOption(_items[_selectedListItemIndex]); + + return Task.CompletedTask; + } + + private Task OnInputBlurred(FocusEventArgs args) + { + OnBlur.InvokeAsync(args); + + return Task.CompletedTask; + // we should not validate on blur in autocomplete, because the user needs to click out of the input to select a value, + // resulting in a premature validation. thus, don't call base + //base.OnBlurred(args); + } + + private Task CoerceTextToValue() + { + if (!CoerceText) + return Task.CompletedTask; + + _timer?.Dispose(); + + var text = Value is null ? null : GetItemString(Value); + /* + * Don't update the value to prevent the popover from opening again after coercion + */ + if (text != Text) + return SetTextAsync(text, updateValue: false); + + return Task.CompletedTask; + } + + private Task CoerceValueToText() + { + if (!CoerceValue) + return Task.CompletedTask; + + _timer?.Dispose(); + + var value = Converter.ConvertBack(Text); + + return SetValueAsync(value, updateText: false); + } + + + /// + /// Focus the input in the Autocomplete component. + /// + public override ValueTask FocusAsync() + { + return _elementReference.FocusAsync(); + } + /// + /// Blur from the input in the Autocomplete component. + /// + public override ValueTask BlurAsync() + { + return _elementReference.BlurAsync(); + } + + /// + /// Select all text within the Autocomplete input. + /// + public override ValueTask SelectAsync() + { + return _elementReference.SelectAsync(); + } + + /// + /// Select all text within the Autocomplete input and aligns its start and end points to the text content of the current input. + /// + public override ValueTask SelectRangeAsync(int pos1, int pos2) + { + return _elementReference.SelectRangeAsync(pos1, pos2); + } + + private async Task OnTextChanged(string text) + { + await TextChanged.InvokeAsync(); + + if (text is null) + return; + + await SetTextAsync(text, true); + } + + private async Task ListItemOnClick(T item) + { + await SelectOption(item); + } + + #endregion + + #region Content placeholders + + + + #endregion + + #region Styling properties + + /// + /// Show clear button. + /// + [Parameter] + public bool Clearable { get; set; } = false; + private string CurrentIcon => !string.IsNullOrWhiteSpace(AdornmentIcon) ? AdornmentIcon : _isOpen ? CloseIcon : OpenIcon; + + /// + /// Returns the open state of the drop-down. + /// + public bool IsOpen + { + get => _isOpen; + // Note: the setter is protected because it was needed by a user who derived his own autocomplete from this class. + // Note: setting IsOpen will not open or close it. Use ToggleMenu() for that. + protected set + { + if (value == _isOpen) return; - } - - StateHasChanged(); - } - - /// - /// Clears the autocomplete's text - /// - public async Task Clear() - { - _isCleared = true; - IsOpen = false; - - await SetTextAsync(string.Empty, updateValue: false); - await CoerceValueToText(); - - if (_elementReference is not null) - await _elementReference.SetText(string.Empty); - - _timer?.Dispose(); - - StateHasChanged(); - } - - protected override async void ResetValue() - { - await Clear(); - base.ResetValue(); - } - - private string GetItemString(T item) - { - if (item is null) - return string.Empty; - try - { - return Converter.Convert(item); - } - catch (NullReferenceException) { } - - return "null"; - } - - internal virtual async Task OnInputKeyDown(KeyboardEventArgs args) - { - switch (args.Key) - { - case "Tab": - // NOTE: We need to catch Tab in Keydown because a tab will move focus to the next element and thus - // in OnInputKeyUp we'd never get the tab key - if (!IsOpen) - return; - if (SelectValueOnTab) - await OnEnterKey(); - else - IsOpen = false; - break; - } - } - - internal virtual async Task OnInputKeyUp(KeyboardEventArgs args) - { - switch (args.Key) - { - case "Enter": - case "NumpadEnter": - if (!IsOpen) - await ToggleMenu(); - else - await OnEnterKey(); - - break; - case "ArrowDown": - if (!IsOpen) - await ToggleMenu(); - else - { - var increment = _enabledItemIndices.ElementAtOrDefault(_enabledItemIndices.IndexOf(_selectedListItemIndex) + 1) - _selectedListItemIndex; - - await SelectNextItem(increment < 0 ? 1 : increment); - } - - break; - case "ArrowUp": - if (args.AltKey) - await ChangeMenu(false); - else if (!IsOpen) - await ToggleMenu(); - else - { - var decrement = _selectedListItemIndex - _enabledItemIndices.ElementAtOrDefault(_enabledItemIndices.IndexOf(_selectedListItemIndex) - 1); - await SelectNextItem(-(decrement < 0 ? 1 : decrement)); - } - - break; - case "Escape": - await ChangeMenu(false); - - break; - case "Tab": - await Task.Delay(1); - - if (!IsOpen) - return; - - if (SelectValueOnTab) - await OnEnterKey(); - else - await ToggleMenu(); - - break; - case "Backspace": - if (args.CtrlKey && args.ShiftKey) - Reset(); - break; - } - - base.InvokeKeyUp(args); - } - - private ValueTask SelectNextItem(int increment) - { - if (increment == 0 || _items is null || !_items.Any() || !_enabledItemIndices.Any()) - return ValueTask.CompletedTask; - // if we are at the end, or the beginning we just do an rollover - _selectedListItemIndex = Math.Clamp(value: (10 * _items.Length + _selectedListItemIndex + increment) % _items.Length, min: 0, max: _items.Length - 1); - - return ScrollToListItem(_selectedListItemIndex); - } - /// - /// Scroll to a specific item index in the Autocomplete list of items. - /// - /// the index to scroll to - public ValueTask ScrollToListItem(int index) - { - var id = GetListItemId(index); - //id of the scrolled element - return ScrollManager.ScrollToListItemAsync(id); - } - /* - * This restores the scroll position after closing the menu and element being 0 - */ - private void RestoreScrollPosition() - { - if (_selectedListItemIndex != 0) - return; - - ScrollManager.ScrollToListItemAsync(GetListItemId(0)); - } - - private string GetListItemId(in int index) - { - return $"{_componentId}_item{index}"; - } - - internal Task OnEnterKey() - { - if (!IsOpen) - return Task.CompletedTask; - - if (_items is null || !_items.Any()) - return Task.CompletedTask; - if (_selectedListItemIndex >= 0 && _selectedListItemIndex < _items.Length) - return SelectOption(_items[_selectedListItemIndex]); - - return Task.CompletedTask; - } - - private Task OnInputBlurred(FocusEventArgs args) - { - OnBlur.InvokeAsync(args); - - return Task.CompletedTask; - // we should not validate on blur in autocomplete, because the user needs to click out of the input to select a value, - // resulting in a premature validation. thus, don't call base - //base.OnBlurred(args); - } - - private Task CoerceTextToValue() - { - if (!CoerceText) - return Task.CompletedTask; - - _timer?.Dispose(); - - var text = Value is null ? null : GetItemString(Value); - /* - * Don't update the value to prevent the popover from opening again after coercion - */ - if (text != Text) - return SetTextAsync(text, updateValue: false); - - return Task.CompletedTask; - } - - private Task CoerceValueToText() - { - if (!CoerceValue) - return Task.CompletedTask; - - _timer?.Dispose(); - - var value = Converter.ConvertBack(Text); - - return SetValueAsync(value, updateText: false); - } - - protected override void Dispose(bool disposing) - { - _timer?.Dispose(); - - if (_cancellationTokenSrc is not null) - { - try - { - _cancellationTokenSrc.Dispose(); - } - catch { } - } - - base.Dispose(disposing); - } - /// - /// Focus the input in the Autocomplete component. - /// - public override ValueTask FocusAsync() - { - return _elementReference.FocusAsync(); - } - /// - /// Blur from the input in the Autocomplete component. - /// - public override ValueTask BlurAsync() - { - return _elementReference.BlurAsync(); - } - - /// - /// Select all text within the Autocomplete input. - /// - public override ValueTask SelectAsync() - { - return _elementReference.SelectAsync(); - } - - /// - /// Select all text within the Autocomplete input and aligns its start and end points to the text content of the current input. - /// - public override ValueTask SelectRangeAsync(int pos1, int pos2) - { - return _elementReference.SelectRangeAsync(pos1, pos2); - } - - private async Task OnTextChanged(string text) - { - await TextChanged.InvokeAsync(); - - if (text is null) + _isOpen = value; + + IsOpenChanged.InvokeAsync(_isOpen).AndForget(); + } + } + + /// + /// User class names for the popover, separated by space + /// + [Parameter] + public string PopoverClass { get; set; } + /// + /// Set the anchor origin point to determen where the popover will open from. + /// + [Parameter] + public Origin AnchorOrigin { get; set; } = Origin.BottomCenter; + /// + /// Sets the transform origin point for the popover. + /// + [Parameter] + public Origin TransformOrigin { get; set; } = Origin.TopCenter; + /// + /// If true, compact vertical padding will be applied to all Autocomplete items. + /// + [Parameter] + public bool Dense { get; set; } + /// + /// The Open Autocomplete Glyph + /// + [Parameter] + public string OpenIcon { get; set; } = Icons.Material.Filled.ArrowDropDown; + /// + /// The Close Autocomplete Glyph + /// + [Parameter] + public string CloseIcon { get; set; } = Icons.Material.Filled.ArrowDropUp; + /// + /// The maximum height of the Autocomplete when it is open. + /// + [Parameter] + public int MaxHeight { get; set; } = 300; + /// + /// Defines how values are displayed in the drop-down list + /// + [Parameter] + public Func? ToStringFunc + { + get => _toStringFunc; + set + { + if (_toStringFunc == value) return; - await SetTextAsync(text, true); - } + _toStringFunc = value; + + SetConverter(new LambdaConverter(_toStringFunc ?? (x => x?.ToString()), null)); + } + } + /// + /// Whether to show the progress indicator. + /// + [Parameter] + public bool ShowProgressIndicator { get; set; } = false; + /// + /// The color of the progress indicator. + /// + [Parameter] + public ThemeColor ProgressIndicatorColor { get; set; } = ThemeColor.Default; + private bool IsLoading => _currentSearchTask is not null && !_currentSearchTask.IsCompleted; + + /// + /// Maximum items to display, defaults to 10. + /// A null value will display all items. + /// + [Parameter] + public int? MaxItems { get; set; } = 10; + /// + /// Minimum characters to initiate a search + /// + [Parameter] + public int MinCharacters { get; set; } = 0; + /// + /// Reset value if user deletes the text + /// + [Parameter] + public bool ResetValueOnEmptyText { get; set; } = false; + /// + /// If true, clicking the text field will select (highlight) its contents. + /// + [Parameter] + public bool SelectOnClick { get; set; } = true; + + protected string ClassList() + { + return new CssBuilder("select") + .Build(); + } + + protected string AutocompleteClassList() + { + return new CssBuilder("select") + .AddClass("autocomplete") + .AddClass("width-full", FullWidth) + .AddClass("autocomplete--with-progress", ShowProgressIndicator && IsLoading) + .Build(); + } + + protected string CircularProgressClassList() + { + return new CssBuilder("progress-indicator-circular") + .AddClass("progress-indicator-circular--with-adornment", Adornment == Adornment.End) + .Build(); + } + + /// + /// Debounce interval in milliseconds. + /// + [Parameter] + public int DebounceInterval { get; set; } = 100; + /// + /// Optional presentation template for unselected items + /// + [Parameter] + public RenderFragment ItemTemplate { get; set; } + /// + /// Optional presentation template for the selected item + /// + [Parameter] + public RenderFragment ItemSelectedTemplate { get; set; } + /// + /// Optional presentation template for disabled item + /// + [Parameter] + public RenderFragment ItemDisabledTemplate { get; set; } + /// + /// Optional presentation template for when more items were returned from the Search function than the MaxItems limit + /// + [Parameter] + public RenderFragment MoreItemsTemplate { get; set; } + /// + /// Optional presentation template for when no items were returned from the Search function + /// + [Parameter] + public RenderFragment NoItemsTemplate { get; set; } + /// + /// Optional template for progress indicator + /// + [Parameter] + public RenderFragment ProgressIndicatorTemplate { get; set; } + /// + /// Optional template for showing progress indicator inside the popover + /// + [Parameter] + public RenderFragment ProgressIndicatorInPopoverTemplate { get; set; } + /// + /// On drop-down close override Text with selected Value. This makes it clear to the user + /// which list value is currently selected and disallows incomplete values in Text. + /// + [Parameter] + public bool CoerceText { get; set; } = true; + /// + /// If user input is not found by the search func and CoerceValue is set to true the user input + /// will be applied to the Value which allows to validate it and display an error message. + /// + [Parameter] + public bool CoerceValue { get; set; } + + #endregion Styling + + #region Lifecycle + /// + /// Constructor + /// + public Autocomplete() + { + Adornment = Adornment.End; + IconSize = Size.Medium; + } + + protected override void OnInitialized() + { + var text = GetItemString(Value); + + if (!string.IsNullOrWhiteSpace(text)) + Text = text; + } + + protected override void OnAfterRender(bool firstRender) + { + _isCleared = false; + + base.OnAfterRender(firstRender); + } + + protected override void Dispose(bool disposing) + { + _timer?.Dispose(); + + if (_cancellationTokenSrc is not null) + { + try + { + _cancellationTokenSrc.Dispose(); + } + catch { } + } + + base.Dispose(disposing); + } + + #endregion - private async Task ListItemOnClick(T item) - { - await SelectOption(item); - } } diff --git a/src/Connected.Components/Components/Avatar/Avatar.razor.cs b/src/Connected.Components/Components/Avatar/Avatar.razor.cs index d42775c..7137688 100644 --- a/src/Connected.Components/Components/Avatar/Avatar.razor.cs +++ b/src/Connected.Components/Components/Avatar/Avatar.razor.cs @@ -7,9 +7,43 @@ namespace Connected.Components; partial class Avatar : UIComponent, IDisposable { + + #region Variables + #endregion + + #region Events + + internal void ForceRedraw() => StateHasChanged(); + + #endregion + + #region Content + + /// + /// Child content of the component. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Link to image, if set a image will be displayed instead of text. + /// + [Parameter] + public string? Image { get; set; } + + /// + /// If set (and Image is also set), will add an alt property to the img element + /// + [Parameter] + public string? ImageAltText { get; set; } + [CascadingParameter] protected AvatarGroup? AvatarGroup { get; set; } + #endregion + + #region Styling + protected CssBuilder CompiledClassList { get @@ -41,18 +75,6 @@ partial class Avatar : UIComponent, IDisposable [Parameter] public string? ClassList { get; set; } - /// - /// If set (and Image is also set), will add an alt property to the img element - /// - [Parameter] - public string? ImageAltText { get; set; } - - /// - /// Child content of the component. - /// - [Parameter] - public RenderFragment? ChildContent { get; set; } - /// /// The color of the component. It supports the theme colors. /// @@ -65,12 +87,6 @@ partial class Avatar : UIComponent, IDisposable [Parameter] public int Elevation { set; get; } = 0; - /// - /// Link to image, if set a image will be displayed instead of text. - /// - [Parameter] - public string? Image { get; set; } - /// /// Sets the avatar appearance /// @@ -88,6 +104,10 @@ partial class Avatar : UIComponent, IDisposable [Parameter] public Variant Variant { get; set; } = Variant.Filled; + #endregion + + #region Lifecycle + protected override void OnInitialized() { base.OnInitialized(); @@ -97,5 +117,6 @@ partial class Avatar : UIComponent, IDisposable public void Dispose() => AvatarGroup?.RemoveAvatar(this); - internal void ForceRedraw() => StateHasChanged(); + #endregion + } diff --git a/src/Connected.Components/Components/Avatar/AvatarGroup.razor.cs b/src/Connected.Components/Components/Avatar/AvatarGroup.razor.cs index 48e1fd4..0cdef1b 100644 --- a/src/Connected.Components/Components/Avatar/AvatarGroup.razor.cs +++ b/src/Connected.Components/Components/Avatar/AvatarGroup.razor.cs @@ -7,10 +7,57 @@ namespace Connected.Components; partial class AvatarGroup : UIComponent { + + #region Variables + private bool _childrenNeedUpdates = false; private int _spacing = 3; private List _avatars = new(); + #endregion + + #region Event callbacks + #endregion + + #region Content + + /// + /// Child content of the component. + /// + [Parameter] + [Category(CategoryTypes.AvatarGroup.Behavior)] + public RenderFragment ChildContent { get; set; } + + internal void AddAvatar(Avatar avatar) + { + _avatars.Add(avatar); + StateHasChanged(); + } + + internal void RemoveAvatar(Avatar avatar) + { + _avatars.Remove(avatar); + } + + internal bool MaxGroupReached(Avatar avatar) + { + if (_avatars.IndexOf(avatar) < Max) + return true; + else + return false; + } + + #endregion + + #region Styling properties + + + internal CssBuilder GetAvatarSpacing() => new CssBuilder() + .AddClass($"ms-n{Spacing}"); + + internal StyleBuilder GetAvatarZindex(Avatar avatar) => new StyleBuilder() + .AddStyle("z-index", $"{_avatars.Count - _avatars.IndexOf(avatar)}"); + private CssBuilder CompiledClassList { get @@ -21,6 +68,11 @@ partial class AvatarGroup : UIComponent .AddClass(ClassList); } } + /// + /// A space separated list of class names, added on top of the default class list. + /// + [Parameter] + public string? ClassList { get; set; } private CssBuilder CompiledMaxAvatarClassList { @@ -31,30 +83,12 @@ partial class AvatarGroup : UIComponent .AddClass(MaxAvatarClass); } } - /// - /// Spacing between avatars where 0 is none and 16 max. - /// - [Parameter] - public int Spacing - { - get => _spacing; - set - { - if (value != _spacing) - { - _spacing = value; - _childrenNeedUpdates = true; - } - } - } - - /// - /// A space separated list of class names, added on top of the default class list. + /// Custom class/classes for MaxAvatar /// [Parameter] - public string? ClassList { get; set; } - + [Category(CategoryTypes.AvatarGroup.Appearance)] + public string MaxAvatarClass { get; set; } /// /// Outlines the grouped avatars to distinguish them, useful when avatars are the same color or uses images. /// @@ -132,44 +166,25 @@ partial class AvatarGroup : UIComponent } /// - /// Custom class/classes for MaxAvatar - /// - [Parameter] - [Category(CategoryTypes.AvatarGroup.Appearance)] - public string MaxAvatarClass { get; set; } - - /// - /// Child content of the component. + /// Spacing between avatars where 0 is none and 16 max. /// [Parameter] - [Category(CategoryTypes.AvatarGroup.Behavior)] - public RenderFragment ChildContent { get; set; } - - internal void AddAvatar(Avatar avatar) - { - _avatars.Add(avatar); - StateHasChanged(); - } - - internal void RemoveAvatar(Avatar avatar) + public int Spacing { - _avatars.Remove(avatar); + get => _spacing; + set + { + if (value != _spacing) + { + _spacing = value; + _childrenNeedUpdates = true; + } + } } - internal CssBuilder GetAvatarSpacing() => new CssBuilder() - .AddClass($"ms-n{Spacing}"); - - internal StyleBuilder GetAvatarZindex(Avatar avatar) => new StyleBuilder() - .AddStyle("z-index", $"{_avatars.Count - _avatars.IndexOf(avatar)}"); - - internal bool MaxGroupReached(Avatar avatar) - { - if (_avatars.IndexOf(avatar) < Max) - return true; - else - return false; - } + #endregion + #region Lifecycle protected override void OnParametersSet() { base.OnParametersSet(); @@ -184,4 +199,7 @@ partial class AvatarGroup : UIComponent _childrenNeedUpdates = false; } } + + #endregion + } diff --git a/src/Connected.Components/Components/Avatar/AvatarKind.cs b/src/Connected.Components/Components/Avatar/AvatarKind.cs index e0d3195..2be8175 100644 --- a/src/Connected.Components/Components/Avatar/AvatarKind.cs +++ b/src/Connected.Components/Components/Avatar/AvatarKind.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Connected.Components; +namespace Connected.Components; public enum AvatarKind { Undefined = 0, diff --git a/src/Connected.Components/Components/Badge/Badge.razor.cs b/src/Connected.Components/Components/Badge/Badge.razor.cs index 7ee9c51..92ce88d 100644 --- a/src/Connected.Components/Components/Badge/Badge.razor.cs +++ b/src/Connected.Components/Components/Badge/Badge.razor.cs @@ -9,6 +9,72 @@ namespace Connected.Components; public partial class Badge : UIComponent { + + #region Variables + #endregion + + #region Events + + /// + /// Button click event if set. + /// + [Parameter] public EventCallback OnClick { get; set; } + + + + internal Task HandleBadgeClick(MouseEventArgs e) + { + if (OnClick.HasDelegate) + return OnClick.InvokeAsync(e); + + return Task.CompletedTask; + } + + #endregion + + #region Content + + private string _content; + + /// + /// Sets the Glyph to use in the badge. + /// + [Parameter] + [Category(CategoryTypes.Badge.Behavior)] + public string Icon { get; set; } + + /// + /// Max value to show when content is integer type. + /// + [Parameter] + [Category(CategoryTypes.Badge.Behavior)] + public int Max { get; set; } = 99; + + /// + /// Content you want inside the badge. Supported types are string and integer. + /// + [Parameter] + [Category(CategoryTypes.Badge.Behavior)] + public object Content { get; set; } + + /// + /// Child content of component, the content that the badge will apply to. + /// + [Parameter] + [Category(CategoryTypes.Badge.Behavior)] + public RenderFragment ChildContent { get; set; } + + #endregion + + #region Styling + + /// + /// Badge class names, separated by space. + /// + [Parameter] + [Category(CategoryTypes.Badge.Appearance)] + public string BadgeClass { get; set; } + protected string CompiledClassList => new CssBuilder("badge-root") .AddClass(AdditionalClassList) @@ -59,27 +125,6 @@ public partial class Badge : UIComponent [Category(CategoryTypes.Badge.Appearance)] public ThemeColor Color { get; set; } = ThemeColor.Default; - /// - /// Aligns the badge to bottom. - /// - [ExcludeFromCodeCoverage] - [Obsolete("Use Origin instead.", true)] - [Parameter] public bool Bottom { get; set; } - - /// - /// Aligns the badge to left. - /// - [ExcludeFromCodeCoverage] - [Obsolete("Use Origin instead.", true)] - [Parameter] public bool Left { get => Start; set { Start = value; } } - - /// - /// Aligns the badge to the start (Left in LTR and right in RTL). - /// - [ExcludeFromCodeCoverage] - [Obsolete("Use Origin instead.", true)] - [Parameter] public bool Start { get; set; } - /// /// Reduces the size of the badge and hide any of its content. /// @@ -101,55 +146,9 @@ public partial class Badge : UIComponent [Category(CategoryTypes.Badge.Appearance)] public bool Bordered { get; set; } - /// - /// Sets the Glyph to use in the badge. - /// - [Parameter] - [Category(CategoryTypes.Badge.Behavior)] - public string Icon { get; set; } - - /// - /// Max value to show when content is integer type. - /// - [Parameter] - [Category(CategoryTypes.Badge.Behavior)] - public int Max { get; set; } = 99; - - /// - /// Content you want inside the badge. Supported types are string and integer. - /// - [Parameter] - [Category(CategoryTypes.Badge.Behavior)] - public object Content { get; set; } - - /// - /// Badge class names, separated by space. - /// - [Parameter] - [Category(CategoryTypes.Badge.Appearance)] - public string BadgeClass { get; set; } + #endregion - /// - /// Child content of component, the content that the badge will apply to. - /// - [Parameter] - [Category(CategoryTypes.Badge.Behavior)] - public RenderFragment ChildContent { get; set; } - - /// - /// Button click event if set. - /// - [Parameter] public EventCallback OnClick { get; set; } - - private string _content; - - internal Task HandleBadgeClick(MouseEventArgs e) - { - if (OnClick.HasDelegate) - return OnClick.InvokeAsync(e); - - return Task.CompletedTask; - } + #region Lifecycle protected override void OnParametersSet() { @@ -173,4 +172,6 @@ public partial class Badge : UIComponent _content = null; } } + + #endregion } diff --git a/src/Connected.Components/Components/Breadcrumbs/BreadcrumbLink.razor.cs b/src/Connected.Components/Components/Breadcrumbs/BreadcrumbLink.razor.cs index 0f1bb03..1d94ce8 100644 --- a/src/Connected.Components/Components/Breadcrumbs/BreadcrumbLink.razor.cs +++ b/src/Connected.Components/Components/Breadcrumbs/BreadcrumbLink.razor.cs @@ -4,13 +4,20 @@ using Microsoft.AspNetCore.Components; namespace Connected.Components; public partial class BreadcrumbLink : UIComponent { + #region Content [Parameter] public BreadcrumbItem Item { get; set; } [CascadingParameter] public Breadcrumbs Parent { get; set; } + #endregion + + #region Style + private string Classname => new CssBuilder("breadcrumb-item") .AddClass("disabled", Item?.Disabled) .Build(); + + #endregion } diff --git a/src/Connected.Components/Components/Breadcrumbs/BreadcrumbSeparator.razor.cs b/src/Connected.Components/Components/Breadcrumbs/BreadcrumbSeparator.razor.cs index a9cca7b..b402d2c 100644 --- a/src/Connected.Components/Components/Breadcrumbs/BreadcrumbSeparator.razor.cs +++ b/src/Connected.Components/Components/Breadcrumbs/BreadcrumbSeparator.razor.cs @@ -3,6 +3,8 @@ namespace Connected.Components; public partial class BreadcrumbSeparator : UIComponent { + #region Content [CascadingParameter] public Breadcrumbs Parent { get; set; } + #endregion } diff --git a/src/Connected.Components/Components/Breadcrumbs/Breadcrumbs.razor.cs b/src/Connected.Components/Components/Breadcrumbs/Breadcrumbs.razor.cs index e0b5a99..b96b818 100644 --- a/src/Connected.Components/Components/Breadcrumbs/Breadcrumbs.razor.cs +++ b/src/Connected.Components/Components/Breadcrumbs/Breadcrumbs.razor.cs @@ -6,6 +6,22 @@ namespace Connected.Components; public partial class Breadcrumbs : UIComponent { + + #region Events + + internal void Expand() + { + if (!Collapsed) + return; + + Collapsed = false; + StateHasChanged(); + } + + #endregion + + #region Content + /// /// A list of breadcrumb items/links. /// @@ -20,6 +36,30 @@ public partial class Breadcrumbs : UIComponent [Category(CategoryTypes.Breadcrumbs.Appearance)] public string Separator { get; set; } = "/"; + /// + /// Custom expander icon. + /// + [Parameter] + [Category(CategoryTypes.Breadcrumbs.Appearance)] + public string ExpanderIcon { get; set; } = Icons.Material.Filled.SettingsEthernet; + + #endregion + + #region Styling + [Parameter] + public string ClassList { get; set; } = string.Empty; + private string Classname => new CssBuilder("breadcrumbs") + .AddClass("typography-body1") + .AddClass(ClassList) + .Build(); + + internal static string GetItemClassname(BreadcrumbItem item) + { + return new CssBuilder("breadcrumb-item") + .AddClass("disabled", item.Disabled) + .Build(); + } + /// /// Specifies a RenderFragment to use as the separator. /// @@ -34,6 +74,8 @@ public partial class Breadcrumbs : UIComponent [Category(CategoryTypes.Breadcrumbs.Behavior)] public RenderFragment ItemTemplate { get; set; } + public bool Collapsed { get; private set; } = true; + /// /// Controls when (and if) the breadcrumbs will automatically collapse. /// @@ -41,32 +83,6 @@ public partial class Breadcrumbs : UIComponent [Category(CategoryTypes.Breadcrumbs.Behavior)] public byte? MaxItems { get; set; } - /// - /// Custom expander icon. - /// - [Parameter] - [Category(CategoryTypes.Breadcrumbs.Appearance)] - public string ExpanderIcon { get; set; } = Icons.Material.Filled.SettingsEthernet; - - public bool Collapsed { get; private set; } = true; - private string Classname => new CssBuilder("breadcrumbs") - .AddClass("typography-body1") - .AddClass(AdditionalClassList) - .Build(); - - internal static string GetItemClassname(BreadcrumbItem item) - { - return new CssBuilder("breadcrumb-item") - .AddClass("disabled", item.Disabled) - .Build(); - } + #endregion - internal void Expand() - { - if (!Collapsed) - return; - - Collapsed = false; - StateHasChanged(); - } } diff --git a/src/Connected.Components/Components/BreakpointProvider/BreakpointProvider.razor.cs b/src/Connected.Components/Components/BreakpointProvider/BreakpointProvider.razor.cs index 3dcca7d..9108251 100644 --- a/src/Connected.Components/Components/BreakpointProvider/BreakpointProvider.razor.cs +++ b/src/Connected.Components/Components/BreakpointProvider/BreakpointProvider.razor.cs @@ -6,18 +6,37 @@ namespace Connected.Components; public partial class BreakpointProvider : UIComponent, IAsyncDisposable { - private Guid _breakPointListenerSubscriptionId; + #region Variables + private Guid _breakPointListenerSubscriptionId; + public Breakpoint Breakpoint { get; private set; } = Breakpoint.Always; - [Parameter] public EventCallback OnBreakpointChanged { get; set; } - [Inject] public IBreakpointService Service { get; set; } + #endregion + + #region Events + private void SetBreakpointCallback(Breakpoint breakpoint) + { + InvokeAsync(() => + { + Breakpoint = breakpoint; + OnBreakpointChanged.InvokeAsync(breakpoint); + StateHasChanged(); + }).AndForget(); + } + + [Parameter] public EventCallback OnBreakpointChanged { get; set; } + #endregion + + #region Content [Parameter] [Category(CategoryTypes.BreakpointProvider.Behavior)] public RenderFragment ChildContent { get; set; } + #endregion + #region Lifecycle protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); @@ -32,15 +51,9 @@ public partial class BreakpointProvider : UIComponent, IAsyncDisposable } } - private void SetBreakpointCallback(Breakpoint breakpoint) - { - InvokeAsync(() => - { - Breakpoint = breakpoint; - OnBreakpointChanged.InvokeAsync(breakpoint); - StateHasChanged(); - }).AndForget(); - } + public async ValueTask DisposeAsync() => await Service.Unsubscribe(_breakPointListenerSubscriptionId); + #endregion + } diff --git a/src/Connected.Components/Components/Button/Button.razor.cs b/src/Connected.Components/Components/Button/Button.razor.cs index e650ba4..b1b835a 100644 --- a/src/Connected.Components/Components/Button/Button.razor.cs +++ b/src/Connected.Components/Components/Button/Button.razor.cs @@ -5,26 +5,16 @@ namespace Connected.Components; public partial class Button : ButtonBase { - /// - /// Contains the default container classlist and the user defined classes. - /// - private CssBuilder CompiledClassList - { - get - { - return new CssBuilder("button-root mud-button") - .AddClass($"button-{Variant}") - .AddClass(ClassList); - } - } - #region Styling properties + #region Content /// /// Child content of component. /// [Parameter] public RenderFragment? ChildContent { get; set; } + #endregion + #region Styling /// /// A space separated list of class names, added on top of the default class list. /// @@ -36,6 +26,18 @@ public partial class Button : ButtonBase /// [Parameter] public Variant Variant { get; set; } = Variant.Text; + /// + /// Contains the default container classlist and the user defined classes. + /// + private CssBuilder CompiledClassList + { + get + { + return new CssBuilder("button-root mud-button") + .AddClass($"button-{Variant}") + .AddClass(ClassList); + } + } #endregion } diff --git a/src/Connected.Components/Components/Button/ButtonBase.cs b/src/Connected.Components/Components/Button/ButtonBase.cs index c6e54ec..029852a 100644 --- a/src/Connected.Components/Components/Button/ButtonBase.cs +++ b/src/Connected.Components/Components/Button/ButtonBase.cs @@ -36,9 +36,13 @@ public abstract class ButtonBase : UIComponent /// [CascadingParameter] protected IActivatable? Activateable { get; set; } - #endregion - #region Styling properties + /// + /// The HTML element that will be rendered in the root by the component + /// By default, is a button. + /// + protected string HtmlTag => ButtonType.ToString().ToLower(); + #endregion #region Behavior properties @@ -65,13 +69,6 @@ public abstract class ButtonBase : UIComponent /// [Parameter] public bool Disabled { get; set; } - #endregion - - /// - /// The HTML element that will be rendered in the root by the component - /// By default, is a button. - /// - protected string HtmlTag => ButtonType.ToString().ToLower(); /// /// Indicates whether the internal click propagation should be disabled, @@ -79,5 +76,6 @@ public abstract class ButtonBase : UIComponent /// protected bool PreventOnClickPropagation => string.Compare(HtmlTag, "button", true) == 0; - + #endregion + } diff --git a/src/Connected.Components/Components/Button/Fab.razor.cs b/src/Connected.Components/Components/Button/Fab.razor.cs index ce3e08c..3ad2a22 100644 --- a/src/Connected.Components/Components/Button/Fab.razor.cs +++ b/src/Connected.Components/Components/Button/Fab.razor.cs @@ -7,71 +7,72 @@ namespace Connected.Components; public partial class Fab : ButtonBase { - protected string Classname => - new CssBuilder("button-root mud-fab") - .AddClass($"fab-extended", !string.IsNullOrEmpty(Label)) - .AddClass($"fab-{Color.ToDescription()}") - .AddClass($"fab-size-{Size.ToDescription()}") - .Build(); + #region Content /// - /// The color of the component. It supports the theme colors. + /// If applied Glyph will be added at the start of the component. /// [Parameter] - [Category(CategoryTypes.Button.Appearance)] - public ThemeColor Color { get; set; } = ThemeColor.Default; + [Category(CategoryTypes.Button.Behavior)] + public string StartIcon { get; set; } /// - /// The Size of the component. + /// If applied Glyph will be added at the end of the component. /// [Parameter] - [Category(CategoryTypes.Button.Appearance)] - public Size Size { get; set; } = Size.Large; - - /// - /// If applied Glyph will be added at the start of the component. - /// - [Obsolete("This property is obsolete. Use StartIcon instead.")][Parameter] public string Icon { get => StartIcon; set => StartIcon = value; } + [Category(CategoryTypes.Button.Behavior)] + public string EndIcon { get; set; } /// - /// If applied Glyph will be added at the start of the component. + /// If applied the text will be added to the component. /// [Parameter] [Category(CategoryTypes.Button.Behavior)] - public string StartIcon { get; set; } + public string Label { get; set; } /// - /// If applied Glyph will be added at the end of the component. + /// GlyphTitle of the icon used for accessibility. /// [Parameter] [Category(CategoryTypes.Button.Behavior)] - public string EndIcon { get; set; } + public string Title { get; set; } + #endregion + + #region Styling + protected string Classname => + new CssBuilder("button-root mud-fab") + .AddClass($"fab-extended", !string.IsNullOrEmpty(Label)) + .AddClass($"fab-{Color.ToDescription()}") + .AddClass($"fab-size-{Size.ToDescription()}") + .Build(); /// - /// The color of the icon. It supports the theme colors. + /// The color of the component. It supports the theme colors. /// [Parameter] [Category(CategoryTypes.Button.Appearance)] - public ThemeColor IconColor { get; set; } = ThemeColor.Inherit; + public ThemeColor Color { get; set; } = ThemeColor.Default; /// - /// The size of the icon. + /// The Size of the component. /// [Parameter] [Category(CategoryTypes.Button.Appearance)] - public Size IconSize { get; set; } = Size.Medium; + public Size Size { get; set; } = Size.Large; /// - /// If applied the text will be added to the component. + /// The color of the icon. It supports the theme colors. /// [Parameter] - [Category(CategoryTypes.Button.Behavior)] - public string Label { get; set; } + [Category(CategoryTypes.Button.Appearance)] + public ThemeColor IconColor { get; set; } = ThemeColor.Inherit; /// - /// GlyphTitle of the icon used for accessibility. + /// The size of the icon. /// [Parameter] - [Category(CategoryTypes.Button.Behavior)] - public string Title { get; set; } + [Category(CategoryTypes.Button.Appearance)] + public Size IconSize { get; set; } = Size.Medium; + #endregion + } diff --git a/src/Connected.Components/Components/Button/IconButton.razor.cs b/src/Connected.Components/Components/Button/IconButton.razor.cs index 2755723..047e8c7 100644 --- a/src/Connected.Components/Components/Button/IconButton.razor.cs +++ b/src/Connected.Components/Components/Button/IconButton.razor.cs @@ -5,28 +5,7 @@ namespace Connected.Components; public partial class IconButton : ButtonBase { - /// - /// Contains the default container classlist and the user defined classes. - /// - private CssBuilder CompiledClassList - { - get - { - return new CssBuilder("button-root glyph-button") - .AddClass(ClassList); - } - } - - /// - /// Child content of component, only shows if Glyph is null or Empty. - /// - [Parameter] - public RenderFragment? ChildContent { get; set; } - - #region EventCallbacks - #endregion - - #region Content placeholders + #region Content /// /// The Icon that will be used in the component. /// @@ -38,9 +17,16 @@ public partial class IconButton : ButtonBase /// [Parameter] public string? IconTitle { get; set; } + + /// + /// Child content of component, only shows if Glyph is null or Empty. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + #endregion - #region Styling properties + #region Styling /// /// A space separated list of class names, added on top of the default class list. /// @@ -52,5 +38,17 @@ public partial class IconButton : ButtonBase /// [Parameter] public Variant Variant { get; set; } = Variant.Text; + + /// + /// Contains the default container classlist and the user defined classes. + /// + private CssBuilder CompiledClassList + { + get + { + return new CssBuilder("button-root glyph-button") + .AddClass(ClassList); + } + } #endregion } diff --git a/src/Connected.Components/Components/Button/ToggleIconButton.razor b/src/Connected.Components/Components/Button/ToggleIconButton.razor index 1bd2f6c..004062b 100644 --- a/src/Connected.Components/Components/Button/ToggleIconButton.razor +++ b/src/Connected.Components/Components/Button/ToggleIconButton.razor @@ -6,8 +6,8 @@ ClassList="@ClassList" Clicked="Toggle" Disabled="Disabled" - Icon="@(Toggled ? ToggledGlyph : Glyph)" - IconTitle="@(Toggled && ToggledGlyphTitle != null ? ToggledGlyphTitle : GlyphTitle)" + Icon="@(Toggled ? ToggledIcon : Icon)" + IconTitle="@(Toggled && ToggledIconTitle != null ? ToggledIconTitle : IconTitle)" Variant="Variant" @attributes="CustomAttributes" /> diff --git a/src/Connected.Components/Components/Button/ToggleIconButton.razor.cs b/src/Connected.Components/Components/Button/ToggleIconButton.razor.cs index 0d42b0b..53371a7 100644 --- a/src/Connected.Components/Components/Button/ToggleIconButton.razor.cs +++ b/src/Connected.Components/Components/Button/ToggleIconButton.razor.cs @@ -4,41 +4,63 @@ namespace Connected.Components; public partial class ToggleIconButton : UIComponent { - #region EventCallbacks + #region Events /// /// Fires whenever toggled is changed. /// [Parameter] public EventCallback ToggledChanged { get; set; } + + public async Task Toggle() + { + await SetToggledAsync(!Toggled); + } + + protected internal async Task SetToggledAsync(bool toggled) + { + if (Disabled) + return; + + if (Toggled != toggled) + { + Toggled = toggled; + + if (!ToggledChanged.HasDelegate) + return; + + await ToggledChanged.InvokeAsync(Toggled); + } + } + #endregion - #region Content placeholders + #region Content /// /// The glyph that will be used in the untoggled state. /// [Parameter] - public string? Glyph { get; set; } + public string? Icon { get; set; } /// /// GlyphTitle of the icon used for accessibility. /// [Parameter] - public string? GlyphTitle { get; set; } + public string? IconTitle { get; set; } /// /// The glyph that will be used in the toggled state. /// [Parameter] - public string? ToggledGlyph { get; set; } + public string? ToggledIcon { get; set; } /// /// GlyphTitle used in toggled state, if different. /// [Parameter] - public string? ToggledGlyphTitle { get; set; } + public string? ToggledIconTitle { get; set; } #endregion - #region Styling properties + #region Styling /// /// A space separated list of class names, added on top of the default class list. /// @@ -50,9 +72,7 @@ public partial class ToggleIconButton : UIComponent /// [Parameter] public Variant Variant { get; set; } = Variant.Text; - #endregion - #region Behavior properties /// /// If true, the button will be disabled. /// @@ -64,26 +84,6 @@ public partial class ToggleIconButton : UIComponent /// [Parameter] public bool Toggled { get; set; } - #endregion - - public async Task Toggle() - { - await SetToggledAsync(!Toggled); - } - - protected internal async Task SetToggledAsync(bool toggled) - { - if (Disabled) - return; - if (Toggled != toggled) - { - Toggled = toggled; - - if (!ToggledChanged.HasDelegate) - return; - - await ToggledChanged.InvokeAsync(Toggled); - } - } + #endregion } diff --git a/src/Connected.Components/Components/ButtonGroup/ButtonGroup.razor.cs b/src/Connected.Components/Components/ButtonGroup/ButtonGroup.razor.cs index 4b2a6e5..2f80de2 100644 --- a/src/Connected.Components/Components/ButtonGroup/ButtonGroup.razor.cs +++ b/src/Connected.Components/Components/ButtonGroup/ButtonGroup.razor.cs @@ -7,69 +7,75 @@ namespace Connected.Components; public partial class ButtonGroup : UIComponent { - protected string Classname => - new CssBuilder("button-group-root") - .AddClass($"button-group-override-styles", OverrideStyles) - .AddClass($"button-group-{Variant.ToDescription()}") - .AddClass($"button-group-{Variant.ToDescription()}-{Color.ToDescription()}") - .AddClass($"button-group-{Variant.ToDescription()}-size-{Size.ToDescription()}") - .AddClass($"button-group-vertical", VerticalAlign) - .AddClass($"button-group-horizontal", !VerticalAlign) - .AddClass($"button-group-disable-elevation", DisableElevation) - .AddClass($"button-group-rtl", RightToLeft) - .AddClass(AdditionalClassList) - .Build(); + #region Content + /// + /// Child content of component. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Behavior)] + public RenderFragment ChildContent { get; set; } + #endregion - [CascadingParameter(Name = "RightToLeft")] - public bool RightToLeft { get; set; } + #region Styling + /// + /// If true, the button group will be displayed vertically. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Appearance)] + public bool VerticalAlign { get; set; } = false; - /// - /// If true, the button group will override the styles of the individual buttons. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Appearance)] - public bool OverrideStyles { get; set; } = true; + /// + /// If true, no drop-shadow will be used. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Appearance)] + public bool DisableElevation { get; set; } = false; - /// - /// Child content of component. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Behavior)] - public RenderFragment ChildContent { get; set; } + /// + /// The color of the component. It supports the theme colors. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Appearance)] + public ThemeColor Color { get; set; } = ThemeColor.Default; - /// - /// If true, the button group will be displayed vertically. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Appearance)] - public bool VerticalAlign { get; set; } = false; + /// + /// The size of the component. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Appearance)] + public Size Size { get; set; } = Size.Medium; - /// - /// If true, no drop-shadow will be used. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Appearance)] - public bool DisableElevation { get; set; } = false; + /// + /// The variant to use. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Appearance)] + public Variant Variant { get; set; } = Variant.Text; - /// - /// The color of the component. It supports the theme colors. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Appearance)] - public ThemeColor Color { get; set; } = ThemeColor.Default; + protected string Classname => + new CssBuilder("button-group-root") + .AddClass($"button-group-override-styles", OverrideStyles) + .AddClass($"button-group-{Variant.ToDescription()}") + .AddClass($"button-group-{Variant.ToDescription()}-{Color.ToDescription()}") + .AddClass($"button-group-{Variant.ToDescription()}-size-{Size.ToDescription()}") + .AddClass($"button-group-vertical", VerticalAlign) + .AddClass($"button-group-horizontal", !VerticalAlign) + .AddClass($"button-group-disable-elevation", DisableElevation) + .AddClass($"button-group-rtl", RightToLeft) + .AddClass(AdditionalClassList) + .Build(); - /// - /// The size of the component. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Appearance)] - public Size Size { get; set; } = Size.Medium; - /// - /// The variant to use. - /// - [Parameter] - [Category(CategoryTypes.ButtonGroup.Appearance)] - public Variant Variant { get; set; } = Variant.Text; + [CascadingParameter(Name = "RightToLeft")] + public bool RightToLeft { get; set; } + + /// + /// If true, the button group will override the styles of the individual buttons. + /// + [Parameter] + [Category(CategoryTypes.ButtonGroup.Appearance)] + public bool OverrideStyles { get; set; } = true; + #endregion + } diff --git a/src/Connected.Components/Components/Card/Card.razor.cs b/src/Connected.Components/Components/Card/Card.razor.cs index 0b954b0..c9e1baa 100644 --- a/src/Connected.Components/Components/Card/Card.razor.cs +++ b/src/Connected.Components/Components/Card/Card.razor.cs @@ -6,6 +6,16 @@ namespace Connected.Components; public partial class Card : UIComponent { + #region Content + /// + /// Child content of the component. + /// + [Parameter] + [Category(CategoryTypes.Card.Behavior)] + public RenderFragment ChildContent { get; set; } + #endregion + + #region Styling protected string Classname => new CssBuilder("card") .AddClass(AdditionalClassList) @@ -31,11 +41,6 @@ public partial class Card : UIComponent [Parameter] [Category(CategoryTypes.Card.Appearance)] public bool Outlined { get; set; } + #endregion - /// - /// Child content of the component. - /// - [Parameter] - [Category(CategoryTypes.Card.Behavior)] - public RenderFragment ChildContent { get; set; } } diff --git a/src/Connected.Components/Components/Card/CardActions.razor.cs b/src/Connected.Components/Components/Card/CardActions.razor.cs index 3c74bf1..8979a02 100644 --- a/src/Connected.Components/Components/Card/CardActions.razor.cs +++ b/src/Connected.Components/Components/Card/CardActions.razor.cs @@ -6,15 +6,22 @@ namespace Connected.Components; public partial class CardActions : UIComponent { + #region Style protected string Classname => new CssBuilder("card-actions") .AddClass(AdditionalClassList) .Build(); + #endregion + + #region Content + /// /// Child content of the component. /// [Parameter] [Category(CategoryTypes.Card.Behavior)] public RenderFragment ChildContent { get; set; } + + #endregion } diff --git a/src/Connected.Components/Components/Card/CardContent.razor.cs b/src/Connected.Components/Components/Card/CardContent.razor.cs index 28c0f76..7c8cbca 100644 --- a/src/Connected.Components/Components/Card/CardContent.razor.cs +++ b/src/Connected.Components/Components/Card/CardContent.razor.cs @@ -6,15 +6,22 @@ namespace Connected.Components; public partial class CardContent : UIComponent { + #region Style protected string Classname => new CssBuilder("card-content") .AddClass(AdditionalClassList) .Build(); + #endregion + + #region Content + /// /// Child content of the component. /// [Parameter] [Category(CategoryTypes.Card.Behavior)] public RenderFragment ChildContent { get; set; } + + #endregion } diff --git a/src/Connected.Components/Components/Card/CardHeader.razor.cs b/src/Connected.Components/Components/Card/CardHeader.razor.cs index 79f0065..f826226 100644 --- a/src/Connected.Components/Components/Card/CardHeader.razor.cs +++ b/src/Connected.Components/Components/Card/CardHeader.razor.cs @@ -6,7 +6,8 @@ namespace Connected.Components; public partial class CardHeader : UIComponent { - protected string Classname => + #region Style + protected string Classname => new CssBuilder("card-header") .AddClass(AdditionalClassList) .Build(); @@ -32,10 +33,16 @@ public partial class CardHeader : UIComponent [Category(CategoryTypes.Card.Behavior)] public RenderFragment CardHeaderActions { get; set; } - /// - /// Optional child content - /// - [Parameter] + #endregion + + #region Content + + /// + /// Optional child content + /// + [Parameter] [Category(CategoryTypes.Card.Behavior)] public RenderFragment ChildContent { get; set; } + + #endregion } diff --git a/src/Connected.Components/Components/Card/CardMedia.razor.cs b/src/Connected.Components/Components/Card/CardMedia.razor.cs index 3009eab..88793be 100644 --- a/src/Connected.Components/Components/Card/CardMedia.razor.cs +++ b/src/Connected.Components/Components/Card/CardMedia.razor.cs @@ -6,6 +6,7 @@ namespace Connected.Components; public partial class CardMedia : UIComponent { + #region Style protected string StyleString => StyleBuilder.Default($"background-image:url(\"{Image}\");height: {Height}px;") .Build(); @@ -16,23 +17,30 @@ public partial class CardMedia : UIComponent .Build(); /// - /// GlyphTitle of the image used for accessibility. + /// Specifies the height of the image in px. /// [Parameter] [Category(CategoryTypes.Card.Behavior)] - public string Title { get; set; } + public int Height { get; set; } = 300; + + #endregion + + #region Content /// - /// Specifies the path to the image. + /// GlyphTitle of the image used for accessibility. /// [Parameter] [Category(CategoryTypes.Card.Behavior)] - public string Image { get; set; } + public string Title { get; set; } /// - /// Specifies the height of the image in px. + /// Specifies the path to the image. /// [Parameter] [Category(CategoryTypes.Card.Behavior)] - public int Height { get; set; } = 300; + public string Image { get; set; } + + #endregion + } diff --git a/src/Connected.Components/Components/Carousel/Carousel.razor.cs b/src/Connected.Components/Components/Carousel/Carousel.razor.cs index 5bac57e..0d1646b 100644 --- a/src/Connected.Components/Components/Carousel/Carousel.razor.cs +++ b/src/Connected.Components/Components/Carousel/Carousel.razor.cs @@ -8,6 +8,102 @@ namespace Connected.Components; public partial class Carousel : BindableItemsControlBase, IAsyncDisposable { + #region Variables + + private Timer _timer; + private bool _autoCycle = true; + private ThemeColor _currentColor = ThemeColor.Inherit; + private TimeSpan _cycleTimeout = TimeSpan.FromSeconds(5); + private void TimerElapsed(object stateInfo) => InvokeAsync(async () => await TimerTickAsync()); + + #endregion + + #region Events + /// + /// Called when selected Index changed on base class + /// + protected override void SelectionChanged() + { + InvokeAsync(async () => await ResetTimerAsync()); + + _currentColor = SelectedContainer?.Color ?? ThemeColor.Inherit; + } + + //When an item is added, it automatically checks the color + public override void AddItem(CarouselItem item) + { + Items.Add(item); + if (Items.Count - 1 == SelectedIndex) + { + _currentColor = item.Color; + StateHasChanged(); + } + } + + /// + /// Provides Selection changes by horizontal swipe gesture + /// + private void OnSwipe(SwipeDirection direction) + { + if (!EnableSwipeGesture) + { + return; + } + + switch (direction) + { + case SwipeDirection.LeftToRight: + if (RightToLeft) Next(); + else Previous(); + break; + + case SwipeDirection.RightToLeft: + if (RightToLeft) Previous(); + else Next(); + break; + } + } + + /// + /// Immediately starts the AutoCycle timer + /// + private ValueTask StartTimerAsync() + { + if (AutoCycle) + _timer?.Change(AutoCycleTime, TimeSpan.Zero); + + return ValueTask.CompletedTask; + } + + /// + /// Immediately stops the AutoCycle timer + /// + private ValueTask StopTimerAsync() + { + _timer?.Change(Timeout.Infinite, Timeout.Infinite); + return ValueTask.CompletedTask; + } + + /// + /// Stops and restart the AutoCycle timer + /// + private async ValueTask ResetTimerAsync() + { + await StopTimerAsync(); + await StartTimerAsync(); + } + + + /// + /// Changes the SelectedIndex to a next one (or restart on 0) + /// + private async ValueTask TimerTickAsync() + { + await InvokeAsync(Next); + } + #endregion + + #region Styling protected string Classname => new CssBuilder("carousel") .AddClass($"carousel-{(BulletsColor ?? _currentColor).ToDescription()}") @@ -26,12 +122,6 @@ public partial class Carousel : BindableItemsControlBase InvokeAsync(async () => await TimerTickAsync()); - private static Position ConvertPosition(Position position) { return position switch @@ -61,12 +151,6 @@ public partial class Carousel : BindableItemsControlBase - /// Gets or Sets if bar with Bullets must be visible - /// - [Category(CategoryTypes.Carousel.Behavior)] - [Parameter] public bool ShowBullets { get; set; } = true; - /// /// Sets the position of the bullets. By default, the position is the Bottom position /// @@ -80,16 +164,6 @@ public partial class Carousel : BindableItemsControlBase - /// Gets or Sets if bottom bar with Delimiters must be visible. - /// Deprecated, use ShowBullets instead. - /// - [Category(CategoryTypes.Carousel.Behavior)] - [Obsolete($"Use {nameof(ShowBullets)} instead", false)] - [ExcludeFromCodeCoverage] - [Parameter] public bool ShowDelimiters { get => ShowBullets; set => ShowBullets = value; } - /// /// Gets or Sets the Delimiters color. /// If not set, the color is determined based on the property of the active child. @@ -100,48 +174,6 @@ public partial class Carousel : BindableItemsControlBase BulletsColor; set => BulletsColor = value; } - /// - /// Gets or Sets automatic cycle on item collection. - /// - [Parameter] - [Category(CategoryTypes.Carousel.Behavior)] - public bool AutoCycle - { - get => _autoCycle; - set - { - _autoCycle = value; - - if (_autoCycle) - InvokeAsync(async () => await ResetTimerAsync()); - - else - InvokeAsync(async () => await StopTimerAsync()); - } - } - - - /// - /// Gets or Sets the Auto Cycle time - /// - [Parameter] - [Category(CategoryTypes.Carousel.Behavior)] - public TimeSpan AutoCycleTime - { - get => _cycleTimeout; - set - { - _cycleTimeout = value; - - if (_autoCycle == true) - InvokeAsync(async () => await ResetTimerAsync()); - - else - InvokeAsync(async () => await StopTimerAsync()); - } - } - - /// /// Gets or Sets custom class(es) for 'Next' and 'Previous' arrows /// @@ -214,13 +246,6 @@ public partial class Carousel : BindableItemsControlBase BulletTemplate { get; set; } - /// - /// Gets or Sets if swipe gestures are allowed for touch devices. - /// - [Category(CategoryTypes.Carousel.Behavior)] - [Parameter] - public bool EnableSwipeGesture { get; set; } = true; - /// /// Gets or Sets the Template for Delimiters. /// Deprecated, use BulletsTemplate instead. @@ -230,91 +255,78 @@ public partial class Carousel : BindableItemsControlBase DelimiterTemplate { get => BulletTemplate; set => BulletTemplate = value; } + #endregion + #region Behavior /// - /// Called when selected Index changed on base class + /// Gets or Sets if bar with Bullets must be visible /// - protected override void SelectionChanged() - { - InvokeAsync(async () => await ResetTimerAsync()); - - _currentColor = SelectedContainer?.Color ?? ThemeColor.Inherit; - } + [Category(CategoryTypes.Carousel.Behavior)] + [Parameter] public bool ShowBullets { get; set; } = true; - //When an item is added, it automatically checks the color - public override void AddItem(CarouselItem item) - { - Items.Add(item); - if (Items.Count - 1 == SelectedIndex) - { - _currentColor = item.Color; - StateHasChanged(); - } - } + /// + /// Gets or Sets if bottom bar with Delimiters must be visible. + /// Deprecated, use ShowBullets instead. + /// + [Category(CategoryTypes.Carousel.Behavior)] + [Obsolete($"Use {nameof(ShowBullets)} instead", false)] + [ExcludeFromCodeCoverage] + [Parameter] public bool ShowDelimiters { get => ShowBullets; set => ShowBullets = value; } /// - /// Provides Selection changes by horizontal swipe gesture + /// Gets or Sets automatic cycle on item collection. /// - private void OnSwipe(SwipeDirection direction) + [Parameter] + [Category(CategoryTypes.Carousel.Behavior)] + public bool AutoCycle { - if (!EnableSwipeGesture) + get => _autoCycle; + set { - return; - } + _autoCycle = value; - switch (direction) - { - case SwipeDirection.LeftToRight: - if (RightToLeft) Next(); - else Previous(); - break; + if (_autoCycle) + InvokeAsync(async () => await ResetTimerAsync()); - case SwipeDirection.RightToLeft: - if (RightToLeft) Previous(); - else Next(); - break; + else + InvokeAsync(async () => await StopTimerAsync()); } } + /// - /// Immediately starts the AutoCycle timer + /// Gets or Sets the Auto Cycle time /// - private ValueTask StartTimerAsync() + [Parameter] + [Category(CategoryTypes.Carousel.Behavior)] + public TimeSpan AutoCycleTime { - if (AutoCycle) - _timer?.Change(AutoCycleTime, TimeSpan.Zero); + get => _cycleTimeout; + set + { + _cycleTimeout = value; - return ValueTask.CompletedTask; - } + if (_autoCycle == true) + InvokeAsync(async () => await ResetTimerAsync()); - /// - /// Immediately stops the AutoCycle timer - /// - private ValueTask StopTimerAsync() - { - _timer?.Change(Timeout.Infinite, Timeout.Infinite); - return ValueTask.CompletedTask; + else + InvokeAsync(async () => await StopTimerAsync()); + } } - /// - /// Stops and restart the AutoCycle timer - /// - private async ValueTask ResetTimerAsync() - { - await StopTimerAsync(); - await StartTimerAsync(); - } + /// - /// Changes the SelectedIndex to a next one (or restart on 0) + /// Gets or Sets if swipe gestures are allowed for touch devices. /// - private async ValueTask TimerTickAsync() - { - await InvokeAsync(Next); - } + [Category(CategoryTypes.Carousel.Behavior)] + [Parameter] + public bool EnableSwipeGesture { get; set; } = true; + #endregion + #region Lifecycle protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); @@ -346,4 +358,6 @@ public partial class Carousel : BindableItemsControlBase Parent { get; set; } + #endregion + + #region Styling protected string Classname => new CssBuilder("carousel-item") .AddClass($"carousel-item-{Color.ToDescription()}") @@ -35,14 +48,6 @@ public partial class CarouselItem : UIComponent, IDisposable .AddClass(AdditionalClassList) .Build(); - [Parameter] - [Category(CategoryTypes.Carousel.Behavior)] - public RenderFragment ChildContent { get; set; } - - [CascadingParameter] protected internal ItemsControlBase Parent { get; set; } - - [CascadingParameter(Name = "RightToLeft")] public bool RightToLeft { get; set; } - /// /// The color of the component. It supports the theme colors. /// @@ -71,21 +76,24 @@ public partial class CarouselItem : UIComponent, IDisposable [Category(CategoryTypes.Carousel.Appearance)] public string CustomTransitionExit { get; set; } + [CascadingParameter(Name = "RightToLeft")] public bool RightToLeft { get; set; } public bool IsVisible => Parent != null && (Parent.LastContainer == this || Parent.SelectedIndex == Parent.Items.IndexOf(this)); + #endregion - + #region Lifecycle protected override Task OnInitializedAsync() { Parent?.AddItem(this); return Task.CompletedTask; } - private bool _disposed = false; + public void Dispose() { _disposed = true; Parent?.Items.Remove(this); } + #endregion } diff --git a/src/Connected.Components/Components/Chart/Chart.razor.cs b/src/Connected.Components/Components/Chart/Chart.razor.cs index e5ba7df..f76d160 100644 --- a/src/Connected.Components/Components/Chart/Chart.razor.cs +++ b/src/Connected.Components/Components/Chart/Chart.razor.cs @@ -8,22 +8,47 @@ namespace Connected.Components; public partial class Chart : UIComponent { - [Parameter] - [Category(CategoryTypes.Chart.Behavior)] - public double[] InputData { get; set; } = Array.Empty(); - [Parameter] - [Category(CategoryTypes.Chart.Behavior)] - public string[] InputLabels { get; set; } = Array.Empty(); + #region Variables + private int _selectedIndex; + #endregion - [Parameter] - [Category(CategoryTypes.Chart.Behavior)] - public string[] XAxisLabels { get; set; } = Array.Empty(); + #region Events + /// + /// Selected index of a portion of the chart. + /// + [Parameter] public EventCallback SelectedIndexChanged { get; set; } - [Parameter] - [Category(CategoryTypes.Chart.Behavior)] - public List ChartSeries { get; set; } = new(); + /// + /// Scales the input data to the range between 0 and 1 + /// + protected double[] GetNormalizedData() + { + if (InputData == null) + return Array.Empty(); + var total = InputData.Sum(); + return InputData.Select(x => Math.Abs(x) / total).ToArray(); + } + protected string ToS(double d, string format = null) + { + if (string.IsNullOrEmpty(format)) + return d.ToString(CultureInfo.InvariantCulture); + + return d.ToString(format); + } + private Position ConvertLegendPosition(Position position) + { + return position switch + { + Position.Start => RightToLeft ? Position.Right : Position.Left, + Position.End => RightToLeft ? Position.Left : Position.Right, + _ => position + }; + } + #endregion + + #region Styling [Parameter] [Category(CategoryTypes.Chart.Appearance)] public ChartOptions ChartOptions { get; set; } = new(); @@ -43,13 +68,6 @@ public partial class Chart : UIComponent [CascadingParameter(Name = "RightToLeft")] public bool RightToLeft { get; set; } - /// - /// The Type of the chart. - /// - [Parameter] - [Category(CategoryTypes.Chart.Behavior)] - public ChartType ChartType { get; set; } - /// /// The Width of the chart, end with % or px. /// @@ -71,17 +89,30 @@ public partial class Chart : UIComponent [Category(CategoryTypes.Chart.Appearance)] public Position LegendPosition { get; set; } = Position.Bottom; - private Position ConvertLegendPosition(Position position) - { - return position switch - { - Position.Start => RightToLeft ? Position.Right : Position.Left, - Position.End => RightToLeft ? Position.Left : Position.Right, - _ => position - }; - } + #endregion - private int _selectedIndex; + #region Behavior + [Parameter] + [Category(CategoryTypes.Chart.Behavior)] + public double[] InputData { get; set; } = Array.Empty(); + + [Parameter] + [Category(CategoryTypes.Chart.Behavior)] + public string[] InputLabels { get; set; } = Array.Empty(); + + [Parameter] + [Category(CategoryTypes.Chart.Behavior)] + public string[] XAxisLabels { get; set; } = Array.Empty(); + + [Parameter] + [Category(CategoryTypes.Chart.Behavior)] + public List ChartSeries { get; set; } = new(); + /// + /// The Type of the chart. + /// + [Parameter] + [Category(CategoryTypes.Chart.Behavior)] + public ChartType ChartType { get; set; } /// /// Selected index of a portion of the chart. @@ -101,29 +132,7 @@ public partial class Chart : UIComponent } } - /// - /// Selected index of a portion of the chart. - /// - [Parameter] public EventCallback SelectedIndexChanged { get; set; } - - /// - /// Scales the input data to the range between 0 and 1 - /// - protected double[] GetNormalizedData() - { - if (InputData == null) - return Array.Empty(); - var total = InputData.Sum(); - return InputData.Select(x => Math.Abs(x) / total).ToArray(); - } - - protected string ToS(double d, string format = null) - { - if (string.IsNullOrEmpty(format)) - return d.ToString(CultureInfo.InvariantCulture); - - return d.ToString(format); - } + #endregion } diff --git a/src/Connected.Components/Components/Chart/Charts/Bar.razor.cs b/src/Connected.Components/Components/Chart/Charts/Bar.razor.cs index 7a49a3f..d1b097a 100644 --- a/src/Connected.Components/Components/Chart/Charts/Bar.razor.cs +++ b/src/Connected.Components/Components/Chart/Charts/Bar.razor.cs @@ -4,6 +4,7 @@ namespace Connected.Components; partial class Bar : Chart { + #region Variables [CascadingParameter] public Chart ChartParent { get; set; } private List _horizontalLines = new(); @@ -16,7 +17,9 @@ partial class Bar : Chart private List _series = new(); private List _bars = new(); + #endregion + #region Lifecycle protected override void OnParametersSet() { base.OnParametersSet(); @@ -147,4 +150,6 @@ partial class Bar : Chart _legends.Add(legend); } } + #endregion + } diff --git a/src/Connected.Components/Components/Chart/Charts/Donut.razor.cs b/src/Connected.Components/Components/Chart/Charts/Donut.razor.cs index 44299f6..83c6807 100644 --- a/src/Connected.Components/Components/Chart/Charts/Donut.razor.cs +++ b/src/Connected.Components/Components/Chart/Charts/Donut.razor.cs @@ -4,6 +4,7 @@ namespace Connected.Components; partial class Donut : Chart { + #region Variables [CascadingParameter] public Chart ChartParent { get; set; } private List _circles = new(); @@ -12,6 +13,10 @@ partial class Donut : Chart protected string ParentWidth => ChartParent?.Width; protected string ParentHeight => ChartParent?.Height; + #endregion + + #region Lifecycle + protected override void OnParametersSet() { _circles.Clear(); @@ -57,4 +62,6 @@ partial class Donut : Chart } } + #endregion + } diff --git a/src/Connected.Components/Components/Chart/Charts/Line.razor.cs b/src/Connected.Components/Components/Chart/Charts/Line.razor.cs index 1833cba..4d6962c 100644 --- a/src/Connected.Components/Components/Chart/Charts/Line.razor.cs +++ b/src/Connected.Components/Components/Chart/Charts/Line.razor.cs @@ -4,6 +4,8 @@ namespace Connected.Components; partial class Line : Chart { + #region Variables + private const int MaxHorizontalGridLines = 100; [CascadingParameter] public Chart ChartParent { get; set; } @@ -19,6 +21,10 @@ partial class Line : Chart private List _chartLines = new(); + #endregion + + #region Lifecycle + protected override void OnParametersSet() { base.OnParametersSet(); @@ -228,4 +234,6 @@ partial class Line : Chart _legends.Add(legend); } } + + #endregion } diff --git a/src/Connected.Components/Components/Chart/Charts/Pie.razor.cs b/src/Connected.Components/Components/Chart/Charts/Pie.razor.cs index 665d684..e0e9c6d 100644 --- a/src/Connected.Components/Components/Chart/Charts/Pie.razor.cs +++ b/src/Connected.Components/Components/Chart/Charts/Pie.razor.cs @@ -4,11 +4,16 @@ namespace Connected.Components; partial class Pie : Chart { + #region Variables [CascadingParameter] public Chart ChartParent { get; set; } private List _paths = new(); private List _legends = new(); + #endregion + + #region Lifecycle + protected override void OnParametersSet() { _paths.Clear(); @@ -52,4 +57,5 @@ partial class Pie : Chart counter += 1; } } + #endregion } diff --git a/src/Connected.Components/Components/CheckBox/CheckBox.razor.cs b/src/Connected.Components/Components/CheckBox/CheckBox.razor.cs index 28b7899..3e67b39 100644 --- a/src/Connected.Components/Components/CheckBox/CheckBox.razor.cs +++ b/src/Connected.Components/Components/CheckBox/CheckBox.razor.cs @@ -9,6 +9,90 @@ namespace Connected.Components; public partial class CheckBox : BooleanInput { + #region Variables + private IKeyInterceptor _keyInterceptor; + [Inject] private IKeyInterceptorFactory _keyInterceptorFactory { get; set; } + private string _elementId = "checkbox" + Guid.NewGuid().ToString().Substring(0, 8); + #endregion + + #region Events + protected override Task OnChange(ChangeEventArgs args) + { + Modified = true; + + // Apply only when TriState parameter is set to true and T is bool? + if (TriState && typeof(T) == typeof(bool?)) + { + // The cycle is forced with the following steps: true, false, indeterminate, true, false, indeterminate... + if (!((bool?)(object)_value).HasValue) + { + return SetBoolValueAsync(true); + } + else + { + return ((bool?)(object)_value).Value ? SetBoolValueAsync(false) : SetBoolValueAsync(default); + } + } + else + { + return SetBoolValueAsync((bool?)args.Value); + } + } + + protected void HandleKeyDown(KeyboardEventArgs obj) + { + if (Disabled || ReadOnly || !KeyboardEnabled) + return; + switch (obj.Key) + { + case "Delete": + SetBoolValueAsync(false); + break; + case "Enter": + case "NumpadEnter": + SetBoolValueAsync(true); + break; + case "Backspace": + if (TriState) + { + SetBoolValueAsync(null); + } + break; + case " ": + if (BoolValue == null) + { + SetBoolValueAsync(true); + } + else if (BoolValue == true) + { + SetBoolValueAsync(false); + } + else if (BoolValue == false) + { + if (TriState == true) + { + SetBoolValueAsync(null); + } + else + { + SetBoolValueAsync(true); + } + } + break; + } + } + #endregion + + #region Content + /// + /// Child content of component. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Behavior)] + public RenderFragment ChildContent { get; set; } + #endregion + + #region Styling protected string Classname => new CssBuilder("input-control-boolean-input") .AddClass(AdditionalClassList) @@ -45,27 +129,6 @@ public partial class CheckBox : BooleanInput [Category(CategoryTypes.Radio.Appearance)] public ThemeColor? UnCheckedColor { get; set; } = null; - /// - /// The text/label will be displayed next to the checkbox if set. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Behavior)] - public string Label { get; set; } - - /// - /// The position of the text/label. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Behavior)] - public LabelPosition LabelPosition { get; set; } = LabelPosition.End; - - /// - /// If true, the checkbox can be controlled with the keyboard. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Behavior)] - public bool KeyboardEnabled { get; set; } = true; - /// /// If true, disables ripple effect. /// @@ -87,13 +150,6 @@ public partial class CheckBox : BooleanInput [Category(CategoryTypes.FormComponent.Appearance)] public Size Size { get; set; } = Size.Medium; - /// - /// Child content of component. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Behavior)] - public RenderFragment ChildContent { get; set; } - /// /// Custom checked icon, leave null for default. /// @@ -115,13 +171,6 @@ public partial class CheckBox : BooleanInput [Category(CategoryTypes.FormComponent.Appearance)] public string IndeterminateIcon { get; set; } = Icons.Material.Filled.IndeterminateCheckBox; - /// - /// Define if the checkbox can cycle again through indeterminate status. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Validation)] - public bool TriState { get; set; } - private string GetIcon() { if (BoolValue == true) @@ -137,77 +186,39 @@ public partial class CheckBox : BooleanInput return IndeterminateIcon; } - protected override Task OnChange(ChangeEventArgs args) - { - Modified = true; + #endregion - // Apply only when TriState parameter is set to true and T is bool? - if (TriState && typeof(T) == typeof(bool?)) - { - // The cycle is forced with the following steps: true, false, indeterminate, true, false, indeterminate... - if (!((bool?)(object)_value).HasValue) - { - return SetBoolValueAsync(true); - } - else - { - return ((bool?)(object)_value).Value ? SetBoolValueAsync(false) : SetBoolValueAsync(default); - } - } - else - { - return SetBoolValueAsync((bool?)args.Value); - } - } + #region Behavior + /// + /// The text/label will be displayed next to the checkbox if set. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Behavior)] + public string Label { get; set; } - protected void HandleKeyDown(KeyboardEventArgs obj) - { - if (Disabled || ReadOnly || !KeyboardEnabled) - return; - switch (obj.Key) - { - case "Delete": - SetBoolValueAsync(false); - break; - case "Enter": - case "NumpadEnter": - SetBoolValueAsync(true); - break; - case "Backspace": - if (TriState) - { - SetBoolValueAsync(null); - } - break; - case " ": - if (BoolValue == null) - { - SetBoolValueAsync(true); - } - else if (BoolValue == true) - { - SetBoolValueAsync(false); - } - else if (BoolValue == false) - { - if (TriState == true) - { - SetBoolValueAsync(null); - } - else - { - SetBoolValueAsync(true); - } - } - break; - } - } + /// + /// The position of the text/label. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Behavior)] + public LabelPosition LabelPosition { get; set; } = LabelPosition.End; - private IKeyInterceptor _keyInterceptor; - [Inject] private IKeyInterceptorFactory _keyInterceptorFactory { get; set; } + /// + /// If true, the checkbox can be controlled with the keyboard. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Behavior)] + public bool KeyboardEnabled { get; set; } = true; - private string _elementId = "checkbox" + Guid.NewGuid().ToString().Substring(0, 8); + /// + /// Define if the checkbox can cycle again through indeterminate status. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Validation)] + public bool TriState { get; set; } + #endregion + #region Lifecycle protected override void OnInitialized() { base.OnInitialized(); @@ -251,4 +262,6 @@ public partial class CheckBox : BooleanInput } } } + #endregion + } diff --git a/src/Connected.Components/Components/Chip/Chip.razor.cs b/src/Connected.Components/Components/Chip/Chip.razor.cs index 8520042..6f10742 100644 --- a/src/Connected.Components/Components/Chip/Chip.razor.cs +++ b/src/Connected.Components/Components/Chip/Chip.razor.cs @@ -10,11 +10,101 @@ namespace Connected.Components; public partial class Chip : UIComponent, IDisposable { + + #region Variables private bool _isSelected; [Inject] public NavigationManager UriHelper { get; set; } [Inject] public IJsApiService JsApiService { get; set; } + #endregion + + #region Events + + internal void ForceRerender() => StateHasChanged(); + + protected internal async Task OnClickHandler(MouseEventArgs ev) + { + if (ChipSet?.ReadOnly == true) + { + return; + } + if (ChipSet != null) + { + _ = ChipSet.OnChipClicked(this); + } + if (Href != null) + { + // TODO: use MudElement to render and this code can be removed. we know that it has potential problems on iOS + if (string.IsNullOrWhiteSpace(Target)) + UriHelper.NavigateTo(Href, ForceLoad); + else + await JsApiService.Open(Href, Target); + } + else + { + await OnClick.InvokeAsync(ev); + if (Command?.CanExecute(CommandParameter) ?? false) + { + Command.Execute(CommandParameter); + } + } + } + + protected async Task OnCloseHandler(MouseEventArgs ev) + { + if (ChipSet?.ReadOnly == true) + { + return; + } + await OnClose.InvokeAsync(this); + ChipSet?.OnChipDeleted(this); + StateHasChanged(); + } + + + /// + /// Chip click event, if set the chip focus, hover and click effects are applied. + /// + [Parameter] public EventCallback OnClick { get; set; } + /// + /// Chip delete event, if set the delete icon will be visible. + /// + [Parameter] public EventCallback OnClose { get; set; } + #endregion + + #region Content + [CascadingParameter] ChipSet ChipSet { get; set; } + + /// + /// If set to a URL, clicking the button will open the referenced document. Use Target to specify where + /// + [Parameter] + [Category(CategoryTypes.Chip.ClickAction)] + public string Href { get; set; } + /// + /// The target attribute specifies where to open the link, if Href is specified. Possible values: _blank | _self | _parent | _top | framename + /// + [Parameter] + [Category(CategoryTypes.Chip.ClickAction)] + public string Target { get; set; } + + /// + /// Command executed when the user clicks on an element. + /// + [Parameter] + [Category(CategoryTypes.Chip.ClickAction)] + public ICommand Command { get; set; } + + /// + /// Command parameter. + /// + [Parameter] + [Category(CategoryTypes.Chip.ClickAction)] + public object CommandParameter { get; set; } + #endregion + + #region Styling protected string Classname => new CssBuilder("chip") .AddClass($"chip-{GetVariant().ToDescription()}") @@ -57,8 +147,6 @@ public partial class Chip : UIComponent, IDisposable } } - [CascadingParameter] ChipSet ChipSet { get; set; } - /// /// The color of the component. /// @@ -87,13 +175,6 @@ public partial class Chip : UIComponent, IDisposable [Category(CategoryTypes.Chip.Appearance)] public ThemeColor SelectedColor { get; set; } = ThemeColor.Inherit; - /// - /// Avatar Glyph, Overrides the regular Glyph if set. - /// - [Parameter] - [Category(CategoryTypes.Chip.Behavior)] - public string Avatar { get; set; } - /// /// Avatar CSS Class, appends to Chips default avatar classes. /// @@ -108,20 +189,6 @@ public partial class Chip : UIComponent, IDisposable [Category(CategoryTypes.Chip.Appearance)] public bool Label { get; set; } - /// - /// If true, the chip will be displayed in disabled state and no events possible. - /// - [Parameter] - [Category(CategoryTypes.Chip.Behavior)] - public bool Disabled { get; set; } - - /// - /// Sets the Glyph to use. - /// - [Parameter] - [Category(CategoryTypes.Chip.Behavior)] - public string Icon { get; set; } - /// /// Custom checked icon. /// @@ -150,38 +217,51 @@ public partial class Chip : UIComponent, IDisposable [Category(CategoryTypes.Chip.Appearance)] public bool DisableRipple { get; set; } + #endregion + + #region Behavior + /// - /// Child content of component. + /// Set by MudChipSet /// - [Parameter] - [Category(CategoryTypes.Chip.Behavior)] - public RenderFragment ChildContent { get; set; } + public bool IsSelected + { + get => _isSelected; + set + { + if (_isSelected == value) + return; + _isSelected = value; + StateHasChanged(); + } + } /// - /// If set to a URL, clicking the button will open the referenced document. Use Target to specify where (Obsolete replaced by Href) + /// If false, this chip has not been seen before /// - [Obsolete("Use Href Instead.", false)] - [Parameter] - [Category(CategoryTypes.Chip.ClickAction)] - public string Link + public bool DefaultProcessed { get; set; } + + /// + /// Set by MudChipSet + /// + public bool IsChecked { - get => Href; - set => Href = value; + get => _isSelected && ChipSet?.Filter == true; } /// - /// If set to a URL, clicking the button will open the referenced document. Use Target to specify where + /// If true, force browser to redirect outside component router-space. /// [Parameter] [Category(CategoryTypes.Chip.ClickAction)] - public string Href { get; set; } + public bool ForceLoad { get; set; } /// - /// The target attribute specifies where to open the link, if Href is specified. Possible values: _blank | _self | _parent | _top | framename + /// If true, this chip is selected by default if used in a ChipSet. /// [Parameter] - [Category(CategoryTypes.Chip.ClickAction)] - public string Target { get; set; } + [Category(CategoryTypes.Chip.Behavior)] + public bool? Default { get; set; } /// /// A string you want to associate with the chip. If the ChildContent is not set this will be shown as chip text. @@ -199,70 +279,51 @@ public partial class Chip : UIComponent, IDisposable public object Value { get; set; } /// - /// If true, force browser to redirect outside component router-space. + /// Avatar Glyph, Overrides the regular Glyph if set. /// [Parameter] - [Category(CategoryTypes.Chip.ClickAction)] - public bool ForceLoad { get; set; } + [Category(CategoryTypes.Chip.Behavior)] + public string Avatar { get; set; } + + /// - /// If true, this chip is selected by default if used in a ChipSet. + /// If true, the chip will be displayed in disabled state and no events possible. /// [Parameter] [Category(CategoryTypes.Chip.Behavior)] - public bool? Default { get; set; } + public bool Disabled { get; set; } /// - /// Command executed when the user clicks on an element. + /// Sets the Glyph to use. /// [Parameter] - [Category(CategoryTypes.Chip.ClickAction)] - public ICommand Command { get; set; } + [Category(CategoryTypes.Chip.Behavior)] + public string Icon { get; set; } - /// - /// Command parameter. - /// - [Parameter] - [Category(CategoryTypes.Chip.ClickAction)] - public object CommandParameter { get; set; } - /// - /// Chip click event, if set the chip focus, hover and click effects are applied. - /// - [Parameter] public EventCallback OnClick { get; set; } /// - /// Chip delete event, if set the delete icon will be visible. + /// Child content of component. /// - [Parameter] public EventCallback OnClose { get; set; } + [Parameter] + [Category(CategoryTypes.Chip.Behavior)] + public RenderFragment ChildContent { get; set; } /// - /// Set by MudChipSet + /// If set to a URL, clicking the button will open the referenced document. Use Target to specify where (Obsolete replaced by Href) /// - public bool IsChecked + [Obsolete("Use Href Instead.", false)] + [Parameter] + [Category(CategoryTypes.Chip.ClickAction)] + public string Link { - get => _isSelected && ChipSet?.Filter == true; + get => Href; + set => Href = value; } + #endregion - /// - /// If false, this chip has not been seen before - /// - public bool DefaultProcessed { get; set; } - - /// - /// Set by MudChipSet - /// - public bool IsSelected - { - get => _isSelected; - set - { - if (_isSelected == value) - return; - _isSelected = value; - StateHasChanged(); - } - } + #region Lifecycle protected override void OnInitialized() { @@ -271,53 +332,12 @@ public partial class Chip : UIComponent, IDisposable Value = this; } - protected internal async Task OnClickHandler(MouseEventArgs ev) - { - if (ChipSet?.ReadOnly == true) - { - return; - } - if (ChipSet != null) - { - _ = ChipSet.OnChipClicked(this); - } - if (Href != null) - { - // TODO: use MudElement to render and this code can be removed. we know that it has potential problems on iOS - if (string.IsNullOrWhiteSpace(Target)) - UriHelper.NavigateTo(Href, ForceLoad); - else - await JsApiService.Open(Href, Target); - } - else - { - await OnClick.InvokeAsync(ev); - if (Command?.CanExecute(CommandParameter) ?? false) - { - Command.Execute(CommandParameter); - } - } - } - - protected async Task OnCloseHandler(MouseEventArgs ev) - { - if (ChipSet?.ReadOnly == true) - { - return; - } - await OnClose.InvokeAsync(this); - ChipSet?.OnChipDeleted(this); - StateHasChanged(); - } - protected override Task OnInitializedAsync() { ChipSet?.Add(this); return base.OnInitializedAsync(); } - internal void ForceRerender() => StateHasChanged(); - //Exclude because we don't test to catching exception yet [ExcludeFromCodeCoverage] public void Dispose() @@ -331,5 +351,5 @@ public partial class Chip : UIComponent, IDisposable /* ignore! */ } } - + #endregion } diff --git a/src/Connected.Components/Components/ChipSet/ChipSet.razor.cs b/src/Connected.Components/Components/ChipSet/ChipSet.razor.cs index 6b5556f..32b112a 100644 --- a/src/Connected.Components/Components/ChipSet/ChipSet.razor.cs +++ b/src/Connected.Components/Components/ChipSet/ChipSet.razor.cs @@ -6,337 +6,344 @@ namespace Connected.Components; public partial class ChipSet : UIComponent, IDisposable { + #region Variables + private IEqualityComparer _comparer; + private HashSet _selectedValues; + private HashSet _initialSelectedValues; + private HashSet _chips = new(); + private bool _filter; + private bool _disposed; + #endregion + + #region Events + /// + /// Called when the selected chip changes, in Choice mode + /// + [Parameter] + public EventCallback SelectedChipChanged { get; set; } + /// + /// Called when the selection changed, in Filter mode + /// + [Parameter] + public EventCallback SelectedChipsChanged { get; set; } + + /// + /// The current selected value. + /// Note: make the list Clickable for item selection to work. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public ICollection SelectedValues + { + get => _selectedValues; + set + { + if (value == null) + SetSelectedValues(new object[0]); + else + SetSelectedValues(value.ToArray()).AndForget(); + } + } + + /// + /// Called whenever the selection changed + /// + [Parameter] public EventCallback> SelectedValuesChanged { get; set; } + internal Task SetSelectedValues(object[] values) + { + HashSet newValues = null; + if (values == null) + values = new object[0]; + if (MultiSelection) + newValues = new HashSet(values, _comparer); + else + { + newValues = new HashSet(_comparer); + if (values.Length > 0) + newValues.Add(values.First()); + } + // avoid update with same values + if (_selectedValues.IsEqualTo(newValues)) + return Task.CompletedTask; + _selectedValues = newValues; + foreach (var chip in _chips.ToArray()) + { + var isSelected = _selectedValues.Contains(chip.Value); + chip.IsSelected = isSelected; + } + return NotifySelection(); + } + + /// + /// Called when a Chip was deleted (by click on the close icon) + /// + [Parameter] + public EventCallback OnClose { get; set; } + internal Task Add(Chip chip) + { + _chips.Add(chip); + if (_selectedValues.Contains(chip.Value)) + chip.IsSelected = true; + return CheckDefault(chip); + } + + internal void Remove(Chip chip) + { + _chips.Remove(chip); + if (chip.IsSelected) + { + _selectedValues.Remove(chip.Value); + NotifySelection().AndForget(); + } + } + + private async Task CheckDefault(Chip chip) + { + if (!MultiSelection) + return; + if (chip.DefaultProcessed) + return; + chip.DefaultProcessed = true; + if (chip.Default == null) + return; + var oldSelected = chip.IsSelected; + chip.IsSelected = chip.Default == true; + if (chip.IsSelected != oldSelected) + { + if (chip.IsSelected) + _selectedValues.Add(chip.Value); + else + _selectedValues.Remove(chip.Value); + await NotifySelection(); + } + } + + internal Task OnChipClicked(Chip chip) + { + var wasSelected = chip.IsSelected; + if (MultiSelection) + { + chip.IsSelected = !chip.IsSelected; + } + else + { + foreach (var ch in _chips) + { + ch.IsSelected = (ch == chip); // <-- exclusively select the one chip only, thus all others must be deselected + } + if (!Mandatory) + chip.IsSelected = !wasSelected; + } + UpdateSelectedValues(); + return NotifySelection(); + } + + private void UpdateSelectedValues() + { + _selectedValues = new HashSet(_chips.Where(x => x.IsSelected).Select(x => x.Value), _comparer); + } + + private object[] _lastSelectedValues = null; + + private async Task NotifySelection() + { + if (_disposed) + return; + // to avoid endless notification loops we check if selection has really changed + if (_selectedValues.IsEqualTo(_lastSelectedValues)) + return; + _lastSelectedValues = _selectedValues.ToArray(); + await SelectedChipChanged.InvokeAsync(SelectedChip); + await SelectedChipsChanged.InvokeAsync(SelectedChips); + await SelectedValuesChanged.InvokeAsync(SelectedValues); + StateHasChanged(); + } + + public void OnChipDeleted(Chip chip) + { + Remove(chip); + OnClose.InvokeAsync(chip); + } + + + + private async Task SelectDefaultChips() + { + if (!MultiSelection) + { + var anySelected = false; + var defaultChip = _chips.LastOrDefault(chip => chip.Default == true); + if (defaultChip != null) + { + defaultChip.IsSelected = true; + anySelected = true; + } + if (anySelected) + { + UpdateSelectedValues(); + await NotifySelection(); + } + } + } + #endregion + + #region Content + /// + /// Child content of component. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public RenderFragment ChildContent { get; set; } + + /// + /// The currently selected chip in Choice mode + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public Chip SelectedChip + { + get { return _chips.OfType().FirstOrDefault(x => x.IsSelected); } + set + { + if (value == null) + { + foreach (var chip in _chips) + { + chip.IsSelected = false; + } + } + else + { + foreach (var chip in _chips) + { + chip.IsSelected = (chip == value); + } + } + this.InvokeAsync(StateHasChanged); + } + } + + /// + /// The currently selected chips in Filter mode + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public Chip[] SelectedChips + { + get { return _chips.OfType().Where(x => x.IsSelected).ToArray(); } + set + { + if (value == null || value.Length == 0) + { + foreach (var chip in _chips) + { + chip.IsSelected = false; + } + } + else + { + var selected = new HashSet(value); + foreach (var chip in _chips) + { + chip.IsSelected = selected.Contains(chip); + } + } + StateHasChanged(); + } + } + + #endregion + + #region Styling + protected string Classname => + new CssBuilder("chipset") + .AddClass(AdditionalClassList) + .Build(); + #endregion + + #region Behavior + /// + /// The Comparer to use for comparing selected values internally. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public IEqualityComparer Comparer + { + get => _comparer; + set + { + _comparer = value; + // Apply comparer and refresh selected values + _selectedValues = new HashSet(_selectedValues, _comparer); + SelectedValues = _selectedValues; + } + } + /// + /// Will make all chips read only. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public bool ReadOnly { get; set; } = false; + + /// + /// Allows to select more than one chip. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public bool MultiSelection { get; set; } = false; + + /// + /// Will not allow to deselect the selected chip in single selection mode. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public bool Mandatory { get; set; } = false; + + /// + /// Will make all chips closable. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Behavior)] + public bool AllClosable { get; set; } = false; + + /// + /// Will show a check-mark for the selected components. + /// + [Parameter] + [Category(CategoryTypes.ChipSet.Appearance)] + public bool Filter + { + get => _filter; + set + { + if (_filter == value) + return; + _filter = value; + StateHasChanged(); + foreach (var chip in _chips) + chip.ForceRerender(); + } + } + #endregion + + #region Lifecycle + public void Dispose() + { + _disposed = true; + } + protected override void OnInitialized() + { + base.OnInitialized(); + if (_selectedValues == null) + _selectedValues = new HashSet(_comparer); + _initialSelectedValues = new HashSet(_selectedValues, _comparer); + } + protected override async void OnAfterRender(bool firstRender) + { + if (firstRender) + await SelectDefaultChips(); + base.OnAfterRender(firstRender); + } + #endregion - protected string Classname => - new CssBuilder("chipset") - .AddClass(AdditionalClassList) - .Build(); - - /// - /// Child content of component. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public RenderFragment ChildContent { get; set; } - - /// - /// Allows to select more than one chip. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public bool MultiSelection { get; set; } = false; - - /// - /// Will not allow to deselect the selected chip in single selection mode. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public bool Mandatory { get; set; } = false; - - /// - /// Will make all chips closable. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public bool AllClosable { get; set; } = false; - - /// - /// Will show a check-mark for the selected components. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Appearance)] - public bool Filter - { - get => _filter; - set - { - if (_filter == value) - return; - _filter = value; - StateHasChanged(); - foreach (var chip in _chips) - chip.ForceRerender(); - } - } - - /// - /// Will make all chips read only. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public bool ReadOnly { get; set; } = false; - - /// - /// The currently selected chip in Choice mode - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public Chip SelectedChip - { - get { return _chips.OfType().FirstOrDefault(x => x.IsSelected); } - set - { - if (value == null) - { - foreach (var chip in _chips) - { - chip.IsSelected = false; - } - } - else - { - foreach (var chip in _chips) - { - chip.IsSelected = (chip == value); - } - } - this.InvokeAsync(StateHasChanged); - } - } - - /// - /// Called when the selected chip changes, in Choice mode - /// - [Parameter] - public EventCallback SelectedChipChanged { get; set; } - - /// - /// The currently selected chips in Filter mode - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public Chip[] SelectedChips - { - get { return _chips.OfType().Where(x => x.IsSelected).ToArray(); } - set - { - if (value == null || value.Length == 0) - { - foreach (var chip in _chips) - { - chip.IsSelected = false; - } - } - else - { - var selected = new HashSet(value); - foreach (var chip in _chips) - { - chip.IsSelected = selected.Contains(chip); - } - } - StateHasChanged(); - } - } - - protected override void OnInitialized() - { - base.OnInitialized(); - if (_selectedValues == null) - _selectedValues = new HashSet(_comparer); - _initialSelectedValues = new HashSet(_selectedValues, _comparer); - } - - private IEqualityComparer _comparer; - private HashSet _selectedValues; - private HashSet _initialSelectedValues; - - /// - /// The Comparer to use for comparing selected values internally. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public IEqualityComparer Comparer - { - get => _comparer; - set - { - _comparer = value; - // Apply comparer and refresh selected values - _selectedValues = new HashSet(_selectedValues, _comparer); - SelectedValues = _selectedValues; - } - } - - /// - /// Called when the selection changed, in Filter mode - /// - [Parameter] - public EventCallback SelectedChipsChanged { get; set; } - - /// - /// The current selected value. - /// Note: make the list Clickable for item selection to work. - /// - [Parameter] - [Category(CategoryTypes.ChipSet.Behavior)] - public ICollection SelectedValues - { - get => _selectedValues; - set - { - if (value == null) - SetSelectedValues(new object[0]); - else - SetSelectedValues(value.ToArray()).AndForget(); - } - } - - /// - /// Called whenever the selection changed - /// - [Parameter] public EventCallback> SelectedValuesChanged { get; set; } - - internal Task SetSelectedValues(object[] values) - { - HashSet newValues = null; - if (values == null) - values = new object[0]; - if (MultiSelection) - newValues = new HashSet(values, _comparer); - else - { - newValues = new HashSet(_comparer); - if (values.Length > 0) - newValues.Add(values.First()); - } - // avoid update with same values - if (_selectedValues.IsEqualTo(newValues)) - return Task.CompletedTask; - _selectedValues = newValues; - foreach (var chip in _chips.ToArray()) - { - var isSelected = _selectedValues.Contains(chip.Value); - chip.IsSelected = isSelected; - } - return NotifySelection(); - } - - /// - /// Called when a Chip was deleted (by click on the close icon) - /// - [Parameter] - public EventCallback OnClose { get; set; } - - internal Task Add(Chip chip) - { - _chips.Add(chip); - if (_selectedValues.Contains(chip.Value)) - chip.IsSelected = true; - return CheckDefault(chip); - } - - internal void Remove(Chip chip) - { - _chips.Remove(chip); - if (chip.IsSelected) - { - _selectedValues.Remove(chip.Value); - NotifySelection().AndForget(); - } - } - - private async Task CheckDefault(Chip chip) - { - if (!MultiSelection) - return; - if (chip.DefaultProcessed) - return; - chip.DefaultProcessed = true; - if (chip.Default == null) - return; - var oldSelected = chip.IsSelected; - chip.IsSelected = chip.Default == true; - if (chip.IsSelected != oldSelected) - { - if (chip.IsSelected) - _selectedValues.Add(chip.Value); - else - _selectedValues.Remove(chip.Value); - await NotifySelection(); - } - } - - private HashSet _chips = new(); - private bool _filter; - - internal Task OnChipClicked(Chip chip) - { - var wasSelected = chip.IsSelected; - if (MultiSelection) - { - chip.IsSelected = !chip.IsSelected; - } - else - { - foreach (var ch in _chips) - { - ch.IsSelected = (ch == chip); // <-- exclusively select the one chip only, thus all others must be deselected - } - if (!Mandatory) - chip.IsSelected = !wasSelected; - } - UpdateSelectedValues(); - return NotifySelection(); - } - - private void UpdateSelectedValues() - { - _selectedValues = new HashSet(_chips.Where(x => x.IsSelected).Select(x => x.Value), _comparer); - } - - private object[] _lastSelectedValues = null; - - private async Task NotifySelection() - { - if (_disposed) - return; - // to avoid endless notification loops we check if selection has really changed - if (_selectedValues.IsEqualTo(_lastSelectedValues)) - return; - _lastSelectedValues = _selectedValues.ToArray(); - await SelectedChipChanged.InvokeAsync(SelectedChip); - await SelectedChipsChanged.InvokeAsync(SelectedChips); - await SelectedValuesChanged.InvokeAsync(SelectedValues); - StateHasChanged(); - } - - public void OnChipDeleted(Chip chip) - { - Remove(chip); - OnClose.InvokeAsync(chip); - } - - protected override async void OnAfterRender(bool firstRender) - { - if (firstRender) - await SelectDefaultChips(); - base.OnAfterRender(firstRender); - } - - private async Task SelectDefaultChips() - { - if (!MultiSelection) - { - var anySelected = false; - var defaultChip = _chips.LastOrDefault(chip => chip.Default == true); - if (defaultChip != null) - { - defaultChip.IsSelected = true; - anySelected = true; - } - if (anySelected) - { - UpdateSelectedValues(); - await NotifySelection(); - } - } - } - - private bool _disposed; - - public void Dispose() - { - _disposed = true; - } } diff --git a/src/Connected.Components/Components/Collapse/Collapse.razor.cs b/src/Connected.Components/Components/Collapse/Collapse.razor.cs index 100cfae..dc9da98 100644 --- a/src/Connected.Components/Components/Collapse/Collapse.razor.cs +++ b/src/Connected.Components/Components/Collapse/Collapse.razor.cs @@ -7,6 +7,7 @@ namespace Connected.Components; public partial class Collapse : UIComponent { + #region Variables internal enum CollapseState { Entering, Entered, Exiting, Exited @@ -16,7 +17,58 @@ public partial class Collapse : UIComponent private bool _expanded, _isRendered, _updateHeight; private ElementReference _wrapper; internal CollapseState _state = CollapseState.Exited; + #endregion + #region Events + [Parameter] public EventCallback OnAnimationEnd { get; set; } + + [Parameter] public EventCallback ExpandedChanged { get; set; } + + internal async Task UpdateHeight() + { + try + { + _height = (await _wrapper.MudGetBoundingClientRectAsync())?.Height ?? 0; + } + catch (Exception ex) when (ex is JSDisconnectedException || ex is TaskCanceledException) + { + _height = 0; + } + + if (MaxHeight != null && _height > MaxHeight) + { + _height = MaxHeight.Value; + } + } + + + + public void AnimationEnd() + { + if (_state == CollapseState.Entering) + { + _state = CollapseState.Entered; + StateHasChanged(); + } + else if (_state == CollapseState.Exiting) + { + _state = CollapseState.Exited; + StateHasChanged(); + } + OnAnimationEnd.InvokeAsync(Expanded); + } + #endregion + + #region Content + /// + /// Child content of component. + /// + [Parameter] public RenderFragment ChildContent { get; set; } + + + #endregion + + #region Styling protected string Stylename => new StyleBuilder() .AddStyle("max-height", MaxHeight.ToPx(), MaxHeight != null) @@ -66,15 +118,6 @@ public partial class Collapse : UIComponent /// [Parameter] public int? MaxHeight { get; set; } - /// - /// Child content of component. - /// - [Parameter] public RenderFragment ChildContent { get; set; } - - [Parameter] public EventCallback OnAnimationEnd { get; set; } - - [Parameter] public EventCallback ExpandedChanged { get; set; } - /// /// Modified Animation duration that scales with height parameter. /// Basic implementation for now but should be a math formula to allow it to scale between 0.1s and 1s for the effect to be consistently smooth. @@ -95,23 +138,9 @@ public partial class Collapse : UIComponent set { } } - internal async Task UpdateHeight() - { - try - { - _height = (await _wrapper.MudGetBoundingClientRectAsync())?.Height ?? 0; - } - catch (Exception ex) when (ex is JSDisconnectedException || ex is TaskCanceledException) - { - _height = 0; - } - - if (MaxHeight != null && _height > MaxHeight) - { - _height = MaxHeight.Value; - } - } + #endregion + #region Lifecycle protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -127,19 +156,6 @@ public partial class Collapse : UIComponent } await base.OnAfterRenderAsync(firstRender); } + #endregion - public void AnimationEnd() - { - if (_state == CollapseState.Entering) - { - _state = CollapseState.Entered; - StateHasChanged(); - } - else if (_state == CollapseState.Exiting) - { - _state = CollapseState.Exited; - StateHasChanged(); - } - OnAnimationEnd.InvokeAsync(Expanded); - } -} +} \ No newline at end of file diff --git a/src/Connected.Components/Components/ColorPicker/ColorPicker.razor.cs b/src/Connected.Components/Components/ColorPicker/ColorPicker.razor.cs index 2f4aae0..e3cac1e 100644 --- a/src/Connected.Components/Components/ColorPicker/ColorPicker.razor.cs +++ b/src/Connected.Components/Components/ColorPicker/ColorPicker.razor.cs @@ -13,585 +13,581 @@ namespace Connected.Components; public partial class ColorPicker : Picker, IAsyncDisposable { - public ColorPicker() : base(new DefaultConverter()) - { - AdornmentIcon = Icons.Material.Outlined.Palette; - DisableToolbar = true; - Value = "#594ae2"; //MudBlazor Blue - Text = GetColorTextValue(); - AdornmentAriaLabel = "Open Color Picker"; - } - - #region Fields - - private static Dictionary r, Func g, Func b, string dominantColorPart)> _rgbToHueMapper = new() - { - { 0, ((x) => 255, x => x, x => 0, "rb") }, - { 1, ((x) => 255 - x, x => 255, x => 0, "gb") }, - { 2, ((x) => 0, x => 255, x => x, "gr") }, - { 3, ((x) => 0, x => 255 - x, x => 255, "br") }, - { 4, ((x) => x, x => 0, x => 255, "bg") }, - { 5, ((x) => 255, x => 0, x => 255 - x, "rg") }, - }; - - private const double _maxY = 250; - private const double _maxX = 312; - private const double _selctorSize = 26.0; - - private double _selectorX; - private double _selectorY; - private bool _skipFeedback = false; - - private Color _baseColor; - private Color _color; - - private bool _collectionOpen; - - private readonly Guid _id = Guid.NewGuid(); - private Guid _throttledMouseOverEventId; - - private IEventListener _throttledEventManager; - [Inject] IEventListenerFactory ThrottledEventManagerFactory { get; set; } - - #endregion - - #region Parameters - - [CascadingParameter(Name = "RightToLeft")] public bool RightToLeft { get; set; } - - private bool _disableAlpha = false; - - /// - /// If true, Alpha options will not be displayed and color output will be RGB, HSL or HEX and not RGBA, HSLA or HEXA. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisableAlpha - { - get => _disableAlpha; - set - { - if (value != _disableAlpha) - { - _disableAlpha = value; - - if (value == true) - { - Value = Value.SetAlpha(1.0); - } - - Text = GetColorTextValue(); - } - } - } - - /// - /// If true, the color field will not be displayed. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisableColorField { get; set; } = false; - - /// - /// If true, the switch to change color mode will not be displayed. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisableModeSwitch { get; set; } = false; - - /// - /// If true, textfield inputs and color mode switch will not be displayed. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisableInputs { get; set; } = false; - - /// - /// If true, hue and alpha sliders will not be displayed. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisableSliders { get; set; } = false; - - /// - /// If true, the preview color box will not be displayed, note that the preview color functions as a button as well for collection colors. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisablePreview { get; set; } = false; - - /// - /// The initial mode (RGB, HSL or HEX) the picker should open. Defaults to RGB - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public ColorPickerMode ColorPickerMode { get; set; } = ColorPickerMode.RGB; - - private ColorPickerView _colorPickerView = ColorPickerView.Spectrum; - private ColorPickerView _activeColorPickerView = ColorPickerView.Spectrum; - - /// - /// The initial view of the picker. Views can be changed if toolbar is enabled. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public ColorPickerView ColorPickerView - { - get => _colorPickerView; - set - { - if (value != _colorPickerView) - { - _colorPickerView = value; - ChangeView(value).AndForget(); - } - } - } - - /// - /// If true, binding changes occurred also when HSL values changed without a corresponding RGB change - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Behavior)] - public bool UpdateBindingIfOnlyHSLChanged { get; set; } = false; - - /// - /// A two-way bindable property representing the selected value. MudColor is a utility class that can be used to get the value as RGB, HSL, hex or other value - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Data)] - public Color Value - { - get => _color; - set - { - if (value == null) { return; } - - var rgbChanged = value != _color; - var hslChanged = _color == null ? false : value.HslChanged(_color); - _color = value; - - if (rgbChanged) - { - if (_skipFeedback == false) - { - UpdateBaseColor(); - UpdateColorSelectorBasedOnRgb(); - } - - SetTextAsync(GetColorTextValue(), false).AndForget(); - ValueChanged.InvokeAsync(value).AndForget(); - FieldChanged(value); - } - - if (rgbChanged == false && UpdateBindingIfOnlyHSLChanged && hslChanged == true) - { - SetTextAsync(GetColorTextValue(), false).AndForget(); - ValueChanged.InvokeAsync(value).AndForget(); - FieldChanged(value); - } - } - } - - [Parameter] public EventCallback ValueChanged { get; set; } - - /// - /// MudColor list of predefined colors. The first five colors will show up as the quick colors on preview dot click. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public IEnumerable Palette { get; set; } = new Color[] - { "#424242", "#2196f3", "#00c853", "#ff9800", "#f44336", - "#f6f9fb", "#9df1fa", "#bdffcf", "#fff0a3", "#ffd254", - "#e6e9eb", "#27dbf5", "#7ef7a0", "#ffe273", "#ffb31f", - "#c9cccf", "#13b8e8", "#14dc71", "#fdd22f", "#ff9102", - "#858791", "#0989c2", "#1bbd66", "#ebb323", "#fe6800", - "#585b62", "#17698e", "#17a258", "#d9980d", "#dc3f11", - "#353940", "#113b53", "#127942", "#bf7d11", "#aa0000" - }; - - private IEnumerable _gridList = new Color[] - { - "#FFFFFF","#ebebeb","#d6d6d6","#c2c2c2","#adadad","#999999","#858586","#707070","#5c5c5c","#474747","#333333","#000000", - "#133648","#071d53","#0f0638","#2a093b","#370c1b","#541107","#532009","#53350d","#523e0f","#65611b","#505518","#2b3d16", - "#1e4c63","#0f2e76","#180b4e","#3f1256","#4e1629","#781e0e","#722f10","#734c16","#73591a","#8c8629","#707625","#3f5623", - "#2e6c8c","#1841a3","#280c72","#591e77","#6f223d","#a62c17","#a0451a","#a06b23","#9f7d28","#c3bc3c","#9da436","#587934", - "#3c8ab0","#2155ce","#331c8e","#702898","#8d2e4f","#d03a20","#ca5a24","#c8862e","#c99f35","#f3ec4e","#c6d047","#729b44", - "#479fd3","#2660f5","#4725ab","#8c33b5","#aa395d","#eb512e","#ed732e","#f3ae3d","#f5c944","#fefb67","#ddeb5c","#86b953", - "#59c4f7","#4e85f6","#5733e2","#af43eb","#d44a7a","#ed6c59","#ef8c56","#f3b757","#f6cd5b","#fef881","#e6ee7a","#a3d16e", - "#78d3f8","#7fa6f8","#7e52f5","#c45ff6","#de789d","#f09286","#f2a984","#f6c983","#f9da85","#fef9a1","#ebf29b","#badc94", - "#a5e1fa","#adc5fa","#ab8df7","#d696f8","#e8a7bf","#f4b8b1","#f6c7af","#f9daae","#fae5af","#fefbc0","#f3f7be","#d2e7ba", - "#d2effd","#d6e1fc","#d6c9fa","#e9cbfb","#f3d4df","#f9dcd9","#fae3d8","#fcecd7","#fdf2d8","#fefce0","#f7fade","#e3edd6" - }; - - /// - /// When set to true, no mouse move events in the spectrum mode will be captured, so the selector circle won't fellow the mouse. - /// Under some conditions like long latency the visual representation might not reflect the user behaviour anymore. So, it can be disabled - /// Enabled by default - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool DisableDragEffect { get; set; } = false; - - /// - /// Custom close icon. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string CloseIcon { get; set; } = Icons.Material.Filled.Close; - - /// - /// Custom spectrum icon. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string SpectrumIcon { get; set; } = Icons.Material.Filled.Tune; - - /// - /// Custom grid icon. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string GridIcon { get; set; } = Icons.Material.Filled.Apps; - - /// - /// Custom palette icon. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string PaletteIcon { get; set; } = Icons.Material.Filled.Palette; - - /// - /// Custom import/export icont. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string ImportExportIcon { get; set; } = Icons.Material.Filled.ImportExport; - - #endregion - - private void ToggleCollection() - { - _collectionOpen = !_collectionOpen; - } - - private void SelectPaletteColor(Color color) - { - Value = color; - _collectionOpen = false; - - if ( - IsAnyControlVisible() == false || _activeColorPickerView is ColorPickerView.GridCompact or ColorPickerView.Palette) - { - Close(); - } - } - - public void ChangeMode() => - ColorPickerMode = ColorPickerMode switch - { - ColorPickerMode.RGB => ColorPickerMode.HSL, - ColorPickerMode.HSL => ColorPickerMode.HEX, - ColorPickerMode.HEX => ColorPickerMode.RGB, - _ => ColorPickerMode.RGB, - }; - - public async Task ChangeView(ColorPickerView value) - { - - var oldValue = _activeColorPickerView; - - _activeColorPickerView = value; - Text = GetColorTextValue(); - - if (oldValue == ColorPickerView.Spectrum) - { - await RemoveMouseOverEvent(); - } - - if (value == ColorPickerView.Spectrum) - { - _attachedMouseEvent = true; - } - } - - private void UpdateBaseColorSlider(int value) - { - var diff = Math.Abs(value - (int)Value.H); - if (diff == 0) { return; } - - Value = Value.SetH(value); - } - - private void UpdateBaseColor() - { - var index = (int)_color.H / 60; - if (index == 6) - { - index = 5; - } - - var valueInDeg = (int)_color.H - (index * 60); - var value = (int)(MathExtensions.Map(0, 60, 0, 255, valueInDeg)); - var section = _rgbToHueMapper[index]; - - _baseColor = new(section.r(value), section.g(value), section.b(value), 255); - } - - private void UpdateColorBaseOnSelection() - { - var x = _selectorX / _maxX; - - var r_x = 255 - (int)((255 - _baseColor.R) * x); - var g_x = 255 - (int)((255 - _baseColor.G) * x); - var b_x = 255 - (int)((255 - _baseColor.B) * x); - - var y = 1.0 - _selectorY / _maxY; - - var r = r_x * y; - var g = g_x * y; - var b = b_x * y; - - _skipFeedback = true; - //in this mode, H is expected to be stable, so copy H value - Value = new Color((byte)r, (byte)g, (byte)b, _color); - _skipFeedback = false; - } - - private void UpdateColorSelectorBasedOnRgb() - { - var hueValue = (int)MathExtensions.Map(0, 360, 0, 6 * 255, _color.H); - var index = hueValue / 255; - if (index == 6) - { - index = 5; - } - - var section = _rgbToHueMapper[index]; - - var colorValues = section.dominantColorPart switch - { - "rb" => (_color.R, _color.B), - "rg" => (_color.R, _color.G), - "gb" => (_color.G, _color.B), - "gr" => (_color.G, _color.R), - "br" => (_color.B, _color.R), - "bg" => (_color.B, _color.G), - _ => (255, 255) - }; - - var primaryDiff = 255 - colorValues.Item1; - var primaryDiffDelta = colorValues.Item1 / 255.0; - - _selectorY = MathExtensions.Map(0, 255, 0, _maxY, primaryDiff); - - var secondaryColorX = colorValues.Item2 * (1.0 / primaryDiffDelta); - var relation = (255 - secondaryColorX) / 255.0; - - _selectorX = relation * _maxX; - } - - #region mouse interactions - - private void HandleColorOverlayClicked() - { - UpdateColorBaseOnSelection(); - - if (IsAnyControlVisible() == false) - { - Close(); - } - } - - private void OnSelectorClicked(MouseEventArgs e) - { - SetSelectorBasedOnMouseEvents(e, false); - HandleColorOverlayClicked(); - } - - private void OnColorOverlayClick(MouseEventArgs e) - { - SetSelectorBasedOnMouseEvents(e, true); - HandleColorOverlayClicked(); - } - - private void OnMouseOver(MouseEventArgs e) - { - if (e.Buttons == 1) - { - SetSelectorBasedOnMouseEvents(e, true); - UpdateColorBaseOnSelection(); - } - } - - private void SetSelectorBasedOnMouseEvents(MouseEventArgs e, bool offsetIsAbsolute) - { - _selectorX = (offsetIsAbsolute == true ? e.OffsetX : (e.OffsetX - _selctorSize / 2.0) + _selectorX).EnsureRange(_maxX); - _selectorY = (offsetIsAbsolute == true ? e.OffsetY : (e.OffsetY - _selctorSize / 2.0) + _selectorY).EnsureRange(_maxY); - } - - #endregion - - #region updating inputs - - /// - /// Set the R (red) component of the color picker - /// - /// A value between 0 (no red) or 255 (max red) - public void SetR(int value) => Value = Value.SetR(value); - - /// - /// Set the G (green) component of the color picker - /// - /// A value between 0 (no green) or 255 (max green) - public void SetG(int value) => Value = Value.SetG(value); - - /// - /// Set the B (blue) component of the color picker - /// - /// A value between 0 (no blue) or 255 (max blue) - public void SetB(int value) => Value = Value.SetB(value); - - /// - /// Set the H (hue) component of the color picker - /// - /// A value between 0 and 360 (degrees) - public void SetH(double value) => Value = Value.SetH(value); - - /// - /// Set the S (saturation) component of the color picker - /// - /// A value between 0.0 (no saturation) and 1.0 (max saturation) - public void SetS(double value) => Value = Value.SetS(value); - - /// - /// Set the L (Lightness) component of the color picker - /// - /// A value between 0.0 (no light, black) and 1.0 (max light, white) - public void SetL(double value) => Value = Value.SetL(value); - - /// - /// Set the Alpha (transparency) component of the color picker - /// - /// A value between 0.0 (full transparent) and 1.0 (solid) - public void SetAlpha(double value) => Value = Value.SetAlpha(value); - - /// - /// Set the Alpha (transparency) component of the color picker - /// - /// A value between 0 (full transparent) and 1 (solid) - public void SetAlpha(int value) => Value = Value.SetAlpha(value); - - /// - /// Set the color of the picker based on the string input - /// - /// Accepting different formats for a color representation such as rbg, rgba, # - public void SetInputString(string input) - { - Color color; - try - { - color = new Color(input); - } - catch (Exception) - { - return; - } - - Value = color; - } - - protected override Task StringValueChanged(string value) - { - SetInputString(value); - return Task.CompletedTask; - } - - private bool _attachedMouseEvent = false; - - protected override void OnPickerOpened() - { - base.OnPickerOpened(); - _attachedMouseEvent = true; - StateHasChanged(); - } - - protected override void OnPickerClosed() - { - base.OnPickerClosed(); - RemoveMouseOverEvent().AndForget(); - } - - #endregion - - #region helper - - private string GetSelectorLocation() => $"translate({Math.Round(_selectorX, 2).ToString(CultureInfo.InvariantCulture)}px, {Math.Round(_selectorY, 2).ToString(CultureInfo.InvariantCulture)}px);"; - private string GetColorTextValue() => (DisableAlpha == true || _activeColorPickerView is ColorPickerView.Palette or ColorPickerView.GridCompact) ? _color.ToString(ColorOutputFormats.Hex) : _color.ToString(ColorOutputFormats.HexA); - private int GetHexColorInputMaxLength() => DisableAlpha ? 7 : 9; - - private EventCallback GetEventCallback() => EventCallback.Factory.Create(this, () => Close()); - private bool IsAnyControlVisible() => !(DisablePreview && DisableSliders && DisableInputs); - private EventCallback GetSelectPaletteColorCallback(Color color) => new EventCallbackFactory().Create(this, (MouseEventArgs e) => SelectPaletteColor(color)); - - private ThemeColor GetButtonColor(ColorPickerView view) => _activeColorPickerView == view ? ThemeColor.Primary : ThemeColor.Inherit; - private string GetColorDotClass(Color color) => new CssBuilder("picker-color-dot").AddClass("selected", color == Value).ToString(); - private string AlphaSliderStyle => new StyleBuilder().AddStyle($"background-image: linear-gradient(to {(RightToLeft ? "left" : "right")}, transparent, {_color.ToString(ColorOutputFormats.RGB)})").Build(); - - #endregion - - #region life cycle hooks - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - await base.OnAfterRenderAsync(firstRender); - - if (firstRender == true) - { - if (PickerVariant == PickerVariant.Static) - { - await AddMouseOverEvent(); - } - } - - if (_attachedMouseEvent == true) - { - _attachedMouseEvent = false; - await AddMouseOverEvent(); - } - } - - private async Task AddMouseOverEvent() - { - if (DisableDragEffect == true) { return; } - - if (_throttledEventManager == null) - { - _throttledEventManager = ThrottledEventManagerFactory.Create(); - } - - _throttledMouseOverEventId = await - _throttledEventManager.Subscribe("mousemove", _id.ToString(), "mudEventProjections.correctOffset", 10, async (x) => - { - var e = x as MouseEventArgs; - await InvokeAsync(() => OnMouseOver(e)); - StateHasChanged(); - }); - } - - private Task RemoveMouseOverEvent() - { - if (_throttledMouseOverEventId == default) { return Task.CompletedTask; } - - return _throttledEventManager.Unsubscribe(_throttledMouseOverEventId); - } - - public async ValueTask DisposeAsync() - { - if (_throttledEventManager == null) { return; } - - await _throttledEventManager.DisposeAsync(); - } - - #endregion + #region Variables + + private static Dictionary r, Func g, Func b, string dominantColorPart)> _rgbToHueMapper = new() + { + { 0, ((x) => 255, x => x, x => 0, "rb") }, + { 1, ((x) => 255 - x, x => 255, x => 0, "gb") }, + { 2, ((x) => 0, x => 255, x => x, "gr") }, + { 3, ((x) => 0, x => 255 - x, x => 255, "br") }, + { 4, ((x) => x, x => 0, x => 255, "bg") }, + { 5, ((x) => 255, x => 0, x => 255 - x, "rg") }, + }; + + private const double _maxY = 250; + private const double _maxX = 312; + private const double _selctorSize = 26.0; + + private double _selectorX; + private double _selectorY; + private bool _skipFeedback = false; + + private Color _baseColor; + private Color _color; + + private bool _collectionOpen; + + private readonly Guid _id = Guid.NewGuid(); + private Guid _throttledMouseOverEventId; + + private ColorPickerView _colorPickerView = ColorPickerView.Spectrum; + private ColorPickerView _activeColorPickerView = ColorPickerView.Spectrum; + + private bool _disableAlpha = false; + + private IEventListener _throttledEventManager; + + /// + /// MudColor list of predefined colors. The first five colors will show up as the quick colors on preview dot click. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public IEnumerable Palette { get; set; } = new Color[] + { "#424242", "#2196f3", "#00c853", "#ff9800", "#f44336", + "#f6f9fb", "#9df1fa", "#bdffcf", "#fff0a3", "#ffd254", + "#e6e9eb", "#27dbf5", "#7ef7a0", "#ffe273", "#ffb31f", + "#c9cccf", "#13b8e8", "#14dc71", "#fdd22f", "#ff9102", + "#858791", "#0989c2", "#1bbd66", "#ebb323", "#fe6800", + "#585b62", "#17698e", "#17a258", "#d9980d", "#dc3f11", + "#353940", "#113b53", "#127942", "#bf7d11", "#aa0000" + }; + + private IEnumerable _gridList = new Color[] + { + "#FFFFFF","#ebebeb","#d6d6d6","#c2c2c2","#adadad","#999999","#858586","#707070","#5c5c5c","#474747","#333333","#000000", + "#133648","#071d53","#0f0638","#2a093b","#370c1b","#541107","#532009","#53350d","#523e0f","#65611b","#505518","#2b3d16", + "#1e4c63","#0f2e76","#180b4e","#3f1256","#4e1629","#781e0e","#722f10","#734c16","#73591a","#8c8629","#707625","#3f5623", + "#2e6c8c","#1841a3","#280c72","#591e77","#6f223d","#a62c17","#a0451a","#a06b23","#9f7d28","#c3bc3c","#9da436","#587934", + "#3c8ab0","#2155ce","#331c8e","#702898","#8d2e4f","#d03a20","#ca5a24","#c8862e","#c99f35","#f3ec4e","#c6d047","#729b44", + "#479fd3","#2660f5","#4725ab","#8c33b5","#aa395d","#eb512e","#ed732e","#f3ae3d","#f5c944","#fefb67","#ddeb5c","#86b953", + "#59c4f7","#4e85f6","#5733e2","#af43eb","#d44a7a","#ed6c59","#ef8c56","#f3b757","#f6cd5b","#fef881","#e6ee7a","#a3d16e", + "#78d3f8","#7fa6f8","#7e52f5","#c45ff6","#de789d","#f09286","#f2a984","#f6c983","#f9da85","#fef9a1","#ebf29b","#badc94", + "#a5e1fa","#adc5fa","#ab8df7","#d696f8","#e8a7bf","#f4b8b1","#f6c7af","#f9daae","#fae5af","#fefbc0","#f3f7be","#d2e7ba", + "#d2effd","#d6e1fc","#d6c9fa","#e9cbfb","#f3d4df","#f9dcd9","#fae3d8","#fcecd7","#fdf2d8","#fefce0","#f7fade","#e3edd6" + }; + + [Inject] IEventListenerFactory ThrottledEventManagerFactory { get; set; } + + #endregion + + #region Events + private EventCallback GetEventCallback() => EventCallback.Factory.Create(this, () => Close()); + private EventCallback GetSelectPaletteColorCallback(Color color) => new EventCallbackFactory().Create(this, (MouseEventArgs e) => SelectPaletteColor(color)); + + /// + /// Set the R (red) component of the color picker + /// + /// A value between 0 (no red) or 255 (max red) + public void SetR(int value) => Value = Value.SetR(value); + + /// + /// Set the G (green) component of the color picker + /// + /// A value between 0 (no green) or 255 (max green) + public void SetG(int value) => Value = Value.SetG(value); + + /// + /// Set the B (blue) component of the color picker + /// + /// A value between 0 (no blue) or 255 (max blue) + public void SetB(int value) => Value = Value.SetB(value); + + /// + /// Set the H (hue) component of the color picker + /// + /// A value between 0 and 360 (degrees) + public void SetH(double value) => Value = Value.SetH(value); + + /// + /// Set the S (saturation) component of the color picker + /// + /// A value between 0.0 (no saturation) and 1.0 (max saturation) + public void SetS(double value) => Value = Value.SetS(value); + + /// + /// Set the L (Lightness) component of the color picker + /// + /// A value between 0.0 (no light, black) and 1.0 (max light, white) + public void SetL(double value) => Value = Value.SetL(value); + + /// + /// Set the Alpha (transparency) component of the color picker + /// + /// A value between 0.0 (full transparent) and 1.0 (solid) + public void SetAlpha(double value) => Value = Value.SetAlpha(value); + + /// + /// Set the Alpha (transparency) component of the color picker + /// + /// A value between 0 (full transparent) and 1 (solid) + public void SetAlpha(int value) => Value = Value.SetAlpha(value); + + /// + /// Set the color of the picker based on the string input + /// + /// Accepting different formats for a color representation such as rbg, rgba, # + public void SetInputString(string input) + { + Color color; + try + { + color = new Color(input); + } + catch (Exception) + { + return; + } + + Value = color; + } + + protected override Task StringValueChanged(string value) + { + SetInputString(value); + return Task.CompletedTask; + } + + private bool _attachedMouseEvent = false; + + protected override void OnPickerOpened() + { + base.OnPickerOpened(); + _attachedMouseEvent = true; + StateHasChanged(); + } + + protected override void OnPickerClosed() + { + base.OnPickerClosed(); + RemoveMouseOverEvent().AndForget(); + } + private void HandleColorOverlayClicked() + { + UpdateColorBaseOnSelection(); + + if (IsAnyControlVisible() == false) + { + Close(); + } + } + + private void OnSelectorClicked(MouseEventArgs e) + { + SetSelectorBasedOnMouseEvents(e, false); + HandleColorOverlayClicked(); + } + + private void OnColorOverlayClick(MouseEventArgs e) + { + SetSelectorBasedOnMouseEvents(e, true); + HandleColorOverlayClicked(); + } + + private void OnMouseOver(MouseEventArgs e) + { + if (e.Buttons == 1) + { + SetSelectorBasedOnMouseEvents(e, true); + UpdateColorBaseOnSelection(); + } + } + + private void SetSelectorBasedOnMouseEvents(MouseEventArgs e, bool offsetIsAbsolute) + { + _selectorX = (offsetIsAbsolute == true ? e.OffsetX : (e.OffsetX - _selctorSize / 2.0) + _selectorX).EnsureRange(_maxX); + _selectorY = (offsetIsAbsolute == true ? e.OffsetY : (e.OffsetY - _selctorSize / 2.0) + _selectorY).EnsureRange(_maxY); + } + + [Parameter] public EventCallback ValueChanged { get; set; } + + private void ToggleCollection() + { + _collectionOpen = !_collectionOpen; + } + + private void SelectPaletteColor(Color color) + { + Value = color; + _collectionOpen = false; + + if ( + IsAnyControlVisible() == false || _activeColorPickerView is ColorPickerView.GridCompact or ColorPickerView.Palette) + { + Close(); + } + } + + public void ChangeMode() => + ColorPickerMode = ColorPickerMode switch + { + ColorPickerMode.RGB => ColorPickerMode.HSL, + ColorPickerMode.HSL => ColorPickerMode.HEX, + ColorPickerMode.HEX => ColorPickerMode.RGB, + _ => ColorPickerMode.RGB, + }; + + public async Task ChangeView(ColorPickerView value) + { + + var oldValue = _activeColorPickerView; + + _activeColorPickerView = value; + Text = GetColorTextValue(); + + if (oldValue == ColorPickerView.Spectrum) + { + await RemoveMouseOverEvent(); + } + + if (value == ColorPickerView.Spectrum) + { + _attachedMouseEvent = true; + } + } + + private void UpdateBaseColorSlider(int value) + { + var diff = Math.Abs(value - (int)Value.H); + if (diff == 0) { return; } + + Value = Value.SetH(value); + } + + private void UpdateBaseColor() + { + var index = (int)_color.H / 60; + if (index == 6) + { + index = 5; + } + + var valueInDeg = (int)_color.H - (index * 60); + var value = (int)(MathExtensions.Map(0, 60, 0, 255, valueInDeg)); + var section = _rgbToHueMapper[index]; + + _baseColor = new(section.r(value), section.g(value), section.b(value), 255); + } + + private void UpdateColorBaseOnSelection() + { + var x = _selectorX / _maxX; + + var r_x = 255 - (int)((255 - _baseColor.R) * x); + var g_x = 255 - (int)((255 - _baseColor.G) * x); + var b_x = 255 - (int)((255 - _baseColor.B) * x); + + var y = 1.0 - _selectorY / _maxY; + + var r = r_x * y; + var g = g_x * y; + var b = b_x * y; + + _skipFeedback = true; + //in this mode, H is expected to be stable, so copy H value + Value = new Color((byte)r, (byte)g, (byte)b, _color); + _skipFeedback = false; + } + + private void UpdateColorSelectorBasedOnRgb() + { + var hueValue = (int)MathExtensions.Map(0, 360, 0, 6 * 255, _color.H); + var index = hueValue / 255; + if (index == 6) + { + index = 5; + } + + var section = _rgbToHueMapper[index]; + + var colorValues = section.dominantColorPart switch + { + "rb" => (_color.R, _color.B), + "rg" => (_color.R, _color.G), + "gb" => (_color.G, _color.B), + "gr" => (_color.G, _color.R), + "br" => (_color.B, _color.R), + "bg" => (_color.B, _color.G), + _ => (255, 255) + }; + + var primaryDiff = 255 - colorValues.Item1; + var primaryDiffDelta = colorValues.Item1 / 255.0; + + _selectorY = MathExtensions.Map(0, 255, 0, _maxY, primaryDiff); + + var secondaryColorX = colorValues.Item2 * (1.0 / primaryDiffDelta); + var relation = (255 - secondaryColorX) / 255.0; + + _selectorX = relation * _maxX; + } + + #endregion + + #region Styling + + private bool IsAnyControlVisible() => !(DisablePreview && DisableSliders && DisableInputs); + private ThemeColor GetButtonColor(ColorPickerView view) => _activeColorPickerView == view ? ThemeColor.Primary : ThemeColor.Inherit; + private string GetColorDotClass(Color color) => new CssBuilder("picker-color-dot").AddClass("selected", color == Value).ToString(); + private string AlphaSliderStyle => new StyleBuilder().AddStyle($"background-image: linear-gradient(to {(RightToLeft ? "left" : "right")}, transparent, {_color.ToString(ColorOutputFormats.RGB)})").Build(); + private string GetSelectorLocation() => $"translate({Math.Round(_selectorX, 2).ToString(CultureInfo.InvariantCulture)}px, {Math.Round(_selectorY, 2).ToString(CultureInfo.InvariantCulture)}px);"; + private string GetColorTextValue() => (DisableAlpha == true || _activeColorPickerView is ColorPickerView.Palette or ColorPickerView.GridCompact) ? _color.ToString(ColorOutputFormats.Hex) : _color.ToString(ColorOutputFormats.HexA); + private int GetHexColorInputMaxLength() => DisableAlpha ? 7 : 9; + + /// + /// When set to true, no mouse move events in the spectrum mode will be captured, so the selector circle won't fellow the mouse. + /// Under some conditions like long latency the visual representation might not reflect the user behaviour anymore. So, it can be disabled + /// Enabled by default + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisableDragEffect { get; set; } = false; + + /// + /// Custom close icon. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string CloseIcon { get; set; } = Icons.Material.Filled.Close; + + /// + /// Custom spectrum icon. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string SpectrumIcon { get; set; } = Icons.Material.Filled.Tune; + + /// + /// Custom grid icon. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string GridIcon { get; set; } = Icons.Material.Filled.Apps; + + /// + /// Custom palette icon. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string PaletteIcon { get; set; } = Icons.Material.Filled.Palette; + + /// + /// Custom import/export icont. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string ImportExportIcon { get; set; } = Icons.Material.Filled.ImportExport; + #endregion + + #region Behavior + [CascadingParameter(Name = "RightToLeft")] public bool RightToLeft { get; set; } + + /// + /// If true, Alpha options will not be displayed and color output will be RGB, HSL or HEX and not RGBA, HSLA or HEXA. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisableAlpha + { + get => _disableAlpha; + set + { + if (value != _disableAlpha) + { + _disableAlpha = value; + + if (value == true) + { + Value = Value.SetAlpha(1.0); + } + + Text = GetColorTextValue(); + } + } + } + + /// + /// If true, the color field will not be displayed. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisableColorField { get; set; } = false; + + /// + /// If true, the switch to change color mode will not be displayed. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisableModeSwitch { get; set; } = false; + + /// + /// If true, textfield inputs and color mode switch will not be displayed. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisableInputs { get; set; } = false; + + /// + /// If true, hue and alpha sliders will not be displayed. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisableSliders { get; set; } = false; + + /// + /// If true, the preview color box will not be displayed, note that the preview color functions as a button as well for collection colors. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool DisablePreview { get; set; } = false; + + /// + /// The initial mode (RGB, HSL or HEX) the picker should open. Defaults to RGB + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public ColorPickerMode ColorPickerMode { get; set; } = ColorPickerMode.RGB; + + + + /// + /// The initial view of the picker. Views can be changed if toolbar is enabled. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public ColorPickerView ColorPickerView + { + get => _colorPickerView; + set + { + if (value != _colorPickerView) + { + _colorPickerView = value; + ChangeView(value).AndForget(); + } + } + } + + /// + /// If true, binding changes occurred also when HSL values changed without a corresponding RGB change + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Behavior)] + public bool UpdateBindingIfOnlyHSLChanged { get; set; } = false; + + /// + /// A two-way bindable property representing the selected value. MudColor is a utility class that can be used to get the value as RGB, HSL, hex or other value + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Data)] + public Color Value + { + get => _color; + set + { + if (value == null) { return; } + + var rgbChanged = value != _color; + var hslChanged = _color == null ? false : value.HslChanged(_color); + _color = value; + + if (rgbChanged) + { + if (_skipFeedback == false) + { + UpdateBaseColor(); + UpdateColorSelectorBasedOnRgb(); + } + + SetTextAsync(GetColorTextValue(), false).AndForget(); + ValueChanged.InvokeAsync(value).AndForget(); + FieldChanged(value); + } + + if (rgbChanged == false && UpdateBindingIfOnlyHSLChanged && hslChanged == true) + { + SetTextAsync(GetColorTextValue(), false).AndForget(); + ValueChanged.InvokeAsync(value).AndForget(); + FieldChanged(value); + } + } + } + + + #endregion + + #region Lifecycle + + public ColorPicker() : base(new DefaultConverter()) + { + AdornmentIcon = Icons.Material.Outlined.Palette; + DisableToolbar = true; + Value = "#594ae2"; //MudBlazor Blue + Text = GetColorTextValue(); + AdornmentAriaLabel = "Open Color Picker"; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender == true) + { + if (PickerVariant == PickerVariant.Static) + { + await AddMouseOverEvent(); + } + } + + if (_attachedMouseEvent == true) + { + _attachedMouseEvent = false; + await AddMouseOverEvent(); + } + } + + private async Task AddMouseOverEvent() + { + if (DisableDragEffect == true) { return; } + + if (_throttledEventManager == null) + { + _throttledEventManager = ThrottledEventManagerFactory.Create(); + } + + _throttledMouseOverEventId = await + _throttledEventManager.Subscribe("mousemove", _id.ToString(), "mudEventProjections.correctOffset", 10, async (x) => + { + var e = x as MouseEventArgs; + await InvokeAsync(() => OnMouseOver(e)); + StateHasChanged(); + }); + } + + private Task RemoveMouseOverEvent() + { + if (_throttledMouseOverEventId == default) { return Task.CompletedTask; } + + return _throttledEventManager.Unsubscribe(_throttledMouseOverEventId); + } + + public async ValueTask DisposeAsync() + { + if (_throttledEventManager == null) { return; } + + await _throttledEventManager.DisposeAsync(); + } + + #endregion + } diff --git a/src/Connected.Components/Components/Container/Container.razor.cs b/src/Connected.Components/Components/Container/Container.razor.cs index f1746b3..fcdfaf4 100644 --- a/src/Connected.Components/Components/Container/Container.razor.cs +++ b/src/Connected.Components/Components/Container/Container.razor.cs @@ -5,9 +5,6 @@ namespace Connected.Components; public partial class Container : UIComponent { - #region Event callbacks - #endregion - #region Content placeholders /// diff --git a/src/Connected.Components/Components/DatePicker/DatePickerBase.razor.cs b/src/Connected.Components/Components/DatePicker/DatePickerBase.razor.cs index 8062746..9bb9cea 100644 --- a/src/Connected.Components/Components/DatePicker/DatePickerBase.razor.cs +++ b/src/Connected.Components/Components/DatePicker/DatePickerBase.razor.cs @@ -8,223 +8,100 @@ namespace Connected.Components; public abstract partial class DatePickerBase : Picker { + #region Variables private bool _dateFormatTouched; - - protected DatePickerBase() : base(new DefaultConverter - { - Format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern, - Culture = CultureInfo.CurrentCulture - }) - { - AdornmentAriaLabel = "Open Date Picker"; - } - - [Inject] protected IScrollManager ScrollManager { get; set; } - + private DateTime? _picker_month; + protected virtual bool IsRange { get; } = false; + protected OpenTo CurrentView; /// - /// Max selectable date. + /// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker. /// - [Parameter] - [Category(CategoryTypes.FormComponent.Validation)] - public DateTime? MaxDate { get; set; } + private string _componentId = Guid.NewGuid().ToString(); /// - /// Min selectable date. + /// Is set to true to scroll to the actual year after the next render /// - [Parameter] - [Category(CategoryTypes.FormComponent.Validation)] - public DateTime? MinDate { get; set; } + private bool _scrollToYearAfterRender = false; + #endregion - /// - /// First view to show in the MudDatePicker. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public OpenTo OpenTo { get; set; } = OpenTo.Date; + #region Events + private Typo GetMonthTypo(DateTime month) + { + if (GetMonthStart(0) == month) + return Typo.h5; + return Typo.subtitle1; + } - /// - /// String Format for selected date view - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Behavior)] - public string DateFormat + protected abstract DateTime GetCalendarStartOfMonth(); + + private int GetCalendarDayOfMonth(DateTime date) { - get - { - return (Converter as DefaultConverter)?.Format; - } - set - { - if (Converter is DefaultConverter defaultConverter) - { - defaultConverter.Format = value; - _dateFormatTouched = true; - } - DateFormatChanged(value); - } + return Culture.Calendar.GetDayOfMonth(date); } /// - /// Date format value change hook for descendants. + /// Converts gregorian year into whatever year it is in the provided culture /// - protected virtual Task DateFormatChanged(string newFormat) + /// Gregorian year + /// Year according to culture + protected abstract int GetCalendarYear(int year); + public async void ScrollToYear() { - return Task.CompletedTask; + _scrollToYearAfterRender = false; + var id = $"{_componentId}{GetMonthStart(0).Year}"; + await ScrollManager.ScrollToYearAsync(id); + StateHasChanged(); } - protected override bool SetCulture(CultureInfo value) + private int GetMinYear() { - if (!base.SetCulture(value)) - return false; - - if (!_dateFormatTouched && Converter is DefaultConverter defaultConverter) - defaultConverter.Format = value.DateTimeFormat.ShortDatePattern; - - return true; + if (MinDate.HasValue) + return MinDate.Value.Year; + return DateTime.Today.Year - 100; } - /// - /// Defines on which day the week starts. Depends on the value of Culture. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public DayOfWeek? FirstDayOfWeek { get; set; } = null; - - /// - /// The current month of the date picker (two-way bindable). This changes when the user browses through the calender. - /// The month is represented as a DateTime which is always the first day of that month. You can also set this to define which month is initially shown. If not set, the current month is shown. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public DateTime? PickerMonth + private int GetMaxYear() { - get => _picker_month; - set - { - if (value == _picker_month) - return; - _picker_month = value; - InvokeAsync(StateHasChanged); - PickerMonthChanged.InvokeAsync(value); - } + if (MaxDate.HasValue) + return MaxDate.Value.Year; + return DateTime.Today.Year + 100; } - private DateTime? _picker_month; - /// - /// Fired when the date changes. - /// - [Parameter] public EventCallback PickerMonthChanged { get; set; } - /// - /// Sets the amount of time in milliseconds to wait before closing the picker. This helps the user see that the date was selected before the popover disappears. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public int ClosingDelay { get; set; } = 100; - - /// - /// Number of months to display in the calendar - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public int DisplayMonths { get; set; } = 1; - - /// - /// Maximum number of months in one row - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public int? MaxMonthColumns { get; set; } - - /// - /// Start month when opening the picker. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public DateTime? StartMonth { get; set; } - - /// - /// Display week numbers according to the Culture parameter. If no culture is defined, CultureInfo.CurrentCulture will be used. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool ShowWeekNumbers { get; set; } - - /// - /// Format of the selected date in the title. By default, this is "ddd, dd MMM" which abbreviates day and month names. - /// For instance, display the long names like this "dddd, dd. MMMM". - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public string TitleDateFormat { get; set; } = "ddd, dd MMM"; - - /// - /// If AutoClose is set to true and PickerActions are defined, selecting a day will close the MudDatePicker. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public bool AutoClose { get; set; } - - /// - /// Function to determine whether a date is disabled - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Validation)] - public Func IsDateDisabledFunc + private Typo GetYearTypo(int year) { - get => _isDateDisabledFunc; - set - { - _isDateDisabledFunc = value ?? (_ => false); - } + if (year == GetMonthStart(0).Year) + return Typo.h5; + return Typo.subtitle1; } - private Func _isDateDisabledFunc = _ => false; - /// - /// Function to conditionally apply new classes to specific days - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Appearance)] - public Func AdditionalDateClassesFunc { get; set; } - - /// - /// Custom previous icon. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft; - - /// - /// Custom next icon. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerAppearance)] - public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight; + private void OnFormattedDateClick() + { + // todo: raise an event the user can handle + } - /// - /// Set a predefined fix year - no year can be selected - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public int? FixYear { get; set; } - /// - /// Set a predefined fix month - no month can be selected - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public int? FixMonth { get; set; } - /// - /// Set a predefined fix day - no day can be selected - /// - [Parameter] - [Category(CategoryTypes.FormComponent.PickerBehavior)] - public int? FixDay { get; set; } - protected virtual bool IsRange { get; } = false; + private IEnumerable GetAllMonths() + { + var current = GetMonthStart(0); + var calendarYear = Culture.Calendar.GetYear(current); + var firstOfCalendarYear = Culture.Calendar.ToDateTime(calendarYear, 1, 1, 0, 0, 0, 0); + for (var i = 0; i < Culture.Calendar.GetMonthsInYear(calendarYear); i++) + yield return Culture.Calendar.AddMonths(firstOfCalendarYear, i); + } - protected OpenTo CurrentView; + private string GetAbbreviatedMonthName(DateTime month) + { + var calendarMonth = Culture.Calendar.GetMonth(month); + return Culture.DateTimeFormat.AbbreviatedMonthNames[calendarMonth - 1]; + } + private string GetMonthName(DateTime month) + { + var calendarMonth = Culture.Calendar.GetMonth(month); + return Culture.DateTimeFormat.MonthNames[calendarMonth - 1]; + } protected override void OnPickerOpened() { base.OnPickerOpened(); @@ -461,39 +338,73 @@ public abstract partial class DatePickerBase : Picker _scrollToYearAfterRender = true; } } - /// - /// We need a random id for the year items in the year list so we can scroll to the item safely in every DatePicker. + /// Date format value change hook for descendants. /// - private string _componentId = Guid.NewGuid().ToString(); + protected virtual Task DateFormatChanged(string newFormat) + { + return Task.CompletedTask; + } + + protected override bool SetCulture(CultureInfo value) + { + if (!base.SetCulture(value)) + return false; + + if (!_dateFormatTouched && Converter is DefaultConverter defaultConverter) + defaultConverter.Format = value.DateTimeFormat.ShortDatePattern; + return true; + } /// - /// Is set to true to scroll to the actual year after the next render + /// Fired when the date changes. /// - private bool _scrollToYearAfterRender = false; + [Parameter] public EventCallback PickerMonthChanged { get; set; } - public async void ScrollToYear() - { - _scrollToYearAfterRender = false; - var id = $"{_componentId}{GetMonthStart(0).Year}"; - await ScrollManager.ScrollToYearAsync(id); - StateHasChanged(); - } + #endregion - private int GetMinYear() + #region Content + [Inject] protected IScrollManager ScrollManager { get; set; } + /// + /// Defines on which day the week starts. Depends on the value of Culture. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public DayOfWeek? FirstDayOfWeek { get; set; } = null; + + /// + /// The current month of the date picker (two-way bindable). This changes when the user browses through the calender. + /// The month is represented as a DateTime which is always the first day of that month. You can also set this to define which month is initially shown. If not set, the current month is shown. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public DateTime? PickerMonth { - if (MinDate.HasValue) - return MinDate.Value.Year; - return DateTime.Today.Year - 100; + get => _picker_month; + set + { + if (value == _picker_month) + return; + _picker_month = value; + InvokeAsync(StateHasChanged); + PickerMonthChanged.InvokeAsync(value); + } } + #endregion - private int GetMaxYear() + #region Styling + private string GetMonthClasses(DateTime month) { - if (MaxDate.HasValue) - return MaxDate.Value.Year; - return DateTime.Today.Year + 100; + if (GetMonthStart(0) == month) + return $"picker-month-selected mud-{Color.ToDescription()}-text"; + return null; } - + /// + /// Function to conditionally apply new classes to specific days + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Appearance)] + public Func AdditionalDateClassesFunc { get; set; } private string GetYearClasses(int year) { if (year == GetMonthStart(0).Year) @@ -508,56 +419,161 @@ public abstract partial class DatePickerBase : Picker .AddClass($"picker-calendar-header-last", month == DisplayMonths - 1) .Build(); } + /// + /// Custom previous icon. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string PreviousIcon { get; set; } = Icons.Material.Filled.ChevronLeft; - private Typo GetYearTypo(int year) - { - if (year == GetMonthStart(0).Year) - return Typo.h5; - return Typo.subtitle1; - } + /// + /// Custom next icon. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public string NextIcon { get; set; } = Icons.Material.Filled.ChevronRight; - private void OnFormattedDateClick() + + + /// + /// String Format for selected date view + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Behavior)] + public string DateFormat { - // todo: raise an event the user can handle + get + { + return (Converter as DefaultConverter)?.Format; + } + set + { + if (Converter is DefaultConverter defaultConverter) + { + defaultConverter.Format = value; + _dateFormatTouched = true; + } + DateFormatChanged(value); + } } + #endregion + #region Behavior + /// + /// Max selectable date. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Validation)] + public DateTime? MaxDate { get; set; } - private IEnumerable GetAllMonths() - { - var current = GetMonthStart(0); - var calendarYear = Culture.Calendar.GetYear(current); - var firstOfCalendarYear = Culture.Calendar.ToDateTime(calendarYear, 1, 1, 0, 0, 0, 0); - for (var i = 0; i < Culture.Calendar.GetMonthsInYear(calendarYear); i++) - yield return Culture.Calendar.AddMonths(firstOfCalendarYear, i); - } + /// + /// Min selectable date. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Validation)] + public DateTime? MinDate { get; set; } - private string GetAbbreviatedMonthName(DateTime month) - { - var calendarMonth = Culture.Calendar.GetMonth(month); - return Culture.DateTimeFormat.AbbreviatedMonthNames[calendarMonth - 1]; - } + /// + /// First view to show in the DatePicker. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public OpenTo OpenTo { get; set; } = OpenTo.Date; - private string GetMonthName(DateTime month) - { - var calendarMonth = Culture.Calendar.GetMonth(month); - return Culture.DateTimeFormat.MonthNames[calendarMonth - 1]; - } + /// + /// Set a predefined fix year - no year can be selected + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public int? FixYear { get; set; } + /// + /// Set a predefined fix month - no month can be selected + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public int? FixMonth { get; set; } + /// + /// Set a predefined fix day - no day can be selected + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public int? FixDay { get; set; } + /// + /// Sets the amount of time in milliseconds to wait before closing the picker. This helps the user see that the date was selected before the popover disappears. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public int ClosingDelay { get; set; } = 100; - private string GetMonthClasses(DateTime month) - { - if (GetMonthStart(0) == month) - return $"picker-month-selected mud-{Color.ToDescription()}-text"; - return null; - } + /// + /// Number of months to display in the calendar + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public int DisplayMonths { get; set; } = 1; - private Typo GetMonthTypo(DateTime month) + /// + /// Maximum number of months in one row + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerAppearance)] + public int? MaxMonthColumns { get; set; } + + /// + /// Start month when opening the picker. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public DateTime? StartMonth { get; set; } + + /// + /// Display week numbers according to the Culture parameter. If no culture is defined, CultureInfo.CurrentCulture will be used. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool ShowWeekNumbers { get; set; } + + /// + /// Format of the selected date in the title. By default, this is "ddd, dd MMM" which abbreviates day and month names. + /// For instance, display the long names like this "dddd, dd. MMMM". + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public string TitleDateFormat { get; set; } = "ddd, dd MMM"; + + /// + /// If AutoClose is set to true and PickerActions are defined, selecting a day will close the MudDatePicker. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.PickerBehavior)] + public bool AutoClose { get; set; } + + /// + /// Function to determine whether a date is disabled + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Validation)] + public Func IsDateDisabledFunc { - if (GetMonthStart(0) == month) - return Typo.h5; - return Typo.subtitle1; + get => _isDateDisabledFunc; + set + { + _isDateDisabledFunc = value ?? (_ => false); + } } + private Func _isDateDisabledFunc = _ => false; + #endregion + #region Lifecycle + protected DatePickerBase() : base(new DefaultConverter + { + Format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern, + Culture = CultureInfo.CurrentCulture + }) + { + AdornmentAriaLabel = "Open Date Picker"; + } protected override void OnInitialized() { @@ -584,17 +600,6 @@ public abstract partial class DatePickerBase : Picker ScrollToYear(); } - protected abstract DateTime GetCalendarStartOfMonth(); - - private int GetCalendarDayOfMonth(DateTime date) - { - return Culture.Calendar.GetDayOfMonth(date); - } - - /// - /// Converts gregorian year into whatever year it is in the provided culture - /// - /// Gregorian year - /// Year according to culture - protected abstract int GetCalendarYear(int year); + #endregion + } diff --git a/src/Connected.Components/Components/DatePicker/DateRangePicker.razor.cs b/src/Connected.Components/Components/DatePicker/DateRangePicker.razor.cs index ebf569e..af0723e 100644 --- a/src/Connected.Components/Components/DatePicker/DateRangePicker.razor.cs +++ b/src/Connected.Components/Components/DatePicker/DateRangePicker.razor.cs @@ -7,297 +7,305 @@ namespace Connected.Components; public partial class DateRangePicker : DatePickerBase { - private DateTime? _firstDate = null, _secondDate; - private DateRange _dateRange; - private Range _rangeText; - - protected override bool IsRange => true; - - public DateRangePicker() - { - DisplayMonths = 2; - AdornmentAriaLabel = "Open Date Range Picker"; - } - - /// - /// Fired when the DateFormat changes. - /// - [Parameter] public EventCallback DateRangeChanged { get; set; } - - /// - /// The currently selected range (two-way bindable). If null, then nothing was selected. - /// - [Parameter] - [Category(CategoryTypes.FormComponent.Data)] - public DateRange DateRange - { - get => _dateRange; - set => SetDateRangeAsync(value, true).AndForget(); - } - - protected async Task SetDateRangeAsync(DateRange range, bool updateValue) - { - if (_dateRange != range) - { - var doesRangeContainDisabledDates = range?.Start != null && range?.End != null && Enumerable - .Range(0, int.MaxValue) - .Select(index => range.Start.Value.AddDays(index)) - .TakeWhile(date => date <= range.End.Value) - .Any(date => IsDateDisabledFunc(date.Date)); - - if (doesRangeContainDisabledDates) - { - _rangeText = null; - await SetTextAsync(null, false); - return; - } - - _dateRange = range; - _value = range?.End; - - if (updateValue) - { - if (_dateRange == null) - { - _rangeText = null; - await SetTextAsync(null, false); - } - else - { - _rangeText = new Range( - Converter.Convert(_dateRange.Start), - Converter.Convert(_dateRange.End)); - await SetTextAsync(_dateRange.ToString(Converter), false); - } - } - - await DateRangeChanged.InvokeAsync(_dateRange); - BeginValidate(); - } - } - - private Range RangeText - { - get => _rangeText; - set - { - if (_rangeText?.Equals(value) ?? value == null) - return; - - Modified = true; - _rangeText = value; - SetDateRangeAsync(ParseDateRangeValue(value?.Start, value?.End), false).AndForget(); - } - } - - private RangeInput _rangeInput; - - /// - /// Focuses the start date of MudDateRangePicker - /// - /// - public ValueTask FocusStartAsync() => _rangeInput.FocusStartAsync(); - - /// - /// Selects the start date of MudDateRangePicker - /// - /// - public ValueTask SelectStartAsync() => _rangeInput.SelectStartAsync(); - - /// - /// Selects the specified range of the start date text - /// - /// Start position of the selection - /// End position of the selection - /// - public ValueTask SelectRangeStartAsync(int pos1, int pos2) => _rangeInput.SelectRangeStartAsync(pos1, pos2); - - /// - /// Focuses the end date of MudDateRangePicker - /// - /// - public ValueTask FocusEndAsync() => _rangeInput.FocusEndAsync(); - - /// - /// Selects the end date of MudDateRangePicker - /// - /// - public ValueTask SelectEndAsync() => _rangeInput.SelectEndAsync(); - - /// - /// Selects the specified range of the end date text - /// - /// Start position of the selection - /// End position of the selection - /// - public ValueTask SelectRangeEndAsync(int pos1, int pos2) => _rangeInput.SelectRangeEndAsync(pos1, pos2); - - protected override Task DateFormatChanged(string newFormat) - { - Modified = true; - return SetTextAsync(_dateRange?.ToString(Converter), false); - } - - protected override Task StringValueChanged(string value) - { - Modified = true; - // Update the daterange property (without updating back the Value property) - return SetDateRangeAsync(ParseDateRangeValue(value), false); - } - - protected override bool HasValue(DateTime? value) - { - return null != value && value.HasValue; - } - - private DateRange ParseDateRangeValue(string value) - { - return DateRange.TryParse(value, Converter, out var dateRange) ? dateRange : null; - } - - private DateRange ParseDateRangeValue(string start, string end) - { - return DateRange.TryParse(start, end, Converter, out var dateRange) ? dateRange : null; - } - - protected override void OnPickerClosed() - { - _firstDate = null; - base.OnPickerClosed(); - } - - protected override string GetDayClasses(int month, DateTime day) - { - var b = new CssBuilder("day"); - if (day < GetMonthStart(month) || day > GetMonthEnd(month)) - { - return b.AddClass("hidden").Build(); - } - - if ((_firstDate != null && _secondDate != null && _firstDate < day && _secondDate > day) || - (_firstDate == null && _dateRange != null && _dateRange.Start < day && _dateRange.End > day)) - { - return b - .AddClass("range") - .AddClass("range-between") - .Build(); - } - - if ((_firstDate != null && day == _firstDate) || - (_firstDate == null && _dateRange != null && _dateRange.Start == day && DateRange.End != day)) - { - return b.AddClass("selected") - .AddClass("range") - .AddClass("range-start-selected") - .AddClass("range-selection", _firstDate != null) - .AddClass($"theme-{Color.ToDescription()}") - .Build(); - } - - if ((_firstDate != null && _secondDate != null && day == _secondDate) || - (_firstDate == null && _dateRange != null && _dateRange.Start != day && _dateRange.End == day)) - { - return b.AddClass("selected") - .AddClass("range") - .AddClass("range-end-selected") - .AddClass($"theme-{Color.ToDescription()}") - .Build(); - } - - if (_firstDate == null && _dateRange != null && _dateRange.Start == _dateRange.End && _dateRange.Start == day) - { - return b.AddClass("selected").AddClass($"theme-{Color.ToDescription()}").Build(); - } - else if (_firstDate != null && day > _firstDate) - { - return b.AddClass("range") - .AddClass("range-selection", _secondDate == null) - .AddClass($"range-selection-{Color.ToDescription()}", _firstDate != null) - .Build(); - } - - if (day == DateTime.Today) - { - return b.AddClass("current") - .AddClass("range", _firstDate != null && day > _firstDate) - .AddClass("range-selection", _firstDate != null && day > _firstDate) - .AddClass($"range-selection-{Color.ToDescription()}", _firstDate != null && day > _firstDate) - .AddClass($"{Color.ToDescription()}-text") - .Build(); - } - - return b.Build(); - } - - protected override async void OnDayClicked(DateTime dateTime) - { - if (_firstDate == null || _firstDate > dateTime || _secondDate != null) - { - _secondDate = null; - _firstDate = dateTime; - return; - } - - _secondDate = dateTime; - if (PickerActions == null || AutoClose) - { - Submit(); - - if (PickerVariant != PickerVariant.Static) - { - await Task.Delay(ClosingDelay); - Close(false); - } - } - } - - protected override void OnOpened() - { - _secondDate = null; - base.OnOpened(); - } - - protected internal override async void Submit() - { - if (ReadOnly) - return; - if (_firstDate == null || _secondDate == null) - return; - - await SetDateRangeAsync(new DateRange(_firstDate, _secondDate), true); - - _firstDate = null; - _secondDate = null; - } - - public override void Clear(bool close = true) - { - DateRange = null; - _firstDate = _secondDate = null; - base.Clear(); - } - - protected override string GetTitleDateString() - { - if (_firstDate != null) - return $"{FormatTitleDate(_firstDate)} - {FormatTitleDate(_secondDate)}"; - - return DateRange?.Start != null - ? $"{FormatTitleDate(DateRange.Start)} - {FormatTitleDate(DateRange.End)}" - : ""; - } - - protected override DateTime GetCalendarStartOfMonth() - { - var date = StartMonth ?? DateRange?.Start ?? DateTime.Today; - return date.StartOfMonth(Culture); - } - - protected override int GetCalendarYear(int year) - { - var date = DateRange?.Start ?? DateTime.Today; - var diff = date.Year - year; - var calenderYear = Culture.Calendar.GetYear(date); - return calenderYear - diff; - } + #region Variables + private DateTime? _firstDate = null, _secondDate; + private DateRange _dateRange; + private Range _rangeText; + protected override bool IsRange => true; + private RangeInput _rangeInput; + /// + /// The currently selected range (two-way bindable). If null, then nothing was selected. + /// + [Parameter] + [Category(CategoryTypes.FormComponent.Data)] + public DateRange DateRange + { + get => _dateRange; + set => SetDateRangeAsync(value, true).AndForget(); + } + #endregion + + #region Events + + protected override async void OnDayClicked(DateTime dateTime) + { + if (_firstDate == null || _firstDate > dateTime || _secondDate != null) + { + _secondDate = null; + _firstDate = dateTime; + return; + } + + _secondDate = dateTime; + if (PickerActions == null || AutoClose) + { + Submit(); + + if (PickerVariant != PickerVariant.Static) + { + await Task.Delay(ClosingDelay); + Close(false); + } + } + } + + protected override void OnOpened() + { + _secondDate = null; + base.OnOpened(); + } + + protected internal override async void Submit() + { + if (ReadOnly) + return; + if (_firstDate == null || _secondDate == null) + return; + + await SetDateRangeAsync(new DateRange(_firstDate, _secondDate), true); + + _firstDate = null; + _secondDate = null; + } + + public override void Clear(bool close = true) + { + DateRange = null; + _firstDate = _secondDate = null; + base.Clear(); + } + protected override Task DateFormatChanged(string newFormat) + { + Modified = true; + return SetTextAsync(_dateRange?.ToString(Converter), false); + } + + protected override Task StringValueChanged(string value) + { + Modified = true; + // Update the daterange property (without updating back the Value property) + return SetDateRangeAsync(ParseDateRangeValue(value), false); + } + /// + /// Fired when the DateFormat changes. + /// + [Parameter] public EventCallback DateRangeChanged { get; set; } + protected async Task SetDateRangeAsync(DateRange range, bool updateValue) + { + if (_dateRange != range) + { + var doesRangeContainDisabledDates = range?.Start != null && range?.End != null && Enumerable + .Range(0, int.MaxValue) + .Select(index => range.Start.Value.AddDays(index)) + .TakeWhile(date => date <= range.End.Value) + .Any(date => IsDateDisabledFunc(date.Date)); + + if (doesRangeContainDisabledDates) + { + _rangeText = null; + await SetTextAsync(null, false); + return; + } + + _dateRange = range; + _value = range?.End; + + if (updateValue) + { + if (_dateRange == null) + { + _rangeText = null; + await SetTextAsync(null, false); + } + else + { + _rangeText = new Range( + Converter.Convert(_dateRange.Start), + Converter.Convert(_dateRange.End)); + await SetTextAsync(_dateRange.ToString(Converter), false); + } + } + + await DateRangeChanged.InvokeAsync(_dateRange); + BeginValidate(); + } + } + #endregion + + #region Content + + private Range RangeText + { + get => _rangeText; + set + { + if (_rangeText?.Equals(value) ?? value == null) + return; + + Modified = true; + _rangeText = value; + SetDateRangeAsync(ParseDateRangeValue(value?.Start, value?.End), false).AndForget(); + } + } + + #endregion + + #region Styling + protected override string GetDayClasses(int month, DateTime day) + { + var b = new CssBuilder("day"); + if (day < GetMonthStart(month) || day > GetMonthEnd(month)) + { + return b.AddClass("hidden").Build(); + } + + if ((_firstDate != null && _secondDate != null && _firstDate < day && _secondDate > day) || + (_firstDate == null && _dateRange != null && _dateRange.Start < day && _dateRange.End > day)) + { + return b + .AddClass("range") + .AddClass("range-between") + .Build(); + } + + if ((_firstDate != null && day == _firstDate) || + (_firstDate == null && _dateRange != null && _dateRange.Start == day && DateRange.End != day)) + { + return b.AddClass("selected") + .AddClass("range") + .AddClass("range-start-selected") + .AddClass("range-selection", _firstDate != null) + .AddClass($"theme-{Color.ToDescription()}") + .Build(); + } + + if ((_firstDate != null && _secondDate != null && day == _secondDate) || + (_firstDate == null && _dateRange != null && _dateRange.Start != day && _dateRange.End == day)) + { + return b.AddClass("selected") + .AddClass("range") + .AddClass("range-end-selected") + .AddClass($"theme-{Color.ToDescription()}") + .Build(); + } + + if (_firstDate == null && _dateRange != null && _dateRange.Start == _dateRange.End && _dateRange.Start == day) + { + return b.AddClass("selected").AddClass($"theme-{Color.ToDescription()}").Build(); + } + else if (_firstDate != null && day > _firstDate) + { + return b.AddClass("range") + .AddClass("range-selection", _secondDate == null) + .AddClass($"range-selection-{Color.ToDescription()}", _firstDate != null) + .Build(); + } + + if (day == DateTime.Today) + { + return b.AddClass("current") + .AddClass("range", _firstDate != null && day > _firstDate) + .AddClass("range-selection", _firstDate != null && day > _firstDate) + .AddClass($"range-selection-{Color.ToDescription()}", _firstDate != null && day > _firstDate) + .AddClass($"{Color.ToDescription()}-text") + .Build(); + } + + return b.Build(); + } + #endregion + + #region Behavior + + protected override string GetTitleDateString() + { + if (_firstDate != null) + return $"{FormatTitleDate(_firstDate)} - {FormatTitleDate(_secondDate)}"; + + return DateRange?.Start != null + ? $"{FormatTitleDate(DateRange.Start)} - {FormatTitleDate(DateRange.End)}" + : ""; + } + + protected override DateTime GetCalendarStartOfMonth() + { + var date = StartMonth ?? DateRange?.Start ?? DateTime.Today; + return date.StartOfMonth(Culture); + } + + protected override int GetCalendarYear(int year) + { + var date = DateRange?.Start ?? DateTime.Today; + var diff = date.Year - year; + var calenderYear = Culture.Calendar.GetYear(date); + return calenderYear - diff; + } + protected override bool HasValue(DateTime? value) + { + return null != value && value.HasValue; + } + + private DateRange ParseDateRangeValue(string value) + { + return DateRange.TryParse(value, Converter, out var dateRange) ? dateRange : null; + } + + private DateRange ParseDateRangeValue(string start, string end) + { + return DateRange.TryParse(start, end, Converter, out var dateRange) ? dateRange : null; + } + /// + /// Focuses the start date of MudDateRangePicker + /// + /// + public ValueTask FocusStartAsync() => _rangeInput.FocusStartAsync(); + + /// + /// Selects the start date of MudDateRangePicker + /// + /// + public ValueTask SelectStartAsync() => _rangeInput.SelectStartAsync(); + + /// + /// Selects the specified range of the start date text + /// + /// Start position of the selection + /// End position of the selection + /// + public ValueTask SelectRangeStartAsync(int pos1, int pos2) => _rangeInput.SelectRangeStartAsync(pos1, pos2); + + /// + /// Focuses the end date of MudDateRangePicker + /// + /// + public ValueTask FocusEndAsync() => _rangeInput.FocusEndAsync(); + + /// + /// Selects the end date of MudDateRangePicker + /// + /// + public ValueTask SelectEndAsync() => _rangeInput.SelectEndAsync(); + + /// + /// Selects the specified range of the end date text + /// + /// Start position of the selection + /// End position of the selection + /// + public ValueTask SelectRangeEndAsync(int pos1, int pos2) => _rangeInput.SelectRangeEndAsync(pos1, pos2); + #endregion + + #region Lifecycle + public DateRangePicker() + { + DisplayMonths = 2; + AdornmentAriaLabel = "Open Date Range Picker"; + } + protected override void OnPickerClosed() + { + _firstDate = null; + base.OnPickerClosed(); + } + #endregion + } diff --git a/src/Connected.Components/Components/Input/Input.razor b/src/Connected.Components/Components/Input/Input.razor index e27444b..e0c8326 100644 --- a/src/Connected.Components/Components/Input/Input.razor +++ b/src/Connected.Components/Components/Input/Input.razor @@ -8,7 +8,7 @@ Variant="@Variant" HelperText="@HelperText" HelperTextOnFocus="@HelperTextOnFocus" - CounterText="@CounterText" + CounterText="@GetCounterText()" FullWidth="@FullWidth" class="@CompiledHelperContainerClassList.Build()" Error="@HasErrors" diff --git a/src/Connected.Components/Components/Input/Input.razor.cs b/src/Connected.Components/Components/Input/Input.razor.cs index 0c282bc..846623e 100644 --- a/src/Connected.Components/Components/Input/Input.razor.cs +++ b/src/Connected.Components/Components/Input/Input.razor.cs @@ -198,6 +198,11 @@ public partial class Input : InputBase /// [Parameter] public bool HideSpinButtons { get; set; } = true; + internal override InputType GetInputType() + { + return InputType; + } + /// /// Show clear button. /// @@ -230,7 +235,7 @@ public partial class Input : InputBase #region Input field class [Parameter] - public string InputClass { get; set; } = string.Empty; + public string Class { get; set; } = string.Empty; protected CssBuilder CompiledInputClass { get @@ -240,7 +245,7 @@ public partial class Input : InputBase .AddClass($"input-root-{Variant.ToDescription()}") .AddClass($"input-root-adorned-{Adornment.ToDescription()}", Adornment != Adornment.None) .AddClass($"input-root-margin-{Margin.ToDescription()}", when: () => Margin != Margin.None) - .AddClass(InputClass); + .AddClass(Class); } } #endregion @@ -315,7 +320,17 @@ public partial class Input : InputBase /// /// The current character counter, displayed below the text field. /// - [Parameter] public string CounterText { get; set; } + [Parameter] public bool ShowCharacterCounter { get; set; } + + /// + /// The current character counter, displayed below the text field. + /// + public string GetCounterText() + { + if (ShowCharacterCounter) + return Text.Length.ToString(); + return string.Empty; + } private System.Timers.Timer _timer; @@ -381,7 +396,7 @@ public partial class Input : InputBase [Parameter] public double Step { get; set; } = 1; - internal override InputType GetInputType() => InputType; + //internal override InputType GetInputType() => InputType; protected string InputTypeString => InputType.ToDescription(); public ElementReference ElementReference { get; private set; } private ElementReference _elementReference1;