Select2.razor.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. using Microsoft.AspNetCore.Components;
  2. using Microsoft.JSInterop;
  3. using Newtonsoft.Json;
  4. using System.ComponentModel.DataAnnotations;
  5. namespace KeudellCoding.Blazor.AdvancedBlazorSelect2
  6. {
  7. public partial class Select2<TItem, TSource> : ComponentBase where TSource : IEnumerable<TItem>
  8. {
  9. private const string JS_COMMAND_PREFIX = "KeudellCoding_Blazor_AdvancedBlazorSelect2Component";
  10. private const string HTML_ID_PREFIX = "select2-input-";
  11. //===========================================================================================================================================
  12. [Inject] public IJSRuntime JSRuntime { get; set; }
  13. //===========================================================================================================================================
  14. [Parameter, Required] public Func<TItem, string> IdSelector { get; set; }
  15. [Parameter, Required] public Func<TItem, string> TextSelector { get; set; }
  16. [Parameter, Required] public Func<TSource, string, CancellationToken, Task<List<TItem>>> FilterFunction { get; set; }
  17. [Parameter, Required] public Func<TSource, string, CancellationToken, Task<TItem>> GetElementById { get; set; }
  18. [Parameter, Required] public TSource Datasource { get; set; }
  19. [Parameter] public List<TItem> Value { get; set; }
  20. [Parameter] public EventCallback OnValueChanged { get; set; } = EventCallback.Empty;
  21. [Parameter] public Func<TItem, MarkupString> OptionLayout { get; set; }
  22. [Parameter] public InputSelect2Options Select2Options { get; set; } = new InputSelect2Options();
  23. [Parameter] public ushort MaxItemsPerPage { get; set; } = 50;
  24. [Parameter] public bool Multiselect { get => Select2Options.Multiple; set => Select2Options.Multiple = value; }
  25. //[Parameter] public IMemoryCache Cache { get; set; } = null;
  26. [Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> AdditionalAttributes { get; set; }
  27. //===========================================================================================================================================
  28. private readonly string inputGuid = Guid.NewGuid().ToString();
  29. private CancellationTokenSource searchCancellationToken = null;
  30. private DotNetObjectReference<Select2<TItem, TSource>> dotNetRef;
  31. //===========================================================================================================================================
  32. protected override void OnInitialized()
  33. {
  34. base.OnInitialized();
  35. if (Datasource.Count() > MaxItemsPerPage && Select2Options.MinimumInputLength < 1)
  36. Select2Options.MinimumInputLength = 1;
  37. dotNetRef = DotNetObjectReference.Create(this);
  38. }
  39. protected override async Task OnAfterRenderAsync(bool firstRender)
  40. {
  41. base.OnAfterRender(firstRender);
  42. if (firstRender)
  43. {
  44. await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.init", HTML_ID_PREFIX + inputGuid, JsonConvert.SerializeObject(Select2Options), dotNetRef);
  45. await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.onChange", HTML_ID_PREFIX + inputGuid, dotNetRef);
  46. await updateSelect2Async(false);
  47. }
  48. await checkIfUpdateNeededAsync();
  49. }
  50. //===========================================================================================================================================
  51. public async Task updateSelect2Async(bool triggerChange)
  52. {
  53. checkValueForLimit();
  54. await JSRuntime.InvokeVoidAsync($"{JS_COMMAND_PREFIX}.updateSelected", HTML_ID_PREFIX + inputGuid, getFromTItem(Value.AsQueryable()), triggerChange);
  55. }
  56. private async Task checkIfUpdateNeededAsync()
  57. {
  58. Value.RemoveAll(i => i == null);
  59. var selectedIds = await JSRuntime.InvokeAsync<string[]>($"{JS_COMMAND_PREFIX}.getSelectedIds", HTML_ID_PREFIX + inputGuid);
  60. if (!new HashSet<string>(selectedIds).SetEquals(Value.Select(IdSelector)))
  61. {
  62. await updateSelect2Async(true);
  63. }
  64. }
  65. private Select2Item getFromTItem(TItem item)
  66. {
  67. return new Select2Item
  68. {
  69. Id = IdSelector(item),
  70. Text = TextSelector(item),
  71. FormatedResult = OptionLayout == null ? TextSelector(item) : OptionLayout(item).ToString()
  72. };
  73. }
  74. private IEnumerable<Select2Item> getFromTItem(IEnumerable<TItem> items) => items.Select(getFromTItem);
  75. private object getCacheKey(string id) => new { Namespace = "KeudellCoding.Blazor.AdvancedBlazorSelect2", Type = "SearchCache", Select2Id = inputGuid, ObjId = id };
  76. private object getCacheKey(TItem item) => getCacheKey(IdSelector(item));
  77. private void checkValueForLimit()
  78. {
  79. if (Value == null)
  80. Value = new List<TItem> { };
  81. if (!Multiselect && Value.Count > 1)
  82. Value.RemoveRange(1, Value.Count - 1);
  83. }
  84. private async Task<TItem> getByIdAsync(string id)
  85. {
  86. if (string.IsNullOrEmpty(id))
  87. return default;
  88. //if (Cache != null && Cache.TryGetValue(getCacheKey(id), out TItem value))
  89. // return value;
  90. else
  91. {
  92. var result = await GetElementById(Datasource, id, default);
  93. //if (Cache != null)
  94. // Cache.Set(getCacheKey(id), result);
  95. return result;
  96. }
  97. }
  98. private async Task<IEnumerable<TItem>> getByIdAsync(IEnumerable<string> ids) => (await Task.WhenAll(ids.Select(getByIdAsync))).AsQueryable();
  99. private List<TItem> getPageOfResult(List<TItem> select2Items, int page, out bool hasNextPage)
  100. {
  101. page = page < 1 ? 1 : page;
  102. var elementsFromPage = select2Items.Where(e => e != null).Skip((page - 1) * MaxItemsPerPage).Take(MaxItemsPerPage + 1).ToList();
  103. hasNextPage = elementsFromPage.Count() > MaxItemsPerPage;
  104. if (hasNextPage)
  105. elementsFromPage.RemoveAt(elementsFromPage.Count() - 1);
  106. return elementsFromPage;
  107. }
  108. //===========================================================================================================================================
  109. [JSInvokable("Select2OnChange")]
  110. public async Task OnSelectionChangedAsync(IEnumerable<string> newSelectedIds)
  111. {
  112. Value.Clear();
  113. var selectedElements = await getByIdAsync(newSelectedIds.Where(i => i != null));
  114. if (!Multiselect)
  115. selectedElements = selectedElements.Take(1);
  116. Value.AddRange(selectedElements);
  117. _ = OnValueChanged.InvokeAsync(this);
  118. }
  119. [JSInvokable("Select2OnSearch")]
  120. public async Task<SearchServerResponse> OnSearchAsync(string searchTerm, int page)
  121. {
  122. if (searchCancellationToken != null)
  123. searchCancellationToken.Cancel();
  124. searchCancellationToken = new CancellationTokenSource();
  125. var returnItem = new SearchServerResponse { };
  126. var hasMoreResults = false;
  127. var filteredDatasource = await FilterFunction(Datasource, searchTerm, searchCancellationToken.Token);
  128. var searchResults = await Task.Run(() => getPageOfResult(filteredDatasource, page, out hasMoreResults), searchCancellationToken.Token);
  129. if (searchCancellationToken.IsCancellationRequested)
  130. return returnItem;
  131. //if (Cache != null)
  132. // foreach (var searchResult in searchResults)
  133. // Cache.Set(getCacheKey(searchResult), searchResult);
  134. returnItem.Results = getFromTItem(searchResults.AsQueryable());
  135. returnItem.Pagination.More = hasMoreResults;
  136. return returnItem;
  137. }
  138. }
  139. }