diff --git a/src/components/BootstrapBlazor.Region/Components/SelectCity.razor b/src/components/BootstrapBlazor.Region/Components/SelectCity.razor index f704ac89..b541d032 100644 --- a/src/components/BootstrapBlazor.Region/Components/SelectCity.razor +++ b/src/components/BootstrapBlazor.Region/Components/SelectCity.razor @@ -36,12 +36,12 @@ @code { RenderFragment RenderItem(string item) => - @
+ @
@item
; diff --git a/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.cs b/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.cs index 2a24fec9..d8918872 100644 --- a/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.cs +++ b/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.cs @@ -60,13 +60,20 @@ public partial class SelectCity private readonly HashSet _values = []; private string? _searchText; private bool _showSearch; + private IEnumerable _filteredProvinces = Provinces; + private HashSet? _provinceItems; + private Dictionary>? _cityPinyin; private string? GetActiveClass(string item) => CssBuilder.Default() .AddClass("active", _values.Contains(item) && IsMultiple) .AddClass("active", CurrentValue == item && !IsMultiple) - .AddClass("prev", !string.IsNullOrEmpty(_searchText) && StartsWith(PinyinService.GetFirstLetters(item), _searchText)) + .AddClass("prev", IsPinyinMatch(item)) .Build(); + private bool IsPinyinMatch(string item) => !string.IsNullOrEmpty(_searchText) + && _cityPinyin?.TryGetValue(item, out var pinyin) == true + && StartsWith(pinyin, _searchText); + /// /// /// @@ -84,6 +91,7 @@ protected override void OnParametersSet() if (ShowSearch == false) { _searchText = ""; + _filteredProvinces = Provinces; } } @@ -123,7 +131,14 @@ protected override async Task OnAfterRenderAsync(bool firstRender) [JSInvokable] public void TriggerSearch(string v) { - _searchText = v.ToUpperInvariant(); + var searchText = v.ToUpperInvariant(); + if (_searchText == searchText) + { + return; + } + + _searchText = searchText; + _filteredProvinces = FilterProvinces(searchText); StateHasChanged(); } @@ -143,7 +158,7 @@ private async Task OnClearValue() private RenderFragment RenderCities() => builder => { - foreach (var item in GetProvinces()) + foreach (var item in _filteredProvinces) { builder.AddContent(0, RenderItem(item)); } @@ -190,19 +205,19 @@ private async Task OnSelectCity(string item) } } - private HashSet GetProvinces() + private IEnumerable FilterProvinces(string searchText) { - if (string.IsNullOrEmpty(_searchText)) + if (string.IsNullOrEmpty(searchText)) { return Provinces; } - if (PinyinService.ContainsChinese(_searchText)) + if (PinyinService.ContainsChinese(searchText)) { - return [.. Provinces.Where(i => i.Contains(_searchText) || GetCities(i).Any(city => city.Contains(_searchText)))]; + return Provinces.Where(i => i.Contains(searchText) || GetCities(i).Any(city => city.Contains(searchText))).ToArray(); } - return [.. GenerateProvincePinYin().Where(i => FilterProvince(i, _searchText)).Select(i => i.Name)]; + return GenerateProvincePinYin().Where(i => FilterProvince(i, searchText)).Select(i => i.Name).ToArray(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -211,16 +226,19 @@ private HashSet GetProvinces() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool StartsWith(HashSet source, string searchText) => source.Any(i => i.StartsWith(searchText, StringComparison.OrdinalIgnoreCase)); - private static HashSet? _provinceItems; - private HashSet GenerateProvincePinYin() { - _provinceItems ??= [.. Provinces.Select(i => new ProvinceItem() + if (_provinceItems is null) { - PinYin = PinyinService.GetFirstLetters(i), - Name = i, - Cities = GenerateCityPinYin(i) - })]; + _cityPinyin = []; + _provinceItems = [.. Provinces.Select(i => new ProvinceItem() + { + PinYin = PinyinService.GetFirstLetters(i), + Name = i, + Cities = GenerateCityPinYin(i) + })]; + } + return _provinceItems; } @@ -231,10 +249,15 @@ private HashSet GenerateProvincePinYin() _ => RegionService.GetCities(provinceName) }; - private HashSet GenerateCityPinYin(string provinceName) => [.. GetCities(provinceName).Select(i => new CityItem() + private HashSet GenerateCityPinYin(string provinceName) => [.. GetCities(provinceName).Select(i => { - PinYin = PinyinService.GetFirstLetters(i), - Name = i + var pinyin = PinyinService.GetFirstLetters(i); + _cityPinyin![i] = pinyin; + return new CityItem() + { + PinYin = pinyin, + Name = i + }; })]; private static readonly HashSet Provinces = [ diff --git a/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.js b/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.js index 311d51fe..6e460c5f 100644 --- a/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.js +++ b/src/components/BootstrapBlazor.Region/Components/SelectCity.razor.js @@ -2,6 +2,7 @@ import Data from "../../BootstrapBlazor/modules/data.js" import EventHandler from "../../BootstrapBlazor/modules/event-handler.js" import Input from "../../BootstrapBlazor/modules/input.js" import Popover from "../../BootstrapBlazor/modules/base-popover.js" +import { debounce } from "../../BootstrapBlazor/modules/utility.js" export function init(id, invoke, options) { const el = document.getElementById(id); @@ -37,9 +38,12 @@ const initSearch = region => { const { el, invoke, options } = region; const searchInput = el.querySelector(".search-text"); if (searchInput) { - Input.composition(searchInput, async v => { - await invoke.invokeMethodAsync(options.triggerSearch, v); - }); + const handler = debounce(async v => { + if (searchInput.isConnected && searchInput.value === v) { + await invoke.invokeMethodAsync(options.triggerSearch, v); + } + }) + Input.composition(searchInput, handler); } const search = el.querySelector(".dropdown-menu-search .clear-icon"); diff --git a/test/UnitTestRegion/RegionTest.cs b/test/UnitTestRegion/RegionTest.cs index f3a86db3..3b295b54 100644 --- a/test/UnitTestRegion/RegionTest.cs +++ b/test/UnitTestRegion/RegionTest.cs @@ -2,13 +2,43 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ +using Bunit; + namespace UnitTestRegion; -public class RegionTest +public class RegionTest : IDisposable { + private readonly Bunit.BunitContext _context = new(); + + public RegionTest() + { + _context.JSInterop.Mode = Bunit.JSRuntimeMode.Loose; + _context.Services.AddBootstrapBlazor(); + _context.Services.AddBootstrapBlazorRegionService(); + } + [Fact] - public void Test1() + public async Task Search_Ok() { + var cut = _context.Render(); + await cut.InvokeAsync(() => cut.Instance.TriggerSearch("深圳")); + var province = Assert.Single(cut.FindAll(".bb-region-city-title")); + Assert.Equal("广东省", province.TextContent); + + var pinyinService = _context.Services.GetRequiredService(); + var pinyin = pinyinService.GetFirstLetters("深圳市").MaxBy(i => i.Length)!; + await cut.InvokeAsync(() => cut.Instance.TriggerSearch(pinyin)); + Assert.Contains(cut.FindAll(".bb-region-city-title"), i => i.TextContent == "广东省"); + Assert.Contains("prev", cut.FindAll("li").Single(i => i.TextContent == "深圳市").ClassList); + + await cut.InvokeAsync(() => cut.Instance.TriggerSearch("")); + Assert.Equal(30, cut.FindAll(".bb-region-city-title").Count); + } + + public void Dispose() + { + _context.Dispose(); + GC.SuppressFinalize(this); } } diff --git a/test/UnitTestRegion/UnitTestRegion.csproj b/test/UnitTestRegion/UnitTestRegion.csproj index ae8644b7..367afdee 100644 --- a/test/UnitTestRegion/UnitTestRegion.csproj +++ b/test/UnitTestRegion/UnitTestRegion.csproj @@ -1,6 +1,7 @@  +