ant-design-blazor/components/select/Select.razor.cs
liuhai 5fd4aa9a7d fix(module: select): two-way binding #305 (#320)
* feat: add auto-complete component

* Select 组件
1. 组件支持双向绑定
2. 优化效率
3. 修复多选模式点击时下拉面板折叠问题

Co-authored-by: ElderJames <shunjiey@hotmail.com>
2020-07-29 12:44:16 +08:00

929 lines
30 KiB
C#

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using AntDesign.JsInterop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using OneOf;
#pragma warning disable 1591
#pragma warning disable CA1716
#pragma warning disable CA1062
namespace AntDesign
{
public partial class Select : AntInputComponentBase<string>
{
#region Private
#region Constants
private const string ClassPrefix = "ant-select";
private const string InputDefaultWidth = "4px";
#endregion
#region Fields
private bool _isSelectOpened = false;
private bool _isOverSelectOption = false;
private string _inputWidth = InputDefaultWidth;
private string _searchValue;
private string _dropdownStyle;
private string[] _tokenSeparators;
private SelectOption _searchOption;
private ElementReference _inputRef;
private ElementReference _dropdownRef;
private readonly ClassMapper _dropdownClassMapper = new ClassMapper();
private readonly List<SelectOption> _tagSelectOptions = new List<SelectOption>();
private readonly SortedSet<string> _tokenSelectOptions = new SortedSet<string>();
private OneOf<string, IEnumerable<string>, LabeledValue, IEnumerable<LabeledValue>>? _value;
#endregion
#region Properties
[Inject]
private DomEventService DomEventService { get; set; }
private bool IsTagMode => SelectMode == SelectMode.Tags;
private bool IsDefaultMode => SelectMode == SelectMode.Default;
private bool IsComplexMode => SelectMode != SelectMode.Default;
#endregion
#region Methods
private static bool DefaultFilterOption(string value, SelectOption option)
{
var optionContent = option.Children.ToUpperInvariant();
return optionContent.Contains(value, StringComparison.OrdinalIgnoreCase);
}
#endregion
#endregion
#region Protected
#region Properties
public string Title
{
get
{
if (SelectMode == SelectMode.Default && SelectedOptions.Any())
{
return SelectedOptions.First().Title;
}
return null;
}
}
protected bool HasValue
{
get
{
if (SelectedValues.HasValue)
{
if (SelectMode != SelectMode.Default)
{
if (LabelInValue)
{
return SelectedValues.Value.AsT3.Any();
}
else
{
return SelectedValues.Value.AsT1.Any();
}
}
else
{
return true;
}
}
return false;
}
}
protected bool ShowPlaceholder
{
get
{
return !HasValue && string.IsNullOrEmpty(_searchValue);
}
}
protected List<SelectOption> SelectOptions { get; } = new List<SelectOption>();
protected List<SelectOption> SelectedOptions { get; } = new List<SelectOption>();
protected Properties GetProperties(SelectOption option)
{
return new Properties
{
Closable = true,
Value = option.Value,
Label = option.Label,
OnClose = Action
};
void Action(MouseEventArgs e) => OnRemoveSelected(option);
}
protected bool IsShowSearch()
{
if (SelectMode == SelectMode.Default)
{
return ShowSearch;
}
return true;
}
#endregion
#region Methods
protected void SetClassMap()
{
ClassMapper.Clear()
.Add($"{ClassPrefix}")
.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", () => ShowArrow)
.If($"{ClassPrefix}-show-search", () => IsShowSearch())
.If($"{ClassPrefix}-bordered", () => Bordered)
.If($"{ClassPrefix}-loading", () => Loading)
.If($"{ClassPrefix}-disabled", () => Disabled);
_dropdownClassMapper.Clear()
.Add($"{ClassPrefix}-dropdown-hidden")
.Add($"{ClassPrefix}-placement-bottomLeft");
}
protected void SetHideClass()
{
SetClassMap();
_isSelectOpened = false;
}
protected bool IsEmptyOnSearch()
{
if (IsTagMode || (IsDefaultMode && !ShowSearch))
{
return false;
}
if (!string.IsNullOrEmpty(_searchValue))
{
foreach (var option in SelectOptions)
{
if (IsShowOption(option))
{
return false;
}
}
return true;
}
return false;
}
protected bool IsCreatedTagOption()
{
if (SelectMode == SelectMode.Tags &&
!string.IsNullOrEmpty(_searchValue) &&
!SelectOptions.Any(option => option.Value == _searchValue))
{
return true;
}
return false;
}
protected async Task SetDropdownStyle()
{
var scrollPoint = await JsInvokeAsync<Point>(JSInteropConstants.getScroll);
var domRect = await JsInvokeAsync<DomRect>(JSInteropConstants.getBoundingClientRect, Ref);
var left = Math.Round(domRect.left + scrollPoint.x);
var top = Math.Round(domRect.top + scrollPoint.y + domRect.height + 4);
_dropdownStyle = $"min-width: {domRect.width}px; width: {domRect.width}px; left: {left}px; top: {top}px;";
}
protected IEnumerable<string> GetTagOptions()
{
if (SelectMode == SelectMode.Tags && SelectedValues.HasValue)
{
var optionValues = SelectOptions.Select(option => option.Value).ToHashSet();
if (LabelInValue)
{
var values = SelectedValues.Value.AsT3.ToList();
return values.Where(v => !optionValues.Contains(v.Key)).Select(v => v.Key);
}
else
{
var values = SelectedValues.Value.AsT1.ToList();
return values.Where(v => !optionValues.Contains(v));
}
}
return Array.Empty<string>();
}
protected string GetAriaSelected(SelectOption selectOption)
{
return SelectedOptions.Contains(selectOption) ? "true" : "false";
}
protected OneOf<string, RenderFragment> GetShowValue(SelectOption option)
{
if (option.IsSearch || option.IsTag)
{
return option.Value;
}
return OptionLabelProp switch
{
"children" => option?.ChildContent,
"value" => option?.Value ?? option.Label,
"label" => option?.Label ?? option.Value,
_ => SelectMode == SelectMode.Default ? OneOf<string, RenderFragment>.FromT1((option?.ChildContent)) : option?.Label ?? option.Value,
};
}
protected async Task ResetState()
{
await Task.Delay(1);
Value = null;
SelectedValues = null;
SelectedOptions.Clear();
OnChange?.Invoke(default, default(SelectOption));
//await InvokeAsync(StateHasChanged);
}
#endregion
#region Events
protected override void OnInitialized()
{
SetClassMap();
#region Init Values
if (DefaultValue.HasValue)
{// Set default value
switch (SelectMode)
{
case SelectMode.Default:
if (LabelInValue)
{
if (DefaultValue.Value.IsT2)
{
SelectedValues = DefaultValue.Value;
// Value 赋值
CurrentValueAsString = DefaultValue.Value.AsT2.Key;
}
}
else
{
if (DefaultValue.Value.IsT0)
{
SelectedValues = DefaultValue.Value;
// Value 赋值
CurrentValueAsString = DefaultValue.Value.AsT0;
}
}
break;
default:
if (LabelInValue)
{
if (DefaultValue.Value.IsT3)
{
SelectedValues = DefaultValue.Value;
// Value 赋值
CurrentValueAsString = string.Join(",", DefaultValue.Value.AsT3.Select(s => s.Key));
}
}
else
{
if (DefaultValue.Value.IsT1)
{
SelectedValues = DefaultValue.Value;
// Value 赋值
CurrentValueAsString = string.Join(",", DefaultValue.Value.AsT1);
}
}
break;
}
}
#endregion
base.OnInitialized();
}
protected override async Task OnFirstAfterRenderAsync()
{
await base.OnFirstAfterRenderAsync();
await SetDropdownStyle();
await InvokeAsync(StateHasChanged);
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (IsTagMode && _tokenSelectOptions.Any())
{
_searchValue = string.Empty;
_tokenSelectOptions.Clear();
StateHasChanged();
}
}
protected async Task OnSelectOpenClick(MouseEventArgs _)
{
if (!_isSelectOpened && !Disabled)
{
_isSelectOpened = true;
ClassMapper
.Add($"{ClassPrefix}-open")
.If($"{ClassPrefix}-focused", () => IsShowSearch());
_dropdownClassMapper.Clear()
.Add($"{ClassPrefix}-placement-bottomLeft");
OnFocus?.Invoke();
await SetDropdownStyle();
//await InvokeAsync(StateHasChanged);
await JsInvokeAsync(JSInteropConstants.addElementToBody, _dropdownRef);
if (IsShowSearch())
{
await JsInvokeAsync(JSInteropConstants.focus, _inputRef);
}
OnDropdownVisibleChange?.Invoke(true);
}
else
{
OnSelectHideClick();
}
}
public async Task OnInputBlur()
{
await Task.Delay(1);
if (_isOverSelectOption) return;
SetHideClass();
SetClassMap();
}
protected async void OnSelectHideClick()
{
if (IsShowSearch())
{
if (IsTagMode && !string.IsNullOrEmpty(_searchValue) && !OptionIsSelected(_searchValue))
{
await ToggleOrSetValue(_searchValue, false);
}
if (AutoClearSearchValue)
{
_searchValue = string.Empty;
}
}
SetHideClass();
OnBlur?.Invoke();
//await InvokeAsync(StateHasChanged);
}
private async Task OnSelectMouseEnter()
{
await Task.Delay(1);
_isOverSelectOption = true;
}
private async Task OnSelectMouseLeave()
{
await Task.Delay(1);
_isOverSelectOption = false;
}
protected async Task OnClearClick(MouseEventArgs _)
{
await ResetState();
}
protected async void OnInput(ChangeEventArgs e)
{
if (IsShowSearch())
{
_searchValue = e?.Value.ToString();
if (string.IsNullOrEmpty(_searchValue))
{
_inputWidth = InputDefaultWidth;
}
else
{
_inputWidth = $"{4 + _searchValue.Length * 8}px";
}
if (!string.IsNullOrEmpty(_searchValue))
{
if (_tokenSeparators != null && _tokenSeparators.Any())
{
var tokens = _searchValue.Split(_tokenSeparators, StringSplitOptions.RemoveEmptyEntries);
if (tokens[0] != _searchValue)
{
_searchOption = null;
foreach (var token in tokens)
{
if (!OptionIsSelected(token))
{
await ToggleOrSetValue(token, false);
}
}
}
}
}
OnSearch?.Invoke(_searchValue);
//await InvokeAsync(StateHasChanged);
}
}
protected async void OnRemoveSelected(SelectOption option)
{
await Task.Delay(100);
var value = option?.Value;
if (option.IsTag)
{
_tagSelectOptions.Remove(option);
}
SelectedOptions.Remove(option);
if (LabelInValue)
{
var values = SelectedValues.Value.AsT3 as List<LabeledValue>;
values.RemoveAll(item => item.Key == value);
// Value 赋值
CurrentValueAsString = string.Join(",", values.Select(s => s.Key));
}
else
{
if (!(SelectedValues.Value.AsT1 is List<string> values))
{
values = new List<string>(SelectedValues.Value.AsT1);
SelectedValues = values;
}
values.Remove(value);
// Value 赋值
CurrentValueAsString = string.Join(",", values);
}
OnChange?.Invoke(SelectedValues.Value, SelectedOptions);
//await InvokeAsync(StateHasChanged);
}
#endregion
#endregion
#region Public
#region Properties
#region Parameters(51)
#region Boolean(15)
[Parameter] public bool AutoFocus { get; set; } = false;
[Parameter] public bool AllowClear { get; set; } = false;
[Parameter] public bool AutoClearSearchValue { get; set; } = true;
[Parameter] public bool DefaultActiveFirstOption { get; set; } = true;
[Parameter] public bool Disabled { get; set; } = false;
[Parameter] public bool LabelInValue { get; set; } = false;
[Parameter] public bool ShowArrow { get; set; } = true;
[Parameter] public bool ShowSearch { get; set; } = false;
[Parameter] public bool Virtual { get; set; } = true;
[Parameter] public bool Loading { get; set; } = false;
[Parameter] public bool Bordered { get; set; } = true;
[Parameter] public bool Open { get; set; } = false;
[Parameter] public bool DefaultOpen { get; set; } = false;
[Parameter] public bool HideSelected { get; set; } = false;
#endregion
#region String(7)
[Parameter] public string Mode { get; set; }
[Parameter] public string Placeholder { get; set; }
[Parameter] public string DropdownStyle { get; set; }
[Parameter] public string OptionLabelProp { get; set; }
[Parameter] public string DropdownClassName { get; set; }
[Parameter] public string OptionFilterProp { get; set; } = "value";
#endregion
#region Number(3)
[Parameter] public int? MaxTagCount { get; set; }
[Parameter] public int? MaxTagTextLength { get; set; }
[Parameter] public int ListHeight { get; set; } = 256;
#endregion
#region Array(2)
[Parameter] public IEnumerable<LabeledValue> Options { get; set; }
[Parameter]
public IEnumerable<string> TokenSeparators
{
get => _tokenSeparators;
set
{
_tokenSeparators = value.ToArray();
}
}
#endregion
#region Complex(5)
[Parameter] public OneOf<bool, int>? DropdownMatchSelectWidth { get; set; }
[Parameter]
public OneOf<string, IEnumerable<string>, LabeledValue, IEnumerable<LabeledValue>>? SelectedValues
{
get => _value;
set
{
if (value.HasValue)
{
if (SelectMode == SelectMode.Default)
{
if (LabelInValue)
{
if (value.Value.AsT2 != null)
{
_value = value;
}
}
else if (value.Value.AsT0 != null)
{
_value = value;
}
}
else
{
if (LabelInValue)
{
if (value.Value.AsT3 != null)
{
_value = value;
}
}
else if (value.Value.AsT1 != null)
{
_value = value;
}
}
}
}
}
[Parameter] public OneOf<string, IEnumerable<string>, LabeledValue, IEnumerable<LabeledValue>>? DefaultValue { get; set; }
[Parameter] public OneOf<bool, Func<string, SelectOption, bool>> FilterOption { get; set; } = true;
[Parameter] public OneOf<RenderFragment, Func<string[], RenderFragment>>? MaxTagPlaceholder { get; set; }
#endregion
#region RenderFragment(6)
[Parameter] public RenderFragment ClearIcon { get; set; }
[Parameter] public RenderFragment RemoveIcon { get; set; }
[Parameter] public RenderFragment SuffixIcon { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment NotFoundContent { get; set; }
[Parameter] public RenderFragment MenuItemSelectedIcon { get; set; }
#endregion
#region Function(14)
[Parameter] public Action OnBlur { get; set; }
[Parameter] public Action OnFocus { get; set; }
[Parameter] public Action OnMouseEnter { get; set; }
[Parameter] public Action OnMouseLeave { get; set; }
[Parameter] public Action OnPopupScroll { get; set; }
[Parameter] public Action OnInputKeyDown { get; set; }
[Parameter] public Action<string> OnSearch { get; set; }
[Parameter] public Action<bool> OnDropdownVisibleChange { get; set; }
[Parameter] public Action<OneOf<string, LabeledValue>> OnDeselect { get; set; }
[Parameter] public Action<OneOf<string, LabeledValue>, SelectOption> OnSelect { get; set; }
[Parameter] public Func<Properties, RenderFragment> TagRender { get; set; }
[Parameter] public Func<ElementReference, ElementReference> GetPopupContainer { get; set; }
[Parameter] public Func<RenderFragment, Properties, RenderFragment> DropdownRender { get; set; }
[Parameter] public Action<OneOf<string, IEnumerable<string>, LabeledValue, IEnumerable<LabeledValue>>, OneOf<SelectOption, IEnumerable<SelectOption>>> OnChange { get; set; }
#endregion
#endregion
#region Generals
public SelectMode SelectMode => Mode.ToSelectMode();
#endregion
#endregion
#region Methods
public void Blur()
{
OnSelectHideClick();
}
public async void Focus()
{
await OnSelectOpenClick(null);
}
public void RemoveOption(SelectOption selectOption)
{
SelectOptions.Remove(selectOption);
}
public void AddOption(SelectOption selectOption)
{
if (selectOption.IsSearch)
{
_searchOption = selectOption;
return;
}
if (selectOption.IsTag)
{
if (!_tagSelectOptions.Contains(selectOption))
{
SelectedOptions.Add(selectOption);
_tagSelectOptions.Add(selectOption);
}
return;
}
SelectOptions.Add(selectOption);
if (SelectedValues.HasValue)
{
switch (SelectMode)
{
case SelectMode.Default:
if (LabelInValue)
{
var value = SelectedValues.Value.AsT2;
if (value?.Key == selectOption?.Value)
{
SelectedOptions.Add(selectOption);
//StateHasChanged();
}
}
else
{
var value = SelectedValues.Value.AsT0;
if (value == selectOption?.Value)
{
SelectedOptions.Add(selectOption);
//StateHasChanged();
}
}
break;
default:
if (LabelInValue)
{
var values = SelectedValues.Value.AsT3;
if (values != null && values.Any(value => value.Key == selectOption?.Value))
{
SelectedOptions.Add(selectOption);
//StateHasChanged();
}
}
else
{
var values = SelectedValues.Value.AsT1;
if (values != null && values.Any(value => value == selectOption?.Value))
{
SelectedOptions.Add(selectOption);
//StateHasChanged();
}
}
break;
}
}
}
public bool OptionIsSelected(string value)
{
if (SelectedValues.HasValue)
{
switch (SelectMode)
{
case SelectMode.Default:
if (LabelInValue)
{
return SelectedValues.Value.AsT2.Key == value;
}
else
{
return SelectedValues.Value.AsT0 == value;
}
default:
if (LabelInValue)
{
return SelectedValues.Value.AsT3.Any(item => item.Key == value);
}
else
{
return SelectedValues.Value.AsT1.Any(item => item == value);
}
}
}
return false;
}
public async Task ToggleOrSetValue(string value, bool isRrender = true)
{
await Task.Delay(1);
var currentOption = SelectOptions.FirstOrDefault(option => option.Value == value);
if (IsDefaultMode)
{
if (!LabelInValue)
{
SelectedValues = value;
}
else
{
SelectedValues = new LabeledValue(value, currentOption.Label);
}
// Value 赋值
CurrentValueAsString = value;
SetHideClass();
SelectedOptions.Clear();
SelectedOptions.Add(currentOption);
}
else
{
currentOption ??= _tagSelectOptions.FirstOrDefault(option => option.Value == value);
if (LabelInValue)
{
if (SelectedValues.HasValue)
{
if (!(SelectedValues.Value.AsT3 is List<LabeledValue>))
{
SelectedValues = new List<LabeledValue>(SelectedValues.Value.AsT3);
}
}
else
{
SelectedValues = new List<LabeledValue>();
}
var values = SelectedValues.Value.AsT3 as List<LabeledValue>;
var existValue = values.FirstOrDefault(item => item.Key == value);
if (existValue != null)
{
values.Remove(existValue);
SelectedOptions.Remove(currentOption);
_tagSelectOptions.Remove(currentOption);
}
else
{
values.Add(new LabeledValue(value, currentOption?.Label ?? value));
if (currentOption != null)
{
SelectedOptions.Add(currentOption);
}
else if (_searchOption != null)
{
_searchOption.SearchToTag();
_tagSelectOptions.Add(_searchOption);
SelectedOptions.Add(_searchOption);
}
else
{
_tokenSelectOptions.Add(value);
}
}
// Value 赋值
CurrentValueAsString = string.Join(",", values.Select(s => s.Key));
}
else
{
if (!SelectedValues.HasValue)
{
SelectedValues = new List<string>();
}
else if (!(SelectedValues.Value.AsT1 is List<string>))
{
SelectedValues = new List<string>(SelectedValues.Value.AsT1);
}
var values = SelectedValues.Value.AsT1 as List<string>;
var existValue = values.FirstOrDefault(item => item == value);
if (existValue != null)
{
values.Remove(existValue);
SelectedOptions.Remove(currentOption);
_tagSelectOptions.Remove(currentOption);
}
else
{
values.Add(value);
if (currentOption != null)
{
SelectedOptions.Add(currentOption);
}
else if (_searchOption != null)
{
_searchOption.SearchToTag();
_tagSelectOptions.Add(_searchOption);
SelectedOptions.Add(_searchOption);
}
else
{
_tokenSelectOptions.Add(value);
}
}
// Value 赋值
CurrentValueAsString = string.Join(",", values);
}
// 多选模式换行时, 动态调整下拉框位置
await SetDropdownStyle();
}
OnChange?.Invoke(SelectedValues.Value, currentOption);
if (isRrender)
{
_searchValue = string.Empty;
}
await InvokeAsync(StateHasChanged);
}
public bool IsShowOption(SelectOption option)
{
if (HideSelected && SelectedOptions.Any(item => item.Value == option.Value))
{
return false;
}
if (IsShowSearch() && !string.IsNullOrEmpty(_searchValue))
{
if (FilterOption.IsT0 && FilterOption.AsT0)
{
return DefaultFilterOption(_searchValue, option);
}
else
{
return FilterOption.AsT1.Invoke(_searchValue, option);
}
}
return true;
}
public async Task ClearAll()
{
await ResetState();
}
#endregion
#endregion
}
}