mirror of
https://gitee.com/ant-design-blazor/ant-design-blazor.git
synced 2024-11-29 18:48:50 +08:00
feat(module: mentions): Add dynamic async loading of mentions (#3503)
* Add dynamic async loading of mentions along with more tests. * Make symbol a parameter
This commit is contained in:
parent
c9d95e31a8
commit
161aef5507
@ -10,12 +10,12 @@
|
||||
InlineFlexMode=false>
|
||||
<Overlay>
|
||||
<CascadingValue Value=this>
|
||||
@if (OriginalOptions.Count > 0)
|
||||
@if (ShowOptions.Count > 0)
|
||||
{
|
||||
<ul class="ant-mentions-dropdown-menu ant-mentions-dropdown-menu-root ant-mentions-dropdown-menu-vertical">
|
||||
@foreach (var item in ShowOptions)
|
||||
{
|
||||
<MentionsOption Value="@item.Value">@item.ChildContent</MentionsOption>
|
||||
<MentionsOption Value="@item.Value">@item.Display</MentionsOption>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AntDesign.Internal;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@ -11,34 +12,70 @@ namespace AntDesign
|
||||
{
|
||||
public partial class Mentions
|
||||
{
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
[Parameter] public bool Disable { get; set; }
|
||||
[Parameter] public uint Rows { get; set; } = 3;
|
||||
[Parameter] public bool Focused { get; set; }
|
||||
[Parameter] public bool Readonly { get; set; }
|
||||
[Parameter] public bool Loading { get; set; }
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
[Parameter] public Dictionary<string, object> Attributes { get; set; }
|
||||
[Inject] public IJSRuntime JS { get; set; }
|
||||
[Parameter] public string Placeholder { get; set; }
|
||||
[Parameter] public string Value { get; set; } = String.Empty;
|
||||
[Parameter] public EventCallback<string> ValueChanged { get; set; }
|
||||
[Parameter]
|
||||
public bool Disable { get; set; }
|
||||
|
||||
[Parameter] public RenderFragment<MentionsTextareaTemplateOptions> TextareaTemplate { get; set; }
|
||||
[Parameter]
|
||||
public uint Rows { get; set; } = 3;
|
||||
|
||||
[Parameter]
|
||||
public bool Focused { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Readonly { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Loading { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string, object> Attributes { get; set; }
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JS { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically load mention options for display when the user types a value after the @ symbol
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string, CancellationToken, Task<IEnumerable<MentionsDynamicOption>>> LoadOptions { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment<MentionsTextareaTemplateOptions> TextareaTemplate { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Prefix { get; set; } = "@";
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
internal List<MentionsDynamicOption> OriginalOptions { get; set; } = new List<MentionsDynamicOption>();
|
||||
|
||||
internal List<MentionsDynamicOption> ShowOptions { get; } = new List<MentionsDynamicOption>();
|
||||
|
||||
internal List<MentionsOption> OriginalOptions { get; set; } = new List<MentionsOption>();
|
||||
internal List<MentionsOption> ShowOptions { get; } = new List<MentionsOption>();
|
||||
private OverlayTrigger _overlayTrigger;
|
||||
|
||||
internal string ActiveOptionValue { get; set; }
|
||||
|
||||
internal int ActiveOptionIndex => ShowOptions.FindIndex(x => x.Value == ActiveOptionValue);
|
||||
|
||||
private void SetClassMap()
|
||||
{
|
||||
var prefixCls = "ant-mentions";
|
||||
this.ClassMapper.Clear()
|
||||
ClassMapper.Clear()
|
||||
.Add(prefixCls)
|
||||
.If($"{prefixCls}-disable", () => this.Disable)
|
||||
.If($"{prefixCls}-focused", () => this.Focused)
|
||||
.If($"{prefixCls}-disable", () => Disable)
|
||||
.If($"{prefixCls}-focused", () => Focused)
|
||||
.If($"{prefixCls}-rtl", () => RTL)
|
||||
;
|
||||
}
|
||||
@ -52,12 +89,18 @@ namespace AntDesign
|
||||
internal void AddOption(MentionsOption option)
|
||||
{
|
||||
if (option == null) return;
|
||||
|
||||
var opt = OriginalOptions.Find(x => x.Value == option.Value);
|
||||
if (opt == null)
|
||||
{
|
||||
OriginalOptions.Add(option);
|
||||
OriginalOptions.Add(new MentionsDynamicOption
|
||||
{
|
||||
Value = option.Value,
|
||||
Display = option.ChildContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetMentionNames()
|
||||
{
|
||||
var r = new List<string>();
|
||||
@ -69,6 +112,7 @@ namespace AntDesign
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
@ -79,6 +123,7 @@ namespace AntDesign
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void PrevOption()
|
||||
{
|
||||
@ -86,6 +131,7 @@ namespace AntDesign
|
||||
ActiveOptionValue = ShowOptions[index].Value;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void NextOption()
|
||||
{
|
||||
@ -93,6 +139,7 @@ namespace AntDesign
|
||||
ActiveOptionValue = ShowOptions[index].Value;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task EnterOption()
|
||||
{
|
||||
@ -105,14 +152,9 @@ namespace AntDesign
|
||||
await _overlayTrigger.Hide();
|
||||
}
|
||||
|
||||
async Task ShowOverlay(bool resetOptions, bool reCalcPosition)
|
||||
async Task ShowOverlay(bool reCalcPosition)
|
||||
{
|
||||
await JS.InvokeAsync<double[]>(JSInteropConstants.SetPopShowFlag, true);
|
||||
if (resetOptions)
|
||||
{
|
||||
ShowOptions.Clear();
|
||||
ShowOptions.AddRange(OriginalOptions);
|
||||
}
|
||||
ActiveOptionValue = ShowOptions.First().Value;
|
||||
if (reCalcPosition)
|
||||
{
|
||||
@ -127,6 +169,7 @@ namespace AntDesign
|
||||
}
|
||||
await InvokeStateHasChangedAsync();
|
||||
}
|
||||
|
||||
async void OnKeyDown(KeyboardEventArgs args)
|
||||
{ //↑、↓、回车键只能放进js里判断,不然在Sever异步模式下无法拦截原键功能
|
||||
//开启浮窗的判断放在oninput里,不然会有问题
|
||||
@ -137,55 +180,82 @@ namespace AntDesign
|
||||
{
|
||||
Value = args.Value.ToString();
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
if (Value.EndsWith("@"))
|
||||
|
||||
if (Value.EndsWith(Prefix))
|
||||
{
|
||||
await ShowOverlay(true, true);
|
||||
await LoadItems(string.Empty);
|
||||
await ShowOrHideBasedOnAvailableShowOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var focusPosition = await JS.InvokeAsync<int>(JSInteropConstants.GetProp, _overlayTrigger.Ref, "selectionStart");
|
||||
if (focusPosition == 0)
|
||||
{
|
||||
await HideOverlay();
|
||||
return;
|
||||
};
|
||||
var showPop = false;
|
||||
|
||||
var v = Value.Substring(0, focusPosition); //从光标处切断,向前找匹配项
|
||||
var lastIndex = v.LastIndexOf("@");
|
||||
var lastIndex = v.LastIndexOf(Prefix);
|
||||
if (lastIndex >= 0)
|
||||
{
|
||||
var lastOption = v.Substring(lastIndex + 1);
|
||||
ShowOptions.Clear();
|
||||
ShowOptions.AddRange(OriginalOptions.Where(x => x.Value.Contains(lastOption, StringComparison.OrdinalIgnoreCase)).ToList());
|
||||
if (ShowOptions.Count > 0) showPop = true;
|
||||
await LoadItems(lastOption);
|
||||
}
|
||||
|
||||
if (showPop)
|
||||
await ShowOrHideBasedOnAvailableShowOptions();
|
||||
}
|
||||
|
||||
private async Task LoadItems(string searchValue)
|
||||
{
|
||||
if (LoadOptions is null)
|
||||
{
|
||||
await ShowOverlay(false, true);
|
||||
var optionsToShow = OriginalOptions.Where(x => x.Value.Contains(searchValue, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
SetOptionsToShow(optionsToShow);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HideOverlay();
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
var options = await LoadOptions.Invoke(searchValue, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
SetOptionsToShow(options);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOptionsToShow(IEnumerable<MentionsDynamicOption> optionsToShow)
|
||||
{
|
||||
ShowOptions.Clear();
|
||||
ShowOptions.AddRange(optionsToShow);
|
||||
}
|
||||
|
||||
private Task ShowOrHideBasedOnAvailableShowOptions()
|
||||
{
|
||||
return ShowOptions.Count > 0
|
||||
? ShowOverlay(true)
|
||||
: HideOverlay();
|
||||
}
|
||||
|
||||
internal async Task ItemClick(string optionValue)
|
||||
{
|
||||
var focusPosition = await JS.InvokeAsync<int>(JSInteropConstants.GetProp, _overlayTrigger.Ref, "selectionStart");
|
||||
var preText = Value.Substring(0, focusPosition);
|
||||
preText = preText.LastIndexOf("@") >= 0 ? Value.Substring(0, preText.LastIndexOf("@")) : preText;
|
||||
preText = preText.LastIndexOf(Prefix) >= 0 ? Value.Substring(0, preText.LastIndexOf(Prefix)) : preText;
|
||||
if (preText.EndsWith(' ')) preText = preText.Substring(0, preText.Length - 1);
|
||||
var nextText = Value.Substring(focusPosition);
|
||||
if (nextText.StartsWith(' ')) nextText = nextText.Substring(1);
|
||||
var option = " @" + optionValue + " ";
|
||||
|
||||
|
||||
Value = preText + option + nextText;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
|
||||
|
||||
var pos = preText.Length + option.Length;
|
||||
var js = $"document.querySelector('[_bl_{_overlayTrigger.Ref.Id}]').selectionStart = {pos};";
|
||||
js += $"document.querySelector('[_bl_{_overlayTrigger.Ref.Id}]').selectionEnd = {pos}";
|
||||
await JS.InvokeVoidAsync("eval", js);
|
||||
|
||||
|
||||
await HideOverlay();
|
||||
await InvokeStateHasChangedAsync();
|
||||
}
|
||||
|
11
components/mentions/MentionsDynamicOption.cs
Normal file
11
components/mentions/MentionsDynamicOption.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AntDesign
|
||||
{
|
||||
public class MentionsDynamicOption
|
||||
{
|
||||
public string Value { get; set; }
|
||||
|
||||
public RenderFragment Display { get; set; }
|
||||
}
|
||||
}
|
@ -1,13 +1,35 @@
|
||||
<div>
|
||||
To do
|
||||
@using System.Threading;
|
||||
<div>
|
||||
<Mentions Style="width:100%" Loading="true" LoadOptions="@LoadMentions">
|
||||
</Mentions>
|
||||
</div>
|
||||
|
||||
@*<div>
|
||||
<Mentions Style="width:100%" Loading="true">
|
||||
<MentionsOption Value="afc163">
|
||||
<Icon Type="setting" Theme="fill" />
|
||||
<span>afc163</span>
|
||||
<span>zombieJ</span>
|
||||
</MentionsOption>
|
||||
</Mentions>
|
||||
</div>*@
|
||||
@code
|
||||
{
|
||||
private RenderFragment<string> OptionDisplay => value =>@<span>Display: @value</span>;
|
||||
|
||||
public async Task<IEnumerable<MentionsDynamicOption>> LoadMentions(string search, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Enumerable.Empty<MentionsDynamicOption>();
|
||||
}
|
||||
|
||||
var fakeAsyncResults = new List<string>()
|
||||
{
|
||||
search
|
||||
};
|
||||
|
||||
for (var i = 0; i < 25; i++){
|
||||
fakeAsyncResults.Add(search + i);
|
||||
}
|
||||
|
||||
return fakeAsyncResults.Select(x => new MentionsDynamicOption
|
||||
{
|
||||
Value = x,
|
||||
Display = OptionDisplay(x)
|
||||
});
|
||||
}
|
||||
}
|
@ -11,4 +11,4 @@ title:
|
||||
|
||||
## en-US
|
||||
|
||||
async
|
||||
When the list of matched content is returned asyncronously.
|
||||
|
@ -1,21 +1,148 @@
|
||||
@using AntDesign.Internal
|
||||
@using System.Threading;
|
||||
@using static AntDesign.TextArea;
|
||||
@inherits AntDesignTestBase
|
||||
|
||||
@code {
|
||||
private void SetupJavascript()
|
||||
{
|
||||
JSInterop.SetupVoid("AntDesign.interop.mentionsHelper.setEditorKeyHandler", _ => true);
|
||||
JSInterop.Setup<int>("AntDesign.interop.mentionsHelper.getProp", _ => true).SetResult(1);
|
||||
JSInterop.Setup<Double[]>("AntDesign.interop.mentionsHelper.setPopShowFlag", _ => true).SetResult(new[] { 1d, 1d });
|
||||
JSInterop.Setup<Double[]>("AntDesign.interop.mentionsHelper.getCursorXY", _ => true).SetResult(new[] { 1d, 1d });
|
||||
}
|
||||
|
||||
|
||||
private IRenderedComponent<AntDesign.Mentions> GetStandardRenderSystemUnderTest()
|
||||
{
|
||||
return Render<AntDesign.Mentions>(
|
||||
@<AntDesign.Mentions>
|
||||
<AntDesign.MentionsOption Value="testUsername">Test User</AntDesign.MentionsOption>
|
||||
</AntDesign.Mentions>
|
||||
@<AntDesign.Mentions>
|
||||
<AntDesign.MentionsOption Value="testUsername">Test User</AntDesign.MentionsOption>
|
||||
<AntDesign.MentionsOption Value="anotherUsername">Another User</AntDesign.MentionsOption>
|
||||
</AntDesign.Mentions>
|
||||
);
|
||||
}
|
||||
|
||||
private IRenderedComponent<AntDesign.Mentions> GetLoadOptionsRenderSystemUnderTest(Func<string, CancellationToken, Task<IEnumerable<MentionsDynamicOption>>> loadOptions)
|
||||
{
|
||||
return Render<AntDesign.Mentions>(
|
||||
@<AntDesign.Mentions LoadOptions="@loadOptions" />
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("username")]
|
||||
public async Task ItShouldCallLoadOptionsWithSearchValueWhenUserEntersValueAfterPrefix(string testEntry)
|
||||
{
|
||||
SetupJavascript();
|
||||
JSInterop.Setup<int>(
|
||||
"AntDesign.interop.mentionsHelper.getProp",
|
||||
invocation => (string?)invocation.Arguments[1] == "selectionStart")
|
||||
.SetResult(testEntry.Length + 1);
|
||||
|
||||
string? valueSentToCallback = null;
|
||||
var systemUnderTest = GetLoadOptionsRenderSystemUnderTest((value, _) =>
|
||||
{
|
||||
valueSentToCallback = value;
|
||||
|
||||
return Task.FromResult(Enumerable.Empty<MentionsDynamicOption>());
|
||||
});
|
||||
|
||||
var textArea = systemUnderTest.Find("textarea");
|
||||
|
||||
await textArea.InputAsync(new ChangeEventArgs
|
||||
{
|
||||
Value = "@" + testEntry
|
||||
});
|
||||
|
||||
valueSentToCallback.Should().BeEquivalentTo(testEntry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItShouldDisplayOptionsReturnedFromLoadOptionsCall()
|
||||
{
|
||||
var testEntry = "username";
|
||||
|
||||
SetupJavascript();
|
||||
JSInterop.Setup<int>(
|
||||
"AntDesign.interop.mentionsHelper.getProp",
|
||||
invocation => (string?)invocation.Arguments[1] == "selectionStart")
|
||||
.SetResult(testEntry.Length + 1);
|
||||
|
||||
var systemUnderTest = GetLoadOptionsRenderSystemUnderTest((value, _) =>
|
||||
{
|
||||
return Task.FromResult(new[]
|
||||
{
|
||||
new MentionsDynamicOption
|
||||
{
|
||||
Display = @<span>User Name 1</span>,
|
||||
Value = "username1"
|
||||
},
|
||||
new MentionsDynamicOption
|
||||
{
|
||||
Display = @<span>User Name 2</span>,
|
||||
Value = "username2"
|
||||
}
|
||||
}.AsEnumerable());
|
||||
});
|
||||
|
||||
var textArea = systemUnderTest.Find("textarea");
|
||||
|
||||
await textArea.InputAsync(new ChangeEventArgs
|
||||
{
|
||||
Value = "@" + testEntry
|
||||
});
|
||||
|
||||
systemUnderTest.MarkupMatches(@"
|
||||
<div class=""ant-mentions"">
|
||||
<textarea value="""" class=""rc-textarea"" rows=""3""></textarea>
|
||||
</div>
|
||||
<div class:ignore style:ignore>
|
||||
<ul class:ignore>
|
||||
<li class=""ant-mentions-dropdown-menu-item ant-mentions-dropdown-menu-item-active"">
|
||||
<span>User Name 1</span>
|
||||
</li>
|
||||
<li class=""ant-mentions-dropdown-menu-item"">
|
||||
<span>User Name 2</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ItShouldDisplayOptionsWithValueContainingSearchValueWhenUserTypesForStaticOptions()
|
||||
{
|
||||
var testEntry = "anoth";
|
||||
|
||||
SetupJavascript();
|
||||
JSInterop.Setup<int>(
|
||||
"AntDesign.interop.mentionsHelper.getProp",
|
||||
invocation => (string?)invocation.Arguments[1] == "selectionStart")
|
||||
.SetResult(testEntry.Length + 1);
|
||||
|
||||
var systemUnderTest = GetStandardRenderSystemUnderTest();
|
||||
|
||||
var textArea = systemUnderTest.Find("textarea");
|
||||
|
||||
await textArea.InputAsync(new ChangeEventArgs
|
||||
{
|
||||
Value = "@" + testEntry
|
||||
});
|
||||
|
||||
systemUnderTest.MarkupMatches(@"
|
||||
<div class=""ant-mentions"">
|
||||
<textarea value="""" class=""rc-textarea"" rows=""3""></textarea>
|
||||
</div>
|
||||
<div class:ignore style:ignore>
|
||||
<ul class:ignore>
|
||||
<li class=""ant-mentions-dropdown-menu-item ant-mentions-dropdown-menu-item-active"">
|
||||
Another User
|
||||
</li>
|
||||
</ul>
|
||||
</div>");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ItShouldRenderByDefaultWithTextarea()
|
||||
{
|
||||
@ -31,11 +158,14 @@
|
||||
<li class=""ant-mentions-dropdown-menu-item"">
|
||||
Test User
|
||||
</li>
|
||||
<li class=""ant-mentions-dropdown-menu-item"">
|
||||
Another User
|
||||
</li>
|
||||
</div>");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StandardRender_ShouldCallJavascriptCallbackToDisplayOverlayWhenTypingAtSymbol()
|
||||
public void StandardRender_ShouldCallJavascriptCallbackToDisplayOverlayWhenTypingPrefix()
|
||||
{
|
||||
SetupJavascript();
|
||||
var systemUnderTest = GetStandardRenderSystemUnderTest();
|
||||
@ -48,14 +178,21 @@
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TemplatedRender_ShouldCallJavascriptCallbackToDisplayOverlayWhenTypingAtSymbol()
|
||||
public void ShouldCallJavascriptCallbackToDisplayOverlayWhenTypingProvidedPrefix()
|
||||
{
|
||||
const string prefix = "#";
|
||||
|
||||
SetupJavascript();
|
||||
var systemUnderTest = GetStandardRenderSystemUnderTest();
|
||||
var systemUnderTest = Render<AntDesign.Mentions>(
|
||||
@<AntDesign.Mentions Prefix="@prefix">
|
||||
<AntDesign.MentionsOption Value="testUsername">Test User</AntDesign.MentionsOption>
|
||||
<AntDesign.MentionsOption Value="anotherUsername">Another User</AntDesign.MentionsOption>
|
||||
</AntDesign.Mentions>
|
||||
);
|
||||
|
||||
JSInterop.Setup<Double[]>("AntDesign.interop.mentionsHelper.setPopShowFlag", true);
|
||||
|
||||
systemUnderTest.Find("textarea").Input("@");
|
||||
systemUnderTest.Find("textarea").Input(prefix);
|
||||
|
||||
JSInterop.VerifyInvoke("AntDesign.interop.mentionsHelper.setPopShowFlag", 1);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user