using AntDesign.Select.Internal; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AntDesign.Internal; using AntDesign.JsInterop; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; #pragma warning disable 1591 // Disable missing XML comment #pragma warning disable CA1716 // Disable Select name warning #pragma warning disable CA1305 // IFormatProvider warning namespace AntDesign { public partial class Select { #region Parameters [Parameter] public bool AllowClear { get; set; } [Parameter] public bool AllowCustomTags { get; set; } [Parameter] public bool AutoClearSearchValue { get; set; } = true; [Parameter] public bool Bordered { get; set; } = true; [Parameter] public Action OnCreateCustomTag { get; set; } [Parameter] public bool DefaultActiveFirstItem { get; set; } = false; [Parameter] public bool Disabled { get; set; } [Parameter] public string DisabledName { get; set; } [Parameter] public Func DropdownRender { get; set; } [Parameter] public bool EnableSearch { get; set; } [Parameter] public string GroupName { get; set; } = string.Empty; [Parameter] public bool HideSelected { get; set; } [Parameter] public bool IgnoreItemChanges { get; set; } = true; [Parameter] public RenderFragment ItemTemplate { get; set; } /// /// LabelInValue can only be used if the SelectOption is not created by DataSource. /// [Parameter] public bool LabelInValue { get; set; } [Parameter] public string LabelName { get; set; } [Parameter] public RenderFragment LabelTemplate { get; set; } [Parameter] public bool Loading { get; set; } [Parameter] public string Mode { get; set; } = "default"; [Parameter] public RenderFragment NotFoundContent { get; set; } [Parameter] public Action OnBlur { get; set; } [Parameter] public Action OnClearSelected { get; set; } /// /// The OnDataSourceChanged event occurs after the DataSource property changes. /// /// /// It does not trigger if a value inside the IEnumerable<TItem> changes. /// [Parameter] public Action OnDataSourceChanged { get; set; } [Parameter] public Action OnDropdownVisibleChange { get; set; } [Parameter] public Action OnFocus { get; set; } [Parameter] public Action OnMouseEnter { get; set; } [Parameter] public Action OnMouseLeave { get; set; } [Parameter] public Action OnSearch { get; set; } [Parameter] public Action OnSelectedItemChanged { get; set; } [Parameter] public Action> OnSelectedItemsChanged { get; set; } [Parameter] public bool Open { get; set; } [Parameter] public string Placeholder { get; set; } [Parameter] public string PopupContainerMaxHeight { get; set; } = "256px"; [Parameter] public string PopupContainerSelector { get; set; } = "body"; [Parameter] public bool ShowArrowIcon { get; set; } = true; [Parameter] public bool ShowSearchIcon { get; set; } = true; [Parameter] public SortDirection SortByGroup { get; set; } = SortDirection.None; [Parameter] public SortDirection SortByLabel { get; set; } = SortDirection.None; [Parameter] public RenderFragment SuffixIcon { get; set; } [Parameter] public char[] TokenSeparators { get; set; } [Parameter] public override EventCallback ValueChanged { get; set; } [Parameter] public string ValueName { get; set; } [Parameter] public EventCallback> ValuesChanged { get; set; } [Parameter] public IEnumerable DataSource { get => _datasource; set { if (value == null && _datasource == null) return; if (value == null && _datasource != null) { SelectOptionItems.Clear(); Value = default; _datasource = null; OnDataSourceChanged?.Invoke(); return; } if (value != null && !value.Any() && SelectOptionItems.Any()) { SelectOptionItems.Clear(); Value = default; _datasource = value; OnDataSourceChanged?.Invoke(); return; } if (value != null) { bool hasChanged; if (_datasource == null) { hasChanged = true; } else { hasChanged = !value.SequenceEqual(_datasource); } if (hasChanged) { OnDataSourceChanged?.Invoke(); _datasource = value; } } } } [Parameter] public override TItemValue Value { get => _selectedValue; set { var hasChanged = !EqualityComparer.Default.Equals(value, _selectedValue); if (hasChanged) { _selectedValue = value; OnValueChange(value); } } } [Parameter] public TItemValue DefaultValue { get => _defaultValue; set { var hasChanged = !EqualityComparer.Default.Equals(value, _defaultValue); if (hasChanged) { _defaultValueIsNotNull = !EqualityComparer.Default.Equals(value, default); _defaultValue = value; } } } [Parameter] public IEnumerable Values { get => _selectedValues; set { if (value != null && _selectedValues != null) { var hasChanged = !value.SequenceEqual(_selectedValues); if (!hasChanged) return; _selectedValues = value; _ = OnValuesChangeAsync(value); } else if (value != null && _selectedValues == null) { _selectedValues = value; _ = OnValuesChangeAsync(value); } else if (value == null && _selectedValues != null) { _selectedValues = default; _ = OnValuesChangeAsync(default); } } } [Parameter] public IEnumerable DefaultValues { get => _defaultValues; set { if (value != null && _defaultValues != null) { var hasChanged = !value.SequenceEqual(_defaultValues); if (!hasChanged) return; _defaultValuesHasItems = value.Any(); _defaultValues = value; } else if (value != null && _defaultValues == null) { _defaultValuesHasItems = value.Any(); _defaultValues = value; } else if (value == null && _defaultValues != null) { _defaultValuesHasItems = false; _defaultValues = default; } } } [Parameter] public RenderFragment SelectOptions { get; set; } #endregion Parameters #region Properties private const string ClassPrefix = "ant-select"; private const string DefaultWidth = "width: 100%;"; /// /// Determines if SelectOptions has any selected items /// /// true if SelectOptions has any selected Items, otherwise false internal bool HasValue { get => SelectOptionItems.Where(x => x.IsSelected).Any(); } /// /// Returns a true/false if the placeholder should be displayed or not. /// /// true if SelectOptions has no values and the searchValue is empty; otherwise false protected bool ShowPlaceholder { get { return !HasValue && string.IsNullOrEmpty(_searchValue); } } /// /// Returns the value of EnableSearch parameter /// /// true if search is enabled internal bool IsSearchEnabled { get => EnableSearch; } /// /// Indicates if the GroupName is used. When this value is True, the SelectOptions will be rendered in group mode. /// internal bool IsGroupingEnabled { get => !string.IsNullOrWhiteSpace(GroupName); } internal SelectMode SelectMode => Mode.ToSelectMode(); internal bool Focused { get; private set; } private string _searchValue = string.Empty; private string _dropdownStyle = string.Empty; private TItemValue _selectedValue; private TItemValue _defaultValue; private bool _defaultValueIsNotNull; private IEnumerable _datasource; private IEnumerable _selectedValues; private IEnumerable _defaultValues; private bool _defaultValuesHasItems; private bool _isInitialized; private bool _waittingStateChange; internal ElementReference _inputRef; protected OverlayTrigger _dropDown; internal HashSet> SelectOptionItems { get; } = new HashSet>(); #endregion Properties protected override void OnInitialized() { SetClassMap(); if (string.IsNullOrWhiteSpace(Style)) Style = DefaultWidth; _isInitialized = true; base.OnInitialized(); } protected override async Task OnParametersSetAsync() { if (SelectOptions == null) CreateDeleteSelectOptions(); await base.OnParametersSetAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await SetInitialValuesAsync(); await SetDropdownStyleAsync(); } // if (_isInitialized && SelectOptions == null) CreateDeleteSelectOptions(); if (SelectMode == SelectMode.Default) { // Try to set the default value each render cycle if _selectedValue has no value if (_defaultValueIsNotNull && !HasValue && SelectOptionItems.Any() || DefaultActiveFirstItem && !HasValue && SelectOptionItems.Any()) { await TrySetDefaultValueAsync(); } } else { // Try to set the default value each render cycle if _selectedValue has no value if (_defaultValuesHasItems && !HasValue && SelectOptionItems.Any() || DefaultActiveFirstItem && !HasValue && SelectOptionItems.Any()) { await TrySetDefaultValuesAsync(); } } if (_waittingStateChange) { _waittingStateChange = false; StateHasChanged(); } await base.OnAfterRenderAsync(firstRender); } /// /// Create or delete SelectOption when the datasource changed /// private void CreateDeleteSelectOptions() { if (string.IsNullOrWhiteSpace(ValueName)) throw new ArgumentNullException(nameof(ValueName)); if (string.IsNullOrWhiteSpace(LabelName)) throw new ArgumentNullException(nameof(LabelName)); if (_datasource == null) return; // Compare items of SelectOptions and the datastore if (SelectOptionItems.Any()) { // Delete items from SelectOptions if it is no longer in the datastore for (var i = SelectOptionItems.Count - 1; i >= 0; i--) { var selectOption = SelectOptionItems.ElementAt(i); var exists = _datasource.Contains(selectOption.Item); if (!exists) { SelectOptionItems.Remove(selectOption); } } } foreach (var item in _datasource) { TItemValue value = GetPropertyValueAsTItemValue(item, ValueName); var exists = false; SelectOptionItem updateSelectOption = null; foreach (var selectOption in SelectOptionItems) { var result = EqualityComparer.Default.Equals(selectOption.Value, value); if (result) { exists = true; updateSelectOption = selectOption; continue; } } if (!exists) { var disabled = false; var groupName = string.Empty; var label = GetPropertyValueAsObject(item, LabelName)?.ToString(); if (!string.IsNullOrWhiteSpace(DisabledName)) disabled = (bool)GetPropertyValueAsObject(item, DisabledName); if (!string.IsNullOrWhiteSpace(GroupName)) groupName = GetPropertyValueAsObject(item, GroupName)?.ToString(); var newItem = new SelectOptionItem { Label = label, GroupName = groupName, IsDisabled = disabled, Item = item, Value = value }; SelectOptionItems.Add(newItem); } else if (exists && !IgnoreItemChanges) { updateSelectOption.Label = GetPropertyValueAsObject(item, LabelName)?.ToString(); if (!string.IsNullOrWhiteSpace(DisabledName)) updateSelectOption.IsDisabled = (bool)GetPropertyValueAsObject(item, DisabledName); if (!string.IsNullOrWhiteSpace(GroupName)) updateSelectOption.GroupName = GetPropertyValueAsObject(item, GroupName)?.ToString(); } } } /// /// Sorted list of SelectOptionItems /// protected internal IEnumerable> SortedSelectOptionItems { get { var selectOption = SelectOptionItems; if (SortByGroup == SortDirection.Ascending && SortByLabel == SortDirection.None) { return selectOption.OrderBy(g => g.GroupName); } else if (SortByGroup == SortDirection.Descending && SortByLabel == SortDirection.None) { return selectOption.OrderByDescending(g => g.GroupName); } else if (SortByGroup == SortDirection.None && SortByLabel == SortDirection.Ascending) { return selectOption.OrderBy(l => l.Label); } else if (SortByGroup == SortDirection.None && SortByLabel == SortDirection.Descending) { return selectOption.OrderByDescending(l => l.Label); } else if (SortByGroup == SortDirection.Ascending && SortByLabel == SortDirection.Ascending) { return selectOption.OrderBy(g => g.GroupName).ThenBy(l => l.Label); } else if (SortByGroup == SortDirection.Ascending && SortByLabel == SortDirection.Descending) { return selectOption.OrderBy(g => g.GroupName).OrderByDescending(l => l.Label); } else if (SortByGroup == SortDirection.Descending && SortByLabel == SortDirection.Ascending) { return selectOption.OrderByDescending(g => g.GroupName).ThenBy(l => l.Label); } else if (SortByGroup == SortDirection.Descending && SortByLabel == SortDirection.Descending) { return selectOption.OrderByDescending(g => g.GroupName).OrderByDescending(l => l.Label); } else { return selectOption; } } } /// /// Sets the CSS classes to change the visual style /// protected void SetClassMap() { ClassMapper.Clear() .Add($"{ClassPrefix}") .If($"{ClassPrefix}-open", () => _dropDown?.IsOverlayShow() ?? false) .If($"{ClassPrefix}-focused", () => Focused) .If($"{ClassPrefix}-single", () => SelectMode == SelectMode.Default) .If($"{ClassPrefix}-multiple", () => SelectMode != SelectMode.Default) .If($"{ClassPrefix}-sm", () => Size == AntSizeLDSType.Small) .If($"{ClassPrefix}-lg", () => Size == AntSizeLDSType.Large) .If($"{ClassPrefix}-borderless", () => !Bordered) .If($"{ClassPrefix}-show-arrow", () => ShowArrowIcon) .If($"{ClassPrefix}-show-search", () => EnableSearch) .If($"{ClassPrefix}-bordered", () => Bordered) .If($"{ClassPrefix}-loading", () => Loading) .If($"{ClassPrefix}-disabled", () => Disabled); } /// /// Returns True if the parameter IsHidden is set to true for all entries in the SelectOptions list /// /// true if all items are set to IsHidden(true) protected bool AllOptionsHidden() { return SelectOptionItems.All(x => x.IsHidden); } /// /// Gets the BoundingClientRect of Ref (JSInvoke) and set the min-width and width in px. /// protected async Task SetDropdownStyleAsync() { var domRect = await JsInvokeAsync(JSInteropConstants.GetBoundingClientRect, Ref); _dropdownStyle = $"min-width: {domRect.width}px; width: {domRect.width}px;"; } protected async Task OnOverlayVisibleChangeAsync(bool visible) { OnDropdownVisibleChange?.Invoke(visible); if (visible) { await SetDropdownStyleAsync(); await SetInputFocusAsync(); await ScrollToFirstSelectedItemAsync(); } else { OnOverlayHide(); } } protected void OnOverlayHide() { if (!IsSearchEnabled) return; if (!AutoClearSearchValue) return; if (string.IsNullOrWhiteSpace(_searchValue)) return; _searchValue = string.Empty; if (SelectMode != SelectMode.Default && HideSelected) { SelectOptionItems.Where(x => !x.IsSelected && x.IsHidden) .ForEach(i => i.IsHidden = false); } else { SelectOptionItems.Where(x => x.IsHidden) .ForEach(i => i.IsHidden = false); } } /// /// Scrolls to the item via JavaScript. /// /// /// private async Task ElementScrollIntoViewAsync(ElementReference element) { await JsInvokeAsync(JSInteropConstants.ElementScrollIntoView, element); } /// /// Close the overlay /// /// internal async Task CloseAsync() { await _dropDown.Hide(true); } /// /// Called by the Form reset method /// internal override void ResetValue() { _ = ClearSelectedAsync(); } /// /// The method is called every time if the user select/de-select a item by mouse or keyboard. /// Don't change the IsSelected property outside of this function. /// protected internal async Task SetValueAsync(SelectOptionItem selectOption) { if (selectOption == null) throw new ArgumentNullException(nameof(selectOption)); if (SelectMode == SelectMode.Default) { SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => i.IsSelected = false); selectOption.IsSelected = true; await ValueChanged.InvokeAsync(selectOption.Value); } else { selectOption.IsSelected = !selectOption.IsSelected; if (selectOption.IsSelected) { if (HideSelected && !selectOption.IsHidden) selectOption.IsHidden = true; if (IsSearchEnabled) { if (!string.IsNullOrWhiteSpace(_searchValue)) { ClearSearch(); await SetInputFocusAsync(); } } } else { if (selectOption.IsHidden) selectOption.IsHidden = false; } await InvokeValuesChanged(); await UpdateOverlayPositionAsync(); } } /// /// Clears the selectValue(s) property and send the null(default) value back through the two-way binding. /// protected async Task ClearSelectedAsync() { if (SelectMode == SelectMode.Default) { OnSelectedItemChanged?.Invoke(default); await ValueChanged.InvokeAsync(default); } else { OnSelectedItemsChanged?.Invoke(default); await ValuesChanged.InvokeAsync(default); } } /// /// If DefaultActiveFirstItem is True, the first item which is not IsDisabled(True) is set as selected. /// If there is no item it falls back to the clear method. /// private async Task SetDefaultActiveFirstItemAsync() { if (SelectOptionItems.Any()) { var firstEnabled = SortedSelectOptionItems.FirstOrDefault(x => !x.IsDisabled); if (firstEnabled != null) { firstEnabled.IsSelected = true; if (HideSelected) firstEnabled.IsHidden = true; if (SelectMode == SelectMode.Default) { await ValueChanged.InvokeAsync(firstEnabled.Value); if (!ValueChanged.HasDelegate) await InvokeStateHasChangedAsync(); } else { await InvokeValuesChanged(); if (!ValuesChanged.HasDelegate) await InvokeStateHasChangedAsync(); } } else { await ClearSelectedAsync(); } } else { await ClearSelectedAsync(); } } /// /// Method invoked by OnAfterRenderAsync if the Value is null(default) and /// DefaultValue has a value or DefaultActiveFirstItem is True. /// private async Task TrySetDefaultValueAsync() { if (_defaultValueIsNotNull) { var result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, _defaultValue)); if (result != null && !result.IsDisabled) { result.IsSelected = true; if (HideSelected) result.IsHidden = true; _waittingStateChange = true; await ValueChanged.InvokeAsync(result.Value); } else { await SetDefaultActiveFirstItemAsync(); } } else if (DefaultActiveFirstItem) { await SetDefaultActiveFirstItemAsync(); } else { await ClearSelectedAsync(); } } /// /// Method invoked by OnAfterRenderAsync if the Value is null(default) and /// DefaultValues has a values or DefaultActiveFirstItem is True. /// private async Task TrySetDefaultValuesAsync() { if (_defaultValuesHasItems) { foreach (var defaultValue in _defaultValues) { var result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, defaultValue)); if (result != null && !result.IsDisabled) { result.IsSelected = true; if (HideSelected) result.IsHidden = true; } } var anySelected = SelectOptionItems.Any(x => x.IsSelected); if (!anySelected) { if (DefaultActiveFirstItem) { await SetDefaultActiveFirstItemAsync(); } else { await ClearSelectedAsync(); } } else { _waittingStateChange = true; await InvokeValuesChanged(); } } else if (DefaultActiveFirstItem) { await SetDefaultActiveFirstItemAsync(); } else { await ClearSelectedAsync(); } } /// /// Sets the initial values after initialization, the method should only called once. /// private async Task SetInitialValuesAsync() { if (SelectMode == SelectMode.Default) { if (_selectedValue != null) { var result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, _selectedValue)); if (result != null) { if (result.IsDisabled) { await TrySetDefaultValueAsync(); return; } result.IsSelected = true; if (HideSelected) result.IsHidden = true; OnSelectedItemChanged?.Invoke(result.Item); await ValueChanged.InvokeAsync(result.Value); } } } else { if (_selectedValues != null) { foreach (var value in _selectedValues) { var result = SelectOptionItems.FirstOrDefault(c => EqualityComparer.Default.Equals(c.Value, value)); if (result != null && !result.IsDisabled) { result.IsSelected = true; if (HideSelected) result.IsHidden = true; } } var newSelectedValues = new List(); var newSelectedItems = new List(); SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => { newSelectedValues.Add(i.Value); newSelectedItems.Add(i.Item); }); OnSelectedItemsChanged?.Invoke(newSelectedItems); await ValuesChanged.InvokeAsync(newSelectedValues); } } } /// /// Append a label item in tag mode /// /// private void AppendLabelValue(string label) { if (string.IsNullOrWhiteSpace(label)) return; SelectOptionItems.Add(new SelectOptionItem() { Label = label, IsActive = true, IsSelected = true }); } /// /// A separate method to invoke ValuesChanged and OnSelectedItemsChanged to reduce code duplicates. /// protected void InvokeOnSelectedItemChanged(SelectOptionItem selectOptionItem = null) { if (selectOptionItem == null) { OnSelectedItemsChanged?.Invoke(default); } else { if (LabelInValue && SelectOptions != null) { // Embed the label into the value and return the result as json string. var valueLabel = new Select.Internal.ValueLabel { Value = selectOptionItem.Value, Label = selectOptionItem.Label }; var json = JsonSerializer.Serialize(valueLabel); OnSelectedItemChanged?.Invoke((TItem)Convert.ChangeType(json, typeof(TItem))); } else { OnSelectedItemChanged?.Invoke(selectOptionItem.Item); } } } protected async Task InvokeValuesChanged() { var newSelectedValues = new List(); SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => { newSelectedValues.Add(i.Value); }); await ValuesChanged.InvokeAsync(newSelectedValues); } /// /// Inform the Overlay to update the position. /// internal async Task UpdateOverlayPositionAsync() { await _dropDown.GetOverlayComponent().UpdatePosition(); } private static object GetPropertyValueAsObject(object obj, string propertyName) { return obj.GetType().GetProperties() .Single(p => p.Name == propertyName) .GetValue(obj, null); } private static TItemValue GetPropertyValueAsTItemValue(object obj, string propertyName) { var result = obj.GetType().GetProperties() .Single(p => p.Name == propertyName) .GetValue(obj, null); return (TItemValue)TypeDescriptor.GetConverter(typeof(TItemValue)).ConvertFromInvariantString(result.ToString()); } #region Events /// /// The Method is called every time if the value of the @bind-Value was changed by the two-way binding. /// protected override void OnValueChange(TItemValue value) { if (!_isInitialized) // This is important because otherwise the initial value is overwritten by the EventCallback of ValueChanged and would be NULL. return; SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => i.IsSelected = false); if (EqualityComparer.Default.Equals(value, default)) { OnSelectedItemChanged?.Invoke(default); ValueChanged.InvokeAsync(default); return; } var result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, value)); if (result == null) { _ = TrySetDefaultValueAsync(); return; } if (result.IsDisabled) { _ = TrySetDefaultValueAsync(); return; } result.IsSelected = true; if (HideSelected) result.IsHidden = true; InvokeOnSelectedItemChanged(result); ValueChanged.InvokeAsync(result.Value); } /// /// The Method is called every time if the value of the @bind-Values was changed by the two-way binding. /// protected async Task OnValuesChangeAsync(IEnumerable values) { if (!_isInitialized) // This is important because otherwise the initial value is overwritten by the EventCallback of ValueChanged and would be NULL. return; if (!SelectOptionItems.Any()) return; SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => i.IsSelected = false); if (values == null) { await ValuesChanged.InvokeAsync(default); OnSelectedItemsChanged?.Invoke(default); return; } var valueList = values.ToList(); foreach (var item in valueList) { var result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, item)); if (result != null && !result.IsDisabled) { result.IsSelected = true; } } if (_dropDown.IsOverlayShow()) { await UpdateOverlayPositionAsync(); } var newSelectedValues = new List(); var newSelectedItems = new List(); SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => { newSelectedValues.Add(i.Value); newSelectedItems.Add(i.Item); }); OnSelectedItemsChanged?.Invoke(newSelectedItems); await ValuesChanged.InvokeAsync(newSelectedValues); } /// /// Method is called via EventCallBack if the value of the Input element was changed by keyboard /// /// Contains the value of the Input element protected async void OnInputAsync(ChangeEventArgs e) { if (e == null) throw new ArgumentNullException(nameof(e)); if (!IsSearchEnabled) { return; } if (!_dropDown.IsOverlayShow()) { await _dropDown.Show(); } _searchValue = e.Value?.ToString(); //_inputWidth = string.IsNullOrEmpty(_searchValue) ? InputDefaultWidth : $"{4 + _searchValue.Length * 8}px"; if (SelectMode == SelectMode.Default) { SelectOptionItems.Where(x => x.IsHidden).ForEach(i => i.IsHidden = false); if (!string.IsNullOrWhiteSpace(_searchValue)) { SelectOptionItems .Where(x => !x.Label.Contains(_searchValue, StringComparison.InvariantCultureIgnoreCase)) .ForEach(i => { i.IsHidden = true; i.IsActive = false; }); } OnSearch?.Invoke(_searchValue); } else if (TokenSeparators?.Any() == true) { // Automatic tokenization _searchValue?.Split(TokenSeparators).ForEach(AppendLabelValue); ClearSearch(); } } /// /// Method is called via EventCallBack if the keyboard key is no longer pressed inside the Input element. /// /// Contains the key (combination) which was pressed inside the Input element protected async Task OnKeyUpAsync(KeyboardEventArgs e) { if (e == null) throw new ArgumentNullException(nameof(e)); var key = e.Key.ToUpperInvariant(); var overlayFirstOpen = false; if (key == "ENTER") { if (!_dropDown.IsOverlayShow()) return; if (!SelectOptionItems.Any()) return; if (SelectMode == SelectMode.Default) { var firstActive = SelectOptionItems.FirstOrDefault(x => x.IsActive); if (firstActive != null) { if (!firstActive.IsDisabled) { await SetValueAsync(firstActive); await CloseAsync(); } } return; } if (SelectMode == SelectMode.Multiple) { if (AllOptionsHidden()) return; var firstActive = SelectOptionItems.FirstOrDefault(x => x.IsActive); if (firstActive != null) { if (!firstActive.IsDisabled) { await SetValueAsync(firstActive); } } return; } if (SelectMode == SelectMode.Tags) { if (AllowCustomTags) { var anyActiveItems = SelectOptionItems.Any(x => x.IsActive); if (AllOptionsHidden() || !anyActiveItems) { OnCreateCustomTag?.Invoke(_searchValue); ClearSearch(); return; } } else { AppendLabelValue(_searchValue); ClearSearch(); return; } //var firstActive = SelectOptionItems.FirstOrDefault(x => x.IsActive); //if (firstActive != null) //{ // if (!firstActive.IsDisabled) // { // await SetValueAsync(firstActive); // } //} return; } } if (key == "ARROWUP") { if (!_dropDown.IsOverlayShow() && !Disabled) { await _dropDown.Show(); overlayFirstOpen = true; } if (!SelectOptionItems.Any()) return; var sortedSelectOptionItems = SortedSelectOptionItems.ToList(); if (overlayFirstOpen) { // Check if there is a selected item and set it as active var currentSelected = sortedSelectOptionItems.FirstOrDefault(x => x.IsSelected); if (currentSelected != null) { if (currentSelected.IsActive) return; sortedSelectOptionItems.Where(x => x.IsActive) .ForEach(i => i.IsActive = false); currentSelected.IsActive = true; // ToDo: Sometime the element does not scroll, you have to call the function twice await ElementScrollIntoViewAsync(currentSelected.Ref); await Task.Delay(1); await ElementScrollIntoViewAsync(currentSelected.Ref); } return; } var firstActive = sortedSelectOptionItems.FirstOrDefault(x => x.IsActive && !x.IsHidden && !x.IsDisabled); if (firstActive == null) { var firstOption = sortedSelectOptionItems.FirstOrDefault(x => !x.IsHidden && !x.IsDisabled); if (firstOption != null) { firstOption.IsActive = true; await ElementScrollIntoViewAsync(firstOption.Ref); } } else { var possibilityCount = sortedSelectOptionItems.Where(x => !x.IsHidden && !x.IsDisabled).Count(); if (possibilityCount == 1) // Do nothing if there is only one choice return; var index = sortedSelectOptionItems.FindIndex(x => EqualityComparer.Default.Equals(x.Value, firstActive.Value)); index--; int nextIndex; if (index == -1) { nextIndex = sortedSelectOptionItems.FindLastIndex(x => !x.IsHidden && !x.IsDisabled); } else { nextIndex = sortedSelectOptionItems.FindIndex(index, x => !x.IsHidden && !x.IsDisabled); if (nextIndex != index) { for (var i = index; i >= -1; i--) { if (i < 0) { nextIndex = sortedSelectOptionItems.FindLastIndex(x => !x.IsHidden && !x.IsDisabled); break; } if (!sortedSelectOptionItems[i].IsHidden && !sortedSelectOptionItems[i].IsDisabled) { nextIndex = i; break; } } } } if (nextIndex == -1) return; // Prevent duplicate active items if search has no value sortedSelectOptionItems.Where(x => x.IsActive) .ForEach(x => x.IsActive = false); sortedSelectOptionItems[nextIndex].IsActive = true; await ElementScrollIntoViewAsync(sortedSelectOptionItems[nextIndex].Ref); } } if (key == "ARROWDOWN") { if (!_dropDown.IsOverlayShow() && !Disabled) { await _dropDown.Show(); overlayFirstOpen = true; } if (!SelectOptionItems.Any()) return; var sortedSelectOptionItems = SortedSelectOptionItems.ToList(); if (overlayFirstOpen) { // Check if there is a selected item and set it as active var currentSelected = sortedSelectOptionItems.FirstOrDefault(x => x.IsSelected); if (currentSelected != null) { if (currentSelected.IsActive) return; sortedSelectOptionItems.Where(x => x.IsActive) .ForEach(i => i.IsActive = false); currentSelected.IsActive = true; // ToDo: Sometime the element does not scroll, you have to call the function twice await ElementScrollIntoViewAsync(currentSelected.Ref); await Task.Delay(1); await ElementScrollIntoViewAsync(currentSelected.Ref); } return; } var firstActive = sortedSelectOptionItems.FirstOrDefault(x => x.IsActive && !x.IsHidden && !x.IsDisabled); if (firstActive == null) { var firstOption = sortedSelectOptionItems.FirstOrDefault(x => !x.IsHidden && !x.IsDisabled); if (firstOption != null) firstOption.IsActive = true; } else { var possibilityCount = sortedSelectOptionItems.Count(x => !x.IsHidden && !x.IsDisabled); if (possibilityCount == 1) // Do nothing if there is only one choice return; var index = sortedSelectOptionItems.FindIndex(x => EqualityComparer.Default.Equals(x.Value, firstActive.Value)); index++; var nextIndex = sortedSelectOptionItems.FindIndex(index, x => !x.IsHidden && !x.IsDisabled); if (nextIndex == -1) // Maybe the next item is above the current active item { nextIndex = sortedSelectOptionItems.FindIndex(0, x => !x.IsHidden && !x.IsDisabled); // Try to find the index from the first available item } if (nextIndex == -1) return; // Prevent duplicate active items if search has no value sortedSelectOptionItems.Where(x => x.IsActive) .ForEach(x => x.IsActive = false); sortedSelectOptionItems[nextIndex].IsActive = true; await ElementScrollIntoViewAsync(sortedSelectOptionItems[nextIndex].Ref); } } if (key == "HOME") { if (_dropDown.IsOverlayShow()) { if (!SelectOptionItems.Any()) return; var sortedSelectOptionItems = SortedSelectOptionItems.ToList(); var index = sortedSelectOptionItems.FindIndex(0, x => !x.IsHidden && !x.IsDisabled); if (index == -1) return; // Prevent duplicate active items if search has no value sortedSelectOptionItems.Where(x => x.IsActive) .ForEach(i => i.IsActive = false); sortedSelectOptionItems[index].IsActive = true; await ElementScrollIntoViewAsync(sortedSelectOptionItems[index].Ref); } } if (key == "END") { if (_dropDown.IsOverlayShow()) { if (!SelectOptionItems.Any()) return; var sortedSelectOptionItems = SortedSelectOptionItems.ToList(); var index = sortedSelectOptionItems.FindLastIndex(x => !x.IsHidden && !x.IsDisabled); if (index == -1) return; // Prevent duplicate active items if search has no value sortedSelectOptionItems.Where(x => x.IsActive) .ForEach(i => i.IsActive = false); sortedSelectOptionItems[index].IsActive = true; await ElementScrollIntoViewAsync(sortedSelectOptionItems[index].Ref); } } if (key == "ESCAPE") { if (_dropDown.IsOverlayShow()) { await CloseAsync(); } } } /// /// Method is called via EventCallBack if the Input element get the focus /// protected async Task OnInputFocusAsync(FocusEventArgs _) { await SetInputFocusAsync(); } /// /// Method is called via EventCallback if a key is pressed inside Input element. /// The method is used to get the TAB event if the user press TAB to cycle trough elements. /// If a TAB is received, the overlay will be closed and the Input element blures. /// protected async Task OnKeyDownAsync(KeyboardEventArgs e) { if (e == null) throw new ArgumentNullException(nameof(e)); var key = e.Key.ToUpperInvariant(); if (key == "TAB") { if (_dropDown.IsOverlayShow()) { await CloseAsync(); } await SetInputBlurAsync(); } } /// /// Check if Focused property is False; Set the Focused property to true, change the /// style and set the Focus on the Input element via DOM. It also invoke the OnFocus Action. /// protected async Task SetInputFocusAsync() { if (!Focused) { Focused = true; SetClassMap(); await JsInvokeAsync(JSInteropConstants.Focus, _inputRef); OnFocus?.Invoke(); } } /// /// Method is called via EventCallBack if the Input element loses the focus /// protected async Task OnInputBlurAsync(FocusEventArgs _) { await SetInputBlurAsync(); } /// /// Check if Focused property is true; Set the Focused property to false, change the /// style and blures the Input element via DOM. It also invoke the OnBlur Action. /// /// protected async Task SetInputBlurAsync() { if (Focused) { Focused = false; SetClassMap(); await JsInvokeAsync(JSInteropConstants.Blur, _inputRef); OnBlur?.Invoke(); } } protected void ClearSearch() { if (SelectMode != SelectMode.Default) { if (HideSelected) { SelectOptionItems.Where(x => x.IsHidden && !x.IsSelected) .ForEach(i => i.IsHidden = false); } else { SelectOptionItems.Where(x => x.IsHidden) .ForEach(i => i.IsHidden = false); } SelectOptionItems.Where(x => x.IsActive) .ForEach(i => i.IsActive = false); } _searchValue = string.Empty; } /// /// Search the first selected item, set IsActive to False for all other items and call the scrollIntoView function via JavaScript. /// The method is used to scroll to the first selected item after opening the overlay. /// protected async Task ScrollToFirstSelectedItemAsync() { // Check if there is a selected item and set it as active var currentSelected = SelectOptionItems.FirstOrDefault(x => x.IsSelected); if (currentSelected != null) { SelectOptionItems.Where(x => x.IsActive) .ForEach(i => i.IsActive = false); currentSelected.IsActive = true; // ToDo: Sometime the element does not scroll, you have to call the function twice await ElementScrollIntoViewAsync(currentSelected.Ref); await Task.Delay(1); await ElementScrollIntoViewAsync(currentSelected.Ref); } } /// /// Method is called via EventCallBack after the user clicked on the Clear icon inside the Input element. /// Set the IsSelected and IsHidden properties for all items to False. It updates the overlay position if /// the SelectMode is Tags or Multiple. Invoke the OnClearSelected Action. Set the Value(s) to default. /// protected async Task OnInputClearClickAsync(MouseEventArgs _) { SelectOptionItems.Where(c => c.IsSelected) .ForEach(i => { i.IsSelected = false; i.IsHidden = false; }); await ClearSelectedAsync(); if (SelectMode != SelectMode.Default) { await Task.Delay(1); // Todo - Workaround because UI does not refresh await UpdateOverlayPositionAsync(); StateHasChanged(); // Todo - Workaround because UI does not refresh } OnClearSelected?.Invoke(); } /// /// Method is called via EventCallBack if the user clicked on the Close icon of a Tag. /// protected async Task OnRemoveSelectedAsync(SelectOptionItem selectOption) { if (selectOption == null) throw new ArgumentNullException(nameof(selectOption)); await SetValueAsync(selectOption); } #endregion Events } }