using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text.Json; using System.Threading.Tasks; using AntDesign.Core.Helpers.MemberPath; using AntDesign.Internal; using AntDesign.JsInterop; using AntDesign.Select; using AntDesign.Select.Internal; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using OneOf; #pragma warning disable CA1716 // Disable Select name warning #pragma warning disable CA1305 // IFormatProvider warning namespace AntDesign { public partial class Select : AntInputComponentBase { #region Parameters /// /// Show clear button. /// [Parameter] public bool AllowClear { get; set; } /// /// Whether the current search will be cleared on selecting an item. /// [Parameter] public bool AutoClearSearchValue { get; set; } = true; /// /// Toggle the border style. /// [Parameter] public bool Bordered { get; set; } = true; /// /// Converts custom tag (a string) to TItemValue type. /// [Parameter] public Func CustomTagLabelToValue { get; set; } = (label) => (TItemValue)TypeDescriptor.GetConverter(typeof(TItemValue)).ConvertFromInvariantString(label); bool _dataSourceHasChanged = false; IEnumerable _dataSourceShallowCopy; private bool? _isTItemPrimitive; private bool IsTItemPrimitive { get { if (_isTItemPrimitive is null) { _isTItemPrimitive = IsSimpleType(typeof(TItem)); } return _isTItemPrimitive!.Value; } } /// /// MethodInfo will contain attached MemberwiseClone protected /// method. Due to its protection level, it has to be accessed /// using reflection. It will be used during generation of /// the DataSource shallow copy (which is a new list of DataSource /// items with shallow copy of each item). /// private MethodInfo _dataSourceItemShallowCopyMehtod; private MethodInfo GetDataSourceItemCloneMethod() { if (_dataSourceItemShallowCopyMehtod is null) { _dataSourceItemShallowCopyMehtod = this.GetType().GetGenericArguments()[1] .GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic); if (DataSourceEqualityComparer is null) { DataSourceEqualityComparer = new DataSourceEqualityComparer(this); } } return _dataSourceItemShallowCopyMehtod; } /// /// The datasource for this component. /// [Parameter] public IEnumerable DataSource { get => _datasource; set { if (value == null && _datasource == null) { return; } if (value == null && _datasource != null) { if (!_isInitialized) { _selectedValue = default; } else { SelectOptionItems.Clear(); SelectedOptionItems.Clear(); Value = default; _datasource = null; _dataSourceShallowCopy = null; OnDataSourceChanged?.Invoke(); } return; } if (value != null && !value.Any() && SelectOptionItems.Any()) { SelectOptionItems.Clear(); SelectedOptionItems.Clear(); Value = default; _datasource = value; _dataSourceShallowCopy = value.ToList(); OnDataSourceChanged?.Invoke(); return; } if (value != null) { if (_datasource == null) { _dataSourceHasChanged = true; } else { _dataSourceHasChanged = !value.SequenceEqual(_dataSourceShallowCopy, DataSourceEqualityComparer); } if (_dataSourceHasChanged) { OnDataSourceChanged?.Invoke(); _datasource = value; if (IsTItemPrimitive) { _dataSourceShallowCopy = _datasource.ToList(); } else { var cloneMethod = GetDataSourceItemCloneMethod(); _dataSourceShallowCopy = _datasource.Select(x => (TItem)cloneMethod.Invoke(x, null)).ToList(); } } } } } /// /// EqualityComparer that will be used during DataSource change /// detection. If no comparer set, default .Net is going to be /// used. /// [Parameter] public IEqualityComparer DataSourceEqualityComparer { get; set; } /// /// Activates the first item that is not deactivated. /// [Parameter] public bool DefaultActiveFirstOption { get { return _defaultActiveFirstOption; } set { _defaultActiveFirstOption = value; if (!_defaultActiveFirstOption) { _defaultActiveFirstOptionApplied = true; } } } /// /// Used when Mode = default - The value is used during initialization and when pressing the Reset button within Forms. /// [Parameter] public TItemValue DefaultValue { get => _defaultValue; set { var hasChanged = !EqualityComparer.Default.Equals(value, _defaultValue); if (hasChanged) { _defaultValueIsNotNull = !EqualityComparer.Default.Equals(value, default); _defaultValue = value; } } } /// /// Used when Mode = multiple | tags - The values are used during initialization and when pressing the Reset button within Forms. /// [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; } } } /// /// Whether the Select component is disabled. /// [Parameter] public bool Disabled { get; set; } /// /// The name of the property to be used as a disabled indicator. /// [Parameter] public string DisabledName { get => _disabledName; set { _getDisabled = string.IsNullOrWhiteSpace(value) ? null : PathHelper.GetDelegate(value); _disabledName = value; } } /// /// Will match drowdown width: /// - for boolean: true - with widest item in the dropdown list /// - for string: with value (e.g.: "256px") /// [Parameter] public OneOf DropdownMatchSelectWidth { get; set; } = true; /// /// Will not allow dropdown width to grow above stated in here value (eg. "768px") /// [Parameter] public string DropdownMaxWidth { get; set; } = "auto"; /// /// Customize dropdown content. /// [Parameter] public Func DropdownRender { get; set; } /// /// Indicates whether the search function is active or not. Always true for mode tags. /// [Parameter] public bool EnableSearch { get; set; } /// /// The name of the property to be used as a group indicator. /// If the value is set, the entries are displayed in groups. /// Use additional SortByGroup and SortByLabel. /// [Parameter] public string GroupName { get => _groupName; set { _getGroup = string.IsNullOrWhiteSpace(value) ? null : PathHelper.GetDelegate(value); _groupName = value; } } /// /// Hides the selected items when they are selected. /// [Parameter] public bool HideSelected { get; set; } /// /// Is used to increase the speed. If you expect changes to the label name, /// group name or disabled indicator, disable this property. /// [Parameter] public bool IgnoreItemChanges { get; set; } = true; /// /// Is used to customize the item style. /// [Parameter] public RenderFragment ItemTemplate { get; set; } /// /// Whether to embed label in value, turn the format of value from TItemValue to string (JSON) /// e.g. { "value": TItemValue, "label": "Label value" } /// [Parameter] public bool LabelInValue { get; set; } /// /// The name of the property to be used for the label. /// [Parameter] public string LabelName { get => _labelName; set { _getLabel = string.IsNullOrWhiteSpace(value) ? null : PathHelper.GetDelegate(value); if (SelectMode == SelectMode.Tags) { _setLabel = string.IsNullOrWhiteSpace(value) ? null : PathHelper.SetDelegate(value); } _labelName = value; } } /// /// Is used to customize the label style. /// [Parameter] public RenderFragment LabelTemplate { get; set; } /// /// Show loading indicator. You have to write the loading logic on your own. /// [Parameter] public bool Loading { get; set; } private OneOf _maxTagCount; private int _maxTagCountAsInt; /// /// Max tag count to show. responsive will cost render performance. /// [Parameter] public OneOf MaxTagCount { get { return _maxTagCount; } set { _maxTagCount = value; value.Switch(intValue => { IsResponsive = false; HasTagCount = intValue > 0; _maxTagCountAsInt = intValue; }, enumValue => { IsResponsive = enumValue == ResponsiveTag.Responsive; HasTagCount = false; }); } } internal bool IsResponsive { get; set; } internal bool HasTagCount { get; set; } /// /// Placeholder for hidden tags. If used with ResponsiveTag.Responsive, implement your own handling logic. /// [Parameter] public RenderFragment> MaxTagPlaceholder { get; set; } /// /// How long (number of characters) a tag will be. /// Only for Mode = "multiple" or Mode = "tags" /// /// /// The maximum length of the tag text. /// [Parameter] public int MaxTagTextLength { get; set; } /// /// Set mode of Select - default | multiple | tags /// [Parameter] public string Mode { get; set; } = "default"; /// /// Specify content to show when no result matches. /// [Parameter] public RenderFragment NotFoundContent { get; set; } /// /// Called when blur. /// [Parameter] public Action OnBlur { get; set; } /// /// Called when the user clears the selection. /// [Parameter] public Action OnClearSelected { get; set; } /// /// Called when custom tag is created. /// [Parameter] public Action OnCreateCustomTag { get; set; } /// /// Called when the datasource changes. From null to IEnumerable, from IEnumerable to IEnumerable or from IEnumerable to null. /// /// /// It does not trigger if a value inside the IEnumerable<TItem> changes. /// [Parameter] public Action OnDataSourceChanged { get; set; } /// /// Called when the dropdown visibility changes. /// [Parameter] public Action OnDropdownVisibleChange { get; set; } /// /// Called when focus. /// [Parameter] public Action OnFocus { get; set; } /// /// Called when mouse enter. /// [Parameter] public Action OnMouseEnter { get; set; } /// /// Called when mouse leave. /// [Parameter] public Action OnMouseLeave { get; set; } /// /// Callback function that is fired when input changed. /// [Parameter] public Action OnSearch { get; set; } /// /// Called when the selected item changes. /// [Parameter] public Action OnSelectedItemChanged { get; set; } /// /// Called when the selected items changes. /// [Parameter] public Action> OnSelectedItemsChanged { get; set; } /// /// Controlled open state of dropdown. /// [Parameter] public bool Open { get; set; } /// /// Placeholder of select. /// [Parameter] public string Placeholder { get; set; } /// /// The maximum height of the popup container. /// [Parameter] public string PopupContainerMaxHeight { get; set; } = "256px"; /// /// Use this to fix overlay problems e.g. #area /// [Parameter] public string PopupContainerSelector { get; set; } = "body"; /// /// The custom prefix icon. /// [Parameter] public RenderFragment PrefixIcon { get; set; } /// /// Used for rendering select options manually. /// [Parameter] public RenderFragment SelectOptions { get; set; } private bool _showArrowIconChanged; /// /// Whether to show the drop-down arrow /// [Parameter] public bool ShowArrowIcon { get { return _showArrowIcon; } set { _showArrowIcon = value; _showArrowIconChanged = true; } } /// /// Whether show search input in single mode. /// [Parameter] public bool ShowSearchIcon { get; set; } = true; /// /// Sort items by group name. None | Ascending | Descending /// [Parameter] public SortDirection SortByGroup { get; set; } = SortDirection.None; /// /// Sort items by label value. None | Ascending | Descending /// [Parameter] public SortDirection SortByLabel { get; set; } = SortDirection.None; /// /// The custom suffix icon. /// [Parameter] public RenderFragment SuffixIcon { get; set; } /// /// Define what characters will be treated as token separators for newly created tags. /// Useful when creating new tags using only keyboard. /// [Parameter] public char[] TokenSeparators { get; set; } bool _valueHasChanged; /// /// Get or set the selected value. /// [Parameter] public override TItemValue Value { get => _selectedValue; set { _valueHasChanged = !EqualityComparer.Default.Equals(value, _selectedValue); if (_valueHasChanged) { _selectedValue = value; _valueHasChanged = _isInitialized; } } } /// /// Get or set the selected values. /// [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); } if (_isNotifyFieldChanged && (Form?.ValidateOnChange == true)) { EditContext?.NotifyFieldChanged(FieldIdentifier); } } } /// /// Used for the two-way binding. /// [Parameter] public override EventCallback ValueChanged { get; set; } /// /// The name of the property to be used for the value. /// [Parameter] public string ValueName { get => _valueName; set { _getValue = string.IsNullOrWhiteSpace(value) ? null : PathHelper.GetDelegate(value); _setValue = string.IsNullOrWhiteSpace(value) ? null : PathHelper.SetDelegate(value); _valueName = value; } } /// /// Used for the two-way binding. /// [Parameter] public EventCallback> ValuesChanged { get; set; } #endregion Parameters [Inject] private IDomEventListener DomEventListener { get; set; } #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 => SelectedOptionItems.Any() || (AddedTags?.Any() ?? false); } /// /// 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 || SelectMode == SelectMode.Tags; } /// /// 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 ElementReference DropDownRef => _dropDown.GetOverlayComponent().Ref; internal SelectMode SelectMode => Mode.ToSelectMode(); internal bool Focused { get; private set; } private string _searchValue = string.Empty; private string _prevSearchValue = 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 _afterFirstRender; private bool _optionsHasInitialized; private bool _defaultValueApplied; private bool _defaultActiveFirstOptionApplied; private bool _waittingStateChange; private bool _isValueEnum; internal ElementReference _inputRef; protected OverlayTrigger _dropDown; protected SelectContent _selectContent; private bool _isToken; private SelectOptionItem _activeOption; private bool _defaultActiveFirstOption; internal HashSet> SelectOptionItems { get; } = new HashSet>(); internal List> SelectedOptionItems { get; } = new List>(); internal List> AddedTags { get; } = new List>(); internal SelectOptionItem CustomTagSelectOptionItem { get; set; } /// /// Currently active (highlighted) option. /// It does not have to be equal to selected option. /// internal SelectOptionItem ActiveOption { get { return _activeOption; } set { if (_activeOption != value) { if (_activeOption != null && _activeOption.IsActive) _activeOption.IsActive = false; _activeOption = value; if (_activeOption != null && !_activeOption.IsActive) _activeOption.IsActive = true; } } } private string _labelName; internal Func _getLabel; private Action _setLabel; private string _groupName = string.Empty; private Func _getGroup; private string _disabledName; private Func _getDisabled; private string _valueName; internal Func _getValue; private Action _setValue; private bool _disableSubmitFormOnEnter; private bool _showArrowIcon = true; private Expression> _valueExpression; #endregion Properties private static bool IsSimpleType(Type type) { return type.IsPrimitive || new Type[] { typeof(string), typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid) }.Contains(type) || type.IsEnum || Convert.GetTypeCode(type) != TypeCode.Object || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0])) ; } internal bool IsDropdownShown() => _dropDown.IsOverlayShow(); protected override void OnInitialized() { if (SelectOptions == null && typeof(TItemValue) != typeof(TItem) && string.IsNullOrWhiteSpace(ValueName)) { throw new ArgumentNullException(nameof(ValueName)); } SetClassMap(); if (string.IsNullOrWhiteSpace(Style)) Style = DefaultWidth; if (!_isInitialized) { _isValueEnum = typeof(TItemValue).IsEnum; if (!_showArrowIconChanged && SelectMode != SelectMode.Default) _showArrowIcon = SuffixIcon != null; } _isInitialized = true; base.OnInitialized(); } protected override void OnParametersSet() { if (SelectOptions == null) { if (!_optionsHasInitialized || _dataSourceHasChanged) { CreateDeleteSelectOptions(); _optionsHasInitialized = true; _dataSourceHasChanged = false; } } if (_valueHasChanged && _optionsHasInitialized) { _valueHasChanged = false; OnValueChange(_selectedValue); if (Form?.ValidateOnChange == true) { EditContext?.NotifyFieldChanged(FieldIdentifier); } } base.OnParametersSet(); } /// /// Used only when ChildElement SelectOptions is used. /// Will run this process if after initalization an item /// is added that is also marked as selected. /// /// internal async Task ProcessSelectedSelectOptions() { if (_isInitialized && _afterFirstRender) { if (Mode == "default") { if (LastValueBeforeReset is not null) { OnValueChange(LastValueBeforeReset); LastValueBeforeReset = default; } else { OnValueChange(Value); } } else { await OnValuesChangeAsync(Values); } } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (SelectOptions != null) { _optionsHasInitialized = true; } if (firstRender) { await SetInitialValuesAsync(); DomEventListener.AddShared("window", "resize", OnWindowResize); await SetDropdownStyleAsync(); _defaultValueApplied = !(_defaultValueIsNotNull || _defaultValuesHasItems); _defaultActiveFirstOptionApplied = !_defaultActiveFirstOption; } if (!_defaultValueApplied || !_defaultActiveFirstOptionApplied) { if (SelectMode == SelectMode.Default) { if (_defaultValueIsNotNull && !HasValue && SelectOptionItems.Any() || DefaultActiveFirstOption && !HasValue && SelectOptionItems.Any()) { await TrySetDefaultValueAsync(); } } else { if (_defaultValuesHasItems && !HasValue && SelectOptionItems.Any() || DefaultActiveFirstOption && !HasValue && SelectOptionItems.Any()) { await TrySetDefaultValuesAsync(); } } } if (_isInitialized && SelectOptions == null) { CreateDeleteSelectOptions(); _optionsHasInitialized = true; } if (_waittingStateChange) { _waittingStateChange = false; StateHasChanged(); } await base.OnAfterRenderAsync(firstRender); _afterFirstRender = true; } protected override void Dispose(bool disposing) { DomEventListener.Dispose(); base.Dispose(disposing); } protected async void OnWindowResize(JsonElement element) { await SetDropdownStyleAsync(); } /// /// Create or delete SelectOption when the datasource changed /// private void CreateDeleteSelectOptions() { if (_datasource == null) return; Dictionary> dataStoreToSelectOptionItemsMatch = new(); // 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); if (!selectOption.IsAddedTag) { var exists = _datasource.Where(x => x.Equals(selectOption.Item)).FirstOrDefault(); if (exists is null) { SelectOptionItems.Remove(selectOption); if (selectOption.IsSelected) SelectedOptionItems.Remove(selectOption); } else dataStoreToSelectOptionItemsMatch.Add(exists, selectOption); } } } //A simple approach to avoid unnecessary scanning through _selectedValues once //all of SelectOptionItem where already marked as selected int processedSelectedCount = 0; if (SelectMode == SelectMode.Default && _selectedValue != null) processedSelectedCount = 1; else if (SelectMode != SelectMode.Default && _selectedValues != null) processedSelectedCount = _selectedValues.Count(); foreach (var item in _datasource) { TItemValue value = _getValue == null ? THelper.ChangeType(item) : _getValue(item); var exists = false; SelectOptionItem selectOption; SelectOptionItem updateSelectOption = null; if (dataStoreToSelectOptionItemsMatch.TryGetValue(item, out selectOption)) { var result = EqualityComparer.Default.Equals(selectOption.Value, value); if (result) { exists = true; updateSelectOption = selectOption; } } var disabled = false; var groupName = string.Empty; var label = _getLabel == null ? GetLabel(item) : _getLabel(item); bool isSelected = false; if (processedSelectedCount > 0) { if (SelectMode == SelectMode.Default) isSelected = ReferenceEquals(value, _selectedValue) || value?.Equals(_selectedValue) == true; else isSelected = _selectedValues.Contains(value); } if (!string.IsNullOrWhiteSpace(DisabledName)) disabled = _getDisabled(item); if (!string.IsNullOrWhiteSpace(GroupName)) groupName = _getGroup(item); if (!exists) { var newItem = new SelectOptionItem { Label = label, GroupName = groupName, IsDisabled = disabled, Item = item, Value = value, IsSelected = isSelected, IsHidden = isSelected && HideSelected }; SelectOptionItems.Add(newItem); if (isSelected) { processedSelectedCount--; SelectedOptionItems.Add(newItem); } } else if (exists && !IgnoreItemChanges) { updateSelectOption.Label = label; updateSelectOption.IsDisabled = disabled; updateSelectOption.GroupName = groupName; updateSelectOption.IsHidden = isSelected && HideSelected; if (isSelected) { if (!updateSelectOption.IsSelected) { updateSelectOption.IsSelected = isSelected; SelectedOptionItems.Add(updateSelectOption); } processedSelectedCount--; } } } } /// /// 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 if (SelectMode == SelectMode.Tags) { if (CustomTagSelectOptionItem != null) { return selectOption.OrderByDescending(g => g.Equals(CustomTagSelectOptionItem)); } return selectOption; } 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 || SelectMode == SelectMode.Tags) .If($"{ClassPrefix}-bordered", () => Bordered) .If($"{ClassPrefix}-loading", () => Loading) .If($"{ClassPrefix}-disabled", () => Disabled) .If($"{ClassPrefix}-rtl", () => RTL); } /// /// 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() { if (AddedTags.Count > 0) return SelectOptionItems.All(x => x.IsHidden) && AddedTags.All(x => x.IsHidden); else 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() { string maxWidth = "", minWidth = "", definedWidth = ""; var domRect = await JsInvokeAsync(JSInteropConstants.GetBoundingClientRect, Ref); var width = domRect.Width.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture); minWidth = $"min-width: {width}px;"; if (DropdownMatchSelectWidth.IsT0 && DropdownMatchSelectWidth.AsT0) { definedWidth = $"width: {width}px;"; } else if (DropdownMatchSelectWidth.IsT1) { definedWidth = $"width: {DropdownMatchSelectWidth.AsT1};"; } if (!DropdownMaxWidth.Equals("auto", StringComparison.CurrentCultureIgnoreCase)) maxWidth = $"max-width: {DropdownMaxWidth};"; _dropdownStyle = minWidth + definedWidth + maxWidth; } 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; _prevSearchValue = string.Empty; if (SelectMode != SelectMode.Default && HideSelected) { SelectOptionItems.Where(x => !x.IsSelected && x.IsHidden) .ForEach(i => i.IsHidden = false); } else { if (CustomTagSelectOptionItem is not null) { SelectOptionItems.Remove(CustomTagSelectOptionItem); CustomTagSelectOptionItem = null; } 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.ScrollTo, 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) { if (SelectedOptionItems.Count > 0) { SelectedOptionItems[0].IsSelected = false; SelectedOptionItems[0] = selectOption; } else { SelectedOptionItems.Add(selectOption); } selectOption.IsSelected = true; await ValueChanged.InvokeAsync(selectOption.Value); InvokeOnSelectedItemChanged(selectOption); } else { selectOption.IsSelected = !selectOption.IsSelected; if (selectOption.IsSelected) { if (HideSelected && !selectOption.IsHidden) selectOption.IsHidden = true; if (IsSearchEnabled && !string.IsNullOrWhiteSpace(_searchValue)) ClearSearch(); if (selectOption.IsAddedTag) { CustomTagSelectOptionItem = null; AddedTags.Add(selectOption); SelectOptionItems.Add(selectOption); } } else { if (selectOption.IsHidden) selectOption.IsHidden = false; if (selectOption.IsAddedTag) { SelectOptionItems.Remove(selectOption); SelectedOptionItems.Remove(selectOption); if (selectOption.IsAddedTag && SelectOptions != null) { AddedTags.Remove(selectOption); } } if (IsResponsive) await _selectContent.RemovedItem(); } if (EnableSearch || SelectMode == SelectMode.Tags) await SetInputFocusAsync(); await InvokeValuesChanged(selectOption); 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 (SelectedOptionItems.Count == 0) { SelectedOptionItems.Add(firstEnabled); } else SelectedOptionItems[0] = firstEnabled; 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(); } _defaultActiveFirstOptionApplied = true; } /// /// 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; if (SelectedOptionItems.Count == 0) { SelectedOptionItems.Add(result); } else SelectedOptionItems[0] = result; await ValueChanged.InvokeAsync(result.Value); } else { await SetDefaultActiveFirstItemAsync(); } } else if (DefaultActiveFirstOption) { await SetDefaultActiveFirstItemAsync(); } else { await ClearSelectedAsync(); } _defaultValueApplied = true; } /// /// 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 (DefaultActiveFirstOption) { await SetDefaultActiveFirstItemAsync(); } else { await ClearSelectedAsync(); } } else { _waittingStateChange = true; await InvokeValuesChanged(); } } else if (DefaultActiveFirstOption) { await SetDefaultActiveFirstItemAsync(); } else { await ClearSelectedAsync(); } _defaultValueApplied = true; } /// /// Sets the initial values after initialization, the method should only called once. /// private async Task SetInitialValuesAsync() { SelectedOptionItems.Clear(); 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; ActiveOption = result; if (HideSelected) result.IsHidden = true; SelectedOptionItems.Add(result); 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); SelectedOptionItems.Add(i); }); OnSelectedItemsChanged?.Invoke(newSelectedItems); await ValuesChanged.InvokeAsync(newSelectedValues); } } } /// /// Append a label item in tag mode /// /// private SelectOptionItem AppendLabelValue(string label) { if (string.IsNullOrWhiteSpace(label)) return default; SelectOptionItem newItem = CreateSelectOptionItem(label, true); SelectOptionItems.Add(newItem); return newItem; } /// /// Creates the select option item. Mostly meant to create new tags, that is why IsAddedTag is hardcoded to true. /// /// Creation based on passed label /// if set to true [is active]. /// private SelectOptionItem CreateSelectOptionItem(string label, bool isActive) { TItemValue value = CustomTagLabelToValue.Invoke(label); TItem item; if (IsTItemPrimitive) { item = (TItem)TypeDescriptor.GetConverter(typeof(TItem)).ConvertFromInvariantString(_searchValue); } else { if (_setValue == null) { item = THelper.ChangeType(value); } else { item = Activator.CreateInstance(); _setValue(item, value); } _setLabel?.Invoke(item, _searchValue); } return new SelectOptionItem() { Label = label, Value = value, Item = item, IsActive = isActive, IsSelected = false, IsAddedTag = 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(SelectOptionItem newSelection = null) { List newSelectedValues; if (newSelection is null || Values is null) { newSelectedValues = new List(); SelectedOptionItems.Clear(); SelectOptionItems.Where(x => x.IsSelected) .ForEach(i => { newSelectedValues.Add(i.Value); SelectedOptionItems.Add(i); }); } else { newSelectedValues = Values.ToList(); if (newSelection.IsSelected) { newSelectedValues.Add(newSelection.Value); SelectedOptionItems.Add(newSelection); } else { newSelectedValues.Remove(newSelection.Value); SelectedOptionItems.Remove(newSelection); } } if (ValuesChanged.HasDelegate) await ValuesChanged.InvokeAsync(newSelectedValues); else { Values = newSelectedValues; StateHasChanged(); } } /// /// Inform the Overlay to update the position. /// internal async Task UpdateOverlayPositionAsync() { if (_dropDown.Visible) await _dropDown.GetOverlayComponent().UpdatePosition(); } protected virtual string GetLabel(TItem item) { return item.ToString(); } #region Events /// /// When newly set Value is not found in SelectOptionItems, it is reset to /// default. This property holds the value before reset. It may be needed /// to be reaplied (for example when new Value is set at the same time /// as new SelectOption is added, but Value in the component is set /// before new SelectOptionItem has been created). /// internal TItemValue LastValueBeforeReset { get; set; } /// /// 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 (!_optionsHasInitialized) // This is important because otherwise the initial value is overwritten by the EventCallback of ValueChanged and would be NULL. return; if (!_isValueEnum && EqualityComparer.Default.Equals(value, default)) { _ = InvokeAsync(() => OnInputClearClickAsync(new())); return; } var result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, value)); if (result == null) { if (SelectOptions is not null) { LastValueBeforeReset = value; } if (!AllowClear) _ = TrySetDefaultValueAsync(); else { //Reset value if not found - needed if value changed //outside of the component _ = InvokeAsync(() => OnInputClearClickAsync(new())); } return; } if (result.IsDisabled) { _ = TrySetDefaultValueAsync(); return; } result.IsSelected = true; EvaluateValueChangedOutsideComponent(result, value); if (HideSelected) result.IsHidden = true; ValueChanged.InvokeAsync(result.Value); } /// /// When bind-Value is changed outside of the component, then component /// selected items have to be reselected according to new value passed. /// /// The option item that has been selected. /// The value of the selected option item. private void EvaluateValueChangedOutsideComponent(SelectOptionItem optionItem, TItemValue value) { if (ActiveOption != null && !ActiveOption.Value.Equals(value)) { ActiveOption.IsSelected = false; ActiveOption = optionItem; } if (SelectedOptionItems.Count > 0) { if (!SelectedOptionItems[0].Value.Equals(value)) { SelectedOptionItems[0].IsSelected = false; SelectedOptionItems[0] = optionItem; } } else { SelectedOptionItems.Add(optionItem); } } /// /// 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; if (values == null) { await ValuesChanged.InvokeAsync(default); OnSelectedItemsChanged?.Invoke(default); return; } EvaluateValuesChangedOutsideComponent(values); if (_dropDown.IsOverlayShow()) { //A delay forces a refresh better than StateHasChanged(). //For example when a tag is added that is causing SelectContent to grow, //this Task.Delay will actually allow to reposition the Overlay to match //new size of SelectContent. await Task.Delay(1); await UpdateOverlayPositionAsync(); } OnSelectedItemsChanged?.Invoke(SelectedOptionItems.Select(s => s.Item)); await ValuesChanged.InvokeAsync(Values); } /// /// When bind-Values is changed outside of the component, then component /// selected items have to be reselected according to new values passed. /// TODO: (Perf) Consider using hash to identify if the passed values are different from currently selected. /// /// The values that need to be selected. private void EvaluateValuesChangedOutsideComponent(IEnumerable values) { var newSelectedItems = new List(); var deselectList = SelectedOptionItems.ToDictionary(item => item.Value, item => item); foreach (var value in values.ToList()) { SelectOptionItem result; if (SelectMode == SelectMode.Multiple) { result = SelectOptionItems.FirstOrDefault(x => !x.IsSelected && EqualityComparer.Default.Equals(x.Value, value)); if (result != null && !result.IsDisabled) { result.IsSelected = true; SelectedOptionItems.Add(result); } deselectList.Remove(value); } else { result = SelectOptionItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Value, value)); if (result is null) //tag delivered from outside, needs to be added to the list of options { result = CreateSelectOptionItem(value.ToString(), true); result.IsSelected = true; AddedTags.Add(result); SelectOptionItems.Add(result); SelectedOptionItems.Add(result); } else if (result != null && !result.IsSelected && !result.IsDisabled) { result.IsSelected = true; SelectedOptionItems.Add(result); } deselectList.Remove(value); } } if (deselectList.Count > 0) { foreach (var item in deselectList) { item.Value.IsSelected = false; SelectedOptionItems.Remove(item.Value); if (item.Value.IsAddedTag) { SelectOptionItems.Remove(item.Value); AddedTags.Remove(item.Value); } } } } /// /// 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(); } bool containsToken = false; _prevSearchValue = _searchValue; if (_isToken) _searchValue = e.Value?.ToString().TrimEnd(TokenSeparators); else { _searchValue = e.Value?.ToString(); if (TokenSeparators is not null && TokenSeparators.Length > 0) { containsToken = TokenSeparators.Any(t => _searchValue.Contains(t)); } } //_inputWidth = string.IsNullOrEmpty(_searchValue) ? InputDefaultWidth : $"{4 + _searchValue.Length * 8}px"; if (containsToken) { await TokenizeSearchedPhrase(_searchValue); } if (!string.IsNullOrWhiteSpace(_searchValue)) { FilterOptionItems(_searchValue); } else { SelectOptionItems.Where(x => x.IsHidden).ForEach(i => i.IsHidden = false); if (SelectMode == SelectMode.Tags && CustomTagSelectOptionItem is not null) { SelectOptionItems.Remove(CustomTagSelectOptionItem); CustomTagSelectOptionItem = null; } } OnSearch?.Invoke(_searchValue); } private async Task TokenizeSearchedPhrase(string searchValue) { Dictionary> tokenItemMatch = new(); tokenItemMatch = searchValue.Split(TokenSeparators).Distinct().ToDictionary( item => item.Trim(), _ => default(SelectOptionItem)); if (SelectMode == SelectMode.Tags) { List> selectOptionItems; if (AddedTags.Count > 0) { selectOptionItems = SelectOptionItems.ToList(); selectOptionItems.AddRange(AddedTags); } else selectOptionItems = SelectOptionItems.ToList(); foreach (var item in selectOptionItems) { if (tokenItemMatch.ContainsKey(item.Label)) { await SetValueAsync(item); tokenItemMatch[item.Label] = item; } } foreach (KeyValuePair> tokenItem in tokenItemMatch) { if (tokenItem.Value == null) { tokenItemMatch[tokenItem.Key] = CreateSelectOptionItem(tokenItem.Key, false); SelectOptionItems.Add(tokenItemMatch[tokenItem.Key]); await SetValueAsync(tokenItemMatch[tokenItem.Key]); } } } else { foreach (var item in SelectOptionItems) { if (tokenItemMatch.ContainsKey(item.Label)) { await SetValueAsync(item); } } } if (_dropDown.IsOverlayShow()) { await CloseAsync(); } await SetInputBlurAsync(); } private void FilterOptionItems(string searchValue) { if (SelectMode != SelectMode.Tags) { bool firstDone = false; foreach (var item in SelectOptionItems) { if (item.Label.Contains(searchValue, StringComparison.InvariantCultureIgnoreCase)) { if (!firstDone) { item.IsActive = true; firstDone = true; } else if (item.IsActive) { item.IsActive = false; } if (item.IsHidden) item.IsHidden = false; } else { if (!item.IsHidden) item.IsHidden = true; item.IsActive = false; } } } else { FilterTagsOptionItems(searchValue); } } private void FilterTagsOptionItems(string searchValue) { SelectOptionItem activeCanditate = null; List> selectOptionItems; if (AddedTags.Count > 0) { selectOptionItems = SelectOptionItems.ToList(); selectOptionItems.AddRange(AddedTags); } else selectOptionItems = SelectOptionItems.ToList(); foreach (var item in selectOptionItems) { if (!(CustomTagSelectOptionItem != null && item.Equals(CustomTagSelectOptionItem))) //ignore if analyzing CustomTagSelectOptionItem { if (item.Label.Contains(searchValue, StringComparison.InvariantCultureIgnoreCase)) { if (item.Label.Equals(searchValue, StringComparison.InvariantCulture)) { activeCanditate = item; ActiveOption = item; item.IsActive = true; if (CustomTagSelectOptionItem != null) { SelectOptionItems.Remove(CustomTagSelectOptionItem); CustomTagSelectOptionItem = null; } } else if (item.IsActive) item.IsActive = false; if (item.IsHidden) item.IsHidden = false; } else { if (!item.IsHidden) item.IsHidden = true; item.IsActive = false; } } } if (activeCanditate is null) { //label has to be cast-able to value TItemValue value = CustomTagLabelToValue.Invoke(searchValue); if (CustomTagSelectOptionItem is null) { CustomTagSelectOptionItem = CreateSelectOptionItem(searchValue, true); SelectOptionItems.Add(CustomTagSelectOptionItem); ActiveOption = CustomTagSelectOptionItem; } else { CustomTagSelectOptionItem.Label = searchValue; CustomTagSelectOptionItem.Value = value; if (IsTItemPrimitive) { CustomTagSelectOptionItem.Item = (TItem)TypeDescriptor.GetConverter(typeof(TItem)).ConvertFromInvariantString(_searchValue); } else { _setLabel?.Invoke(CustomTagSelectOptionItem.Item, _searchValue); _setValue?.Invoke(CustomTagSelectOptionItem.Item, value); } } } } /// /// 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 (_isToken && SelectMode == SelectMode.Tags) { if (!_dropDown.IsOverlayShow()) return; if (!SelectOptionItems.Any()) return; SelectOptionItem firstActive; if (ActiveOption.IsAddedTag) { firstActive = SelectOptionItems.FirstOrDefault(x => x.Value.Equals(ActiveOption.Value)); if (firstActive is null) firstActive = ActiveOption; } else firstActive = ActiveOption; // SelectOptionItems.FirstOrDefault(x => x.IsActive); if (AllOptionsHidden() || firstActive is null) { var newItem = AppendLabelValue(_searchValue); await SetValueAsync(newItem); OnCreateCustomTag?.Invoke(_searchValue); } else if (firstActive != null && !firstActive.IsDisabled) { CustomTagSelectOptionItem = null; await SetValueAsync(firstActive); } ClearSearch(); return; } 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 && !firstActive.IsDisabled) { await SetValueAsync(firstActive); ClearSearch(); } return; } if (SelectMode == SelectMode.Tags) { SelectOptionItem firstActive; if (ActiveOption.IsAddedTag) { firstActive = SelectOptionItems.FirstOrDefault(x => x.Value.Equals(ActiveOption.Value)); if (firstActive is null) firstActive = ActiveOption; } else firstActive = ActiveOption; if (AllOptionsHidden() || firstActive is null) { var newItem = AppendLabelValue(_searchValue); await SetValueAsync(newItem); OnCreateCustomTag?.Invoke(_searchValue); } else if (firstActive != null && !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; ActiveOption = currentSelected; // 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; ActiveOption = firstOption; 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; ActiveOption = sortedSelectOptionItems[nextIndex]; 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; ActiveOption = currentSelected; // 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; ActiveOption = firstOption; } } 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; ActiveOption = sortedSelectOptionItems[nextIndex]; 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; ActiveOption = sortedSelectOptionItems[index]; 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; ActiveOption = sortedSelectOptionItems[index]; await ElementScrollIntoViewAsync(sortedSelectOptionItems[index].Ref); } } if (key == "ESCAPE") { if (_dropDown.IsOverlayShow()) { await CloseAsync(); } } if (key == "BACKSPACE" && string.IsNullOrEmpty(_searchValue) && (EnableSearch || SelectMode == SelectMode.Tags || AllowClear)) { if (string.IsNullOrEmpty(_prevSearchValue) && SelectedOptionItems.Count > 0) await OnRemoveSelectedAsync(SelectedOptionItems.Last()); else if (!string.IsNullOrEmpty(_prevSearchValue)) _prevSearchValue = _searchValue; } } /// /// 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(); } else if (TokenSeparators is not null && TokenSeparators.Length > 0) { _isToken = TokenSeparators.Contains(e.Key[0]); } } /// /// 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 FocusAsync(_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) { foreach (var item in SelectOptionItems) { if (item.IsHidden) { if ((HideSelected && !item.IsSelected) || !HideSelected) item.IsHidden = false; } } foreach (var item in AddedTags) { if (item.IsHidden) { if ((HideSelected && !item.IsSelected) || !HideSelected) item.IsHidden = false; } } } _searchValue = string.Empty; _prevSearchValue = 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; ActiveOption = currentSelected; // 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); } else if (ActiveOption == null)//position on first element in the list { var selectionCandidate = SelectOptionItems.FirstOrDefault(); if (selectionCandidate != null) ActiveOption = selectionCandidate; } } /// /// 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 _) { List> tagItems = new(); SelectOptionItems.Where(c => c.IsSelected) .ForEach(i => { i.IsSelected = false; i.IsHidden = false; if (i.IsAddedTag) tagItems.Add(i); }); //When clearing, also remove all added tags that are kept after adding in SelectOptionItems if (tagItems.Count > 0) { foreach (var item in tagItems) { SelectOptionItems.Remove(item); } } AddedTags.Clear(); ActiveOption = SelectOptionItems.FirstOrDefault(); CustomTagSelectOptionItem = null; SelectedOptionItems.Clear(); 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); } internal async Task OnArrowClick(MouseEventArgs args) { await _dropDown.OnClickDiv(args); } #endregion Events } }