ant-design-blazor/components/input/Input.cs

556 lines
18 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AntDesign.JsInterop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
namespace AntDesign
{
/// <summary>
///
/// </summary>
public class Input<TValue> : AntInputComponentBase<TValue>
{
protected const string PrefixCls = "ant-input";
2020-04-24 18:32:50 +08:00
private bool _allowClear;
protected string AffixWrapperClass { get; set; } = $"{PrefixCls}-affix-wrapper";
private bool _hasAffixWrapper;
2020-04-24 18:32:50 +08:00
protected string GroupWrapperClass { get; set; } = $"{PrefixCls}-group-wrapper";
protected virtual string InputType => "input";
2020-04-24 18:32:50 +08:00
//protected string ClearIconClass { get; set; }
protected static readonly EventCallbackFactory CallbackFactory = new EventCallbackFactory();
protected virtual bool IgnoreOnChangeAndBlur { get; }
protected virtual bool EnableOnPressEnter => OnPressEnter.HasDelegate;
[Inject]
public DomEventService DomEventService { get; set; }
2020-03-26 10:45:35 +08:00
[Parameter]
public string Type { get; set; } = "text";
2020-03-26 10:45:35 +08:00
[Parameter]
public RenderFragment AddOnBefore { get; set; }
[Parameter]
public RenderFragment AddOnAfter { get; set; }
[Parameter]
public string Placeholder { get; set; }
[Parameter]
public bool AutoFocus
{
get { return _autoFocus; }
set
{
_autoFocus = value;
if (!_isInitialized && _autoFocus)
IsFocused = _autoFocus;
}
}
[Parameter]
public TValue DefaultValue { get; set; }
[Parameter]
public int MaxLength { get; set; } = -1;
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public bool AllowClear { get; set; }
[Parameter]
public RenderFragment Prefix { get; set; }
[Parameter]
public RenderFragment Suffix { get; set; }
2020-03-26 10:45:35 +08:00
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<TValue> OnChange { get; set; }
[Parameter]
public EventCallback<KeyboardEventArgs> OnPressEnter { get; set; }
[Parameter]
public EventCallback<KeyboardEventArgs> OnkeyUp { get; set; }
[Parameter]
public EventCallback<KeyboardEventArgs> OnkeyDown { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnMouseUp { get; set; }
[Parameter]
public EventCallback<ChangeEventArgs> OnInput { get; set; }
[Parameter]
public EventCallback<FocusEventArgs> OnBlur { get; set; }
[Parameter]
public EventCallback<FocusEventArgs> OnFocus { get; set; }
[Parameter]
public int DebounceMilliseconds { get; set; } = 250;
[Parameter]
public string WrapperStyle { get; set; }
2020-04-24 18:32:50 +08:00
public Dictionary<string, object> Attributes { get; set; }
2020-03-26 10:45:35 +08:00
feat(module: overlay): OverlayTrigger not bound to a div (#937) * feat(module:overlay): OverlayTrigger not bound to a div * feat(module:overlay): OverlayTrigger not bound to a div * feat(module:overlay): Logic transfer to single Overlay * feat(module:overlay): remove obsolete duplication * feat(module:Tooltip): Add for unbounded oncontextmenu event handler * feat(module:tooltip): unbound js event listeners remove * docs(module:tooltip): unbound explanation * fix(module:button): attach Ref to top level html element @ref * feat(module:dropdown&tooltip&popconfirm&popover): Overlay not bound to a div * docs(module:dropdown&tooltip&popconfirm&popover): unbound explanation * feat(module:OverlayTrigger): common logic relocation * feat(module:overlaytrigger): Overlay not bound to a div * feat(module:DatePicker): Overlay not bound to a div * feat(module:select): Overlay not boud to div * fix(module:select): onclickarrow event relocation * fix(module:select): rename Show to OnArrowClick * feat(module:avatar): Overlay not bound to a div * docs(module:avatar): demo switch to unbound version * feat(module:autocomplete): partial OverlayTrigger not bound to a div * feat(module:slider): tooltip * docs(module:slider): tooltip * fix(module:overlay): add SetVisible method * feat: set Ref where missing, performance components register Ref when missing IsFixed flag for CascadeValue changed hard-code sequence numbers when using RenderTreeBuilder Rate component use Tooltip Unbound version Tabs test fix * fix: revert changes (accidental) * feat(module:upload): tooltip with unbound usage * feat(module:table): column use of unbound tooltip * feat(module:autocomplete):overlay unbound from div * fix(module:upload): missing div restore Co-authored-by: James Yeung <shunjiey@hotmail.com>
2021-01-21 17:20:10 +08:00
public ForwardRef WrapperRefBack { get; set; }
private TValue _inputValue;
private bool _compositionInputting;
private Timer _debounceTimer;
private bool _autoFocus;
private bool _isInitialized;
private bool DebounceEnabled => DebounceMilliseconds != 0;
protected bool IsFocused { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (!string.IsNullOrEmpty(DefaultValue?.ToString()) && string.IsNullOrEmpty(Value?.ToString()))
{
Value = DefaultValue;
}
SetClasses();
_isInitialized = true;
}
protected virtual void SetClasses()
{
AffixWrapperClass = $"{PrefixCls}-affix-wrapper {(IsFocused ? $"{PrefixCls}-affix-wrapper-focused" : "")}";
GroupWrapperClass = $"{PrefixCls}-group-wrapper";
if (!string.IsNullOrWhiteSpace(Class))
{
AffixWrapperClass = string.Join(" ", Class, AffixWrapperClass);
ClassMapper.OriginalClass = "";
}
ClassMapper.Clear()
.Add($"{PrefixCls}")
.If($"{PrefixCls}-lg", () => Size == InputSize.Large)
.If($"{PrefixCls}-sm", () => Size == InputSize.Small)
.If($"{PrefixCls}-rtl", () => RTL)
;
Attributes ??= new Dictionary<string, object>();
if (MaxLength >= 0 && !Attributes.ContainsKey("maxlength"))
{
Attributes?.Add("maxlength", MaxLength);
}
if (Disabled)
{
// TODO: disable element
2020-04-24 18:32:50 +08:00
AffixWrapperClass = string.Join(" ", AffixWrapperClass, $"{PrefixCls}-affix-wrapper-disabled");
ClassMapper.Add($"{PrefixCls}-disabled");
}
if (AllowClear)
{
_allowClear = true;
2020-04-24 18:32:50 +08:00
//ClearIconClass = $"{PrefixCls}-clear-icon";
ToggleClearBtn();
}
if (Size == InputSize.Large)
{
2020-04-24 18:32:50 +08:00
AffixWrapperClass = string.Join(" ", AffixWrapperClass, $"{PrefixCls}-affix-wrapper-lg");
GroupWrapperClass = string.Join(" ", GroupWrapperClass, $"{PrefixCls}-group-wrapper-lg");
}
else if (Size == InputSize.Small)
{
2020-04-24 18:32:50 +08:00
AffixWrapperClass = string.Join(" ", AffixWrapperClass, $"{PrefixCls}-affix-wrapper-sm");
GroupWrapperClass = string.Join(" ", GroupWrapperClass, $"{PrefixCls}-group-wrapper-sm");
}
}
protected override void OnParametersSet()
{
base.OnParametersSet();
SetClasses();
}
protected virtual async Task OnChangeAsync(ChangeEventArgs args)
{
if (CurrentValueAsString != args?.Value?.ToString())
{
if (OnChange.HasDelegate)
{
await OnChange.InvokeAsync(Value);
}
}
}
protected async Task OnKeyPressAsync(KeyboardEventArgs args)
{
if (args?.Key == "Enter" && InputType != "textarea")
{
await ChangeValue(true);
if (EnableOnPressEnter)
{
await OnPressEnter.InvokeAsync(args);
await OnPressEnterAsync();
}
}
}
protected virtual Task OnPressEnterAsync() => Task.CompletedTask;
protected async Task OnKeyUpAsync(KeyboardEventArgs args)
{
await ChangeValue();
if (OnkeyUp.HasDelegate) await OnkeyUp.InvokeAsync(args);
}
protected virtual async Task OnkeyDownAsync(KeyboardEventArgs args)
{
if (OnkeyDown.HasDelegate) await OnkeyDown.InvokeAsync(args);
}
protected async Task OnMouseUpAsync(MouseEventArgs args)
{
await ChangeValue(true);
if (OnMouseUp.HasDelegate) await OnMouseUp.InvokeAsync(args);
}
internal virtual async Task OnBlurAsync(FocusEventArgs e)
{
IsFocused = false;
if (_hasAffixWrapper)
SetClasses();
if (_compositionInputting)
{
_compositionInputting = false;
}
await ChangeValue(true);
if (OnBlur.HasDelegate)
{
await OnBlur.InvokeAsync(e);
}
}
private async void OnFocusInternal(JsonElement e) => await OnFocusAsync(new());
internal virtual async Task OnFocusAsync(FocusEventArgs e)
{
IsFocused = true;
if (_hasAffixWrapper)
SetClasses();
if (OnFocus.HasDelegate)
{
await OnFocus.InvokeAsync(e);
}
}
internal virtual void OnCompositionStart(JsonElement e)
{
_compositionInputting = true;
}
internal virtual void OnCompositionEnd(JsonElement e)
{
_compositionInputting = false;
}
private void ToggleClearBtn()
{
2020-04-24 18:32:50 +08:00
Suffix = (builder) =>
{
builder.OpenComponent<Icon>(31);
builder.AddAttribute(32, "Type", "close-circle");
builder.AddAttribute(33, "Class", GetClearIconCls());
if (string.IsNullOrEmpty(Value?.ToString()))
{
builder.AddAttribute(34, "Style", "visibility: hidden;");
}
else
{
builder.AddAttribute(34, "Style", "visibility: visible;");
}
builder.AddAttribute(35, "OnClick", CallbackFactory.Create<MouseEventArgs>(this, async (args) =>
{
CurrentValue = default;
IsFocused = true;
await this.FocusAsync(Ref);
if (OnChange.HasDelegate)
await OnChange.InvokeAsync(Value);
ToggleClearBtn();
}));
builder.CloseComponent();
2020-04-24 18:32:50 +08:00
};
}
protected void DebounceChangeValue()
{
_debounceTimer?.Dispose();
_debounceTimer = new Timer(DebounceTimerIntervalOnTick, null, DebounceMilliseconds, DebounceMilliseconds);
}
protected void DebounceTimerIntervalOnTick(object state)
{
InvokeAsync(async () => await ChangeValue(true));
}
private async Task ChangeValue(bool ignoreDebounce = false)
{
if (DebounceEnabled)
{
if (!ignoreDebounce)
{
DebounceChangeValue();
return;
}
_debounceTimer?.Dispose();
if (_debounceTimer != null)
{
_debounceTimer = null;
}
}
if (!_compositionInputting)
{
if (!EqualityComparer<TValue>.Default.Equals(CurrentValue, _inputValue))
{
CurrentValue = _inputValue;
if (OnChange.HasDelegate)
{
await OnChange.InvokeAsync(Value);
}
}
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
DomEventService.AddEventListener(Ref, "compositionstart", OnCompositionStart);
DomEventService.AddEventListener(Ref, "compositionend", OnCompositionEnd);
if (this.AutoFocus)
{
IsFocused = true;
await this.FocusAsync(Ref);
}
DomEventService.AddEventListener(Ref, "focus", OnFocusInternal, true);
}
}
protected override void Dispose(bool disposing)
{
DomEventService.RemoveEventListerner<JsonElement>(Ref, "compositionstart", OnCompositionStart);
DomEventService.RemoveEventListerner<JsonElement>(Ref, "compositionend", OnCompositionEnd);
DomEventService.RemoveEventListerner<JsonElement>(Ref, "focus", OnFocusInternal);
_debounceTimer?.Dispose();
base.Dispose(disposing);
}
protected virtual string GetClearIconCls()
{
return $"{PrefixCls}-clear-icon";
}
protected override void OnValueChange(TValue value)
{
base.OnValueChange(value);
_inputValue = value;
}
/// <summary>
/// Invoked when user add/remove content
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
2020-04-24 18:32:50 +08:00
protected virtual async void OnInputAsync(ChangeEventArgs args)
{
bool flag = !(!string.IsNullOrEmpty(Value?.ToString()) && args != null && !string.IsNullOrEmpty(args.Value.ToString()));
2020-04-24 18:32:50 +08:00
if (TryParseValueFromString(args?.Value.ToString(), out TValue value, out var error))
{
_inputValue = value;
}
if (_allowClear && flag)
{
ToggleClearBtn();
}
if (OnInput.HasDelegate)
2020-03-26 10:45:35 +08:00
{
await OnInput.InvokeAsync(args);
2020-03-26 10:45:35 +08:00
}
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
2020-04-24 18:32:50 +08:00
if (builder != null)
{
base.BuildRenderTree(builder);
2020-04-24 18:32:50 +08:00
string container = "input";
2020-04-24 18:32:50 +08:00
if (AddOnBefore != null || AddOnAfter != null)
{
container = "groupWrapper";
_hasAffixWrapper = true;
builder.OpenElement(1, "span");
builder.AddAttribute(2, "class", GroupWrapperClass);
builder.AddAttribute(3, "style", WrapperStyle);
builder.OpenElement(4, "span");
builder.AddAttribute(5, "class", $"{PrefixCls}-wrapper {PrefixCls}-group");
2020-04-24 18:32:50 +08:00
}
2020-04-24 18:32:50 +08:00
if (AddOnBefore != null)
{
_hasAffixWrapper = true;
2020-04-24 18:32:50 +08:00
// addOnBefore
builder.OpenElement(11, "span");
builder.AddAttribute(12, "class", $"{PrefixCls}-group-addon");
builder.AddContent(13, AddOnBefore);
2020-04-24 18:32:50 +08:00
builder.CloseElement();
}
2020-04-24 18:32:50 +08:00
if (Prefix != null || Suffix != null)
{
_hasAffixWrapper = true;
builder.OpenElement(21, "span");
builder.AddAttribute(22, "class", AffixWrapperClass);
2020-04-24 18:32:50 +08:00
if (container == "input")
{
container = "affixWrapper";
builder.AddAttribute(3, "style", WrapperStyle);
2020-04-24 18:32:50 +08:00
}
feat(module: overlay): OverlayTrigger not bound to a div (#937) * feat(module:overlay): OverlayTrigger not bound to a div * feat(module:overlay): OverlayTrigger not bound to a div * feat(module:overlay): Logic transfer to single Overlay * feat(module:overlay): remove obsolete duplication * feat(module:Tooltip): Add for unbounded oncontextmenu event handler * feat(module:tooltip): unbound js event listeners remove * docs(module:tooltip): unbound explanation * fix(module:button): attach Ref to top level html element @ref * feat(module:dropdown&tooltip&popconfirm&popover): Overlay not bound to a div * docs(module:dropdown&tooltip&popconfirm&popover): unbound explanation * feat(module:OverlayTrigger): common logic relocation * feat(module:overlaytrigger): Overlay not bound to a div * feat(module:DatePicker): Overlay not bound to a div * feat(module:select): Overlay not boud to div * fix(module:select): onclickarrow event relocation * fix(module:select): rename Show to OnArrowClick * feat(module:avatar): Overlay not bound to a div * docs(module:avatar): demo switch to unbound version * feat(module:autocomplete): partial OverlayTrigger not bound to a div * feat(module:slider): tooltip * docs(module:slider): tooltip * fix(module:overlay): add SetVisible method * feat: set Ref where missing, performance components register Ref when missing IsFixed flag for CascadeValue changed hard-code sequence numbers when using RenderTreeBuilder Rate component use Tooltip Unbound version Tabs test fix * fix: revert changes (accidental) * feat(module:upload): tooltip with unbound usage * feat(module:table): column use of unbound tooltip * feat(module:autocomplete):overlay unbound from div * fix(module:upload): missing div restore Co-authored-by: James Yeung <shunjiey@hotmail.com>
2021-01-21 17:20:10 +08:00
if (WrapperRefBack != null)
{
builder.AddElementReferenceCapture(24, r => WrapperRefBack.Current = r);
}
2020-04-24 18:32:50 +08:00
}
if (Prefix != null)
{
_hasAffixWrapper = true;
2020-04-24 18:32:50 +08:00
// prefix
builder.OpenElement(31, "span");
builder.AddAttribute(32, "class", $"{PrefixCls}-prefix");
builder.AddContent(33, Prefix);
2020-04-24 18:32:50 +08:00
builder.CloseElement();
}
// input
builder.OpenElement(41, "input");
builder.AddAttribute(42, "class", ClassMapper.Class);
if (container == "input")
{
builder.AddAttribute(43, "style", Style);
}
bool needsDisabled = Disabled;
2020-04-24 18:32:50 +08:00
if (Attributes != null)
{
builder.AddMultipleAttributes(44, Attributes);
if (!Attributes.TryGetValue("disabled", out object disabledAttribute))
{
needsDisabled = ((bool?)disabledAttribute ?? needsDisabled) | Disabled;
}
}
if (AdditionalAttributes != null)
{
builder.AddMultipleAttributes(45, AdditionalAttributes);
if (!AdditionalAttributes.TryGetValue("disabled", out object disabledAttribute))
{
needsDisabled = ((bool?)disabledAttribute ?? needsDisabled) | Disabled;
}
2020-04-24 18:32:50 +08:00
}
builder.AddAttribute(50, "Id", Id);
builder.AddAttribute(51, "type", Type);
builder.AddAttribute(60, "placeholder", Placeholder);
builder.AddAttribute(61, "value", CurrentValue);
builder.AddAttribute(62, "disabled", needsDisabled);
// onchange 和 onblur 事件会导致点击 OnSearch 按钮时不触发 Click 事件,暂时取消这两个事件
if (!IgnoreOnChangeAndBlur)
{
builder.AddAttribute(70, "onchange", CallbackFactory.Create(this, OnChangeAsync));
builder.AddAttribute(71, "onblur", CallbackFactory.Create(this, OnBlurAsync));
}
builder.AddAttribute(72, "onkeypress", CallbackFactory.Create(this, OnKeyPressAsync));
builder.AddAttribute(73, "onkeydown", CallbackFactory.Create(this, OnkeyDownAsync));
builder.AddAttribute(74, "onkeyup", CallbackFactory.Create(this, OnKeyUpAsync));
builder.AddAttribute(75, "oninput", CallbackFactory.Create(this, OnInputAsync));
//TODO: Use built in @onfocus once https://github.com/dotnet/aspnetcore/issues/30070 is solved
//builder.AddAttribute(76, "onfocus", CallbackFactory.Create(this, OnFocusAsync));
builder.AddAttribute(77, "onmouseup", CallbackFactory.Create(this, OnMouseUpAsync));
builder.AddElementReferenceCapture(90, r => Ref = r);
builder.CloseElement();
2020-04-24 18:32:50 +08:00
if (Suffix != null)
{
_hasAffixWrapper = true;
2020-04-24 18:32:50 +08:00
// suffix
builder.OpenElement(91, "span");
builder.AddAttribute(92, "class", $"{PrefixCls}-suffix");
builder.AddContent(93, Suffix);
2020-04-24 18:32:50 +08:00
builder.CloseElement();
}
2020-04-24 18:32:50 +08:00
if (Prefix != null || Suffix != null)
{
builder.CloseElement();
}
if (AddOnAfter != null)
{
_hasAffixWrapper = true;
2020-04-24 18:32:50 +08:00
// addOnAfter
builder.OpenElement(100, "span");
builder.AddAttribute(101, "class", $"{PrefixCls}-group-addon");
builder.AddContent(102, AddOnAfter);
2020-04-24 18:32:50 +08:00
builder.CloseElement();
}
if (AddOnBefore != null || AddOnAfter != null)
{
builder.CloseElement();
builder.CloseElement();
}
}
}
}
}