Skip to content

Commit e0f6516

Browse files
feat(MultiSelect): redesign search feature (#5162)
* refactor: 代码格式化 * doc: 更改小写 * style: 更新样式 * fix: 修复搜索逻辑 * chore: bump version 9.2.8-beta04 Co-Authored-By: Alexander Shakhov <[email protected]> * chore: bump version 9.2.8-beta06 * chore: 重构代码 * refactor: 移动代码到独立类中 * refactor: 移动代码 * refactor: 更新 base-select 代码复用 * refactor: 复用客户端脚本 * feat: 增加 NoSearchDataText 功能 * refactor: 增加可编辑状态下按键支持 * refactor: 复用 * chore: bump version 9.2.8 * refactor: 重构搜索逻辑 * refactor: 增加内部缓存提高性能 * refactor: 增加滚动行为参数 * refactor: 重构代码 * test: 补充单元测试 * test: 更新单元测试 --------- Co-Authored-By: Alex chow <[email protected]>
1 parent da7715e commit e0f6516

File tree

16 files changed

+350
-194
lines changed

16 files changed

+350
-194
lines changed
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
.mul-select-item {
22
display: flex;
3-
flex: 1;
4-
align-items: center;
5-
margin: 0 0.5rem;
63
}
74

85
.mul-select-item span {
9-
flex: 1;
106
margin-inline-start: 0.5rem;
117
}

src/BootstrapBlazor/BootstrapBlazor.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.2.8-beta05</Version>
4+
<Version>9.2.8</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/Select/MultiSelect.razor

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
{
88
<BootstrapLabel required="@Required" for="@Id" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText"></BootstrapLabel>
99
}
10-
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id">
10+
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id" data-bb-scroll-behavior="@ScrollIntoViewBehaviorString">
1111
<div class="@ToggleClassString" data-bs-toggle="@ToggleString" data-bs-placement="@PlacementString" data-bs-offset="@OffsetString" data-bs-auto-close="outside" data-bs-custom-class="@CustomClassString" tabindex="0">
1212
@if(!CheckCanEdit())
1313
{
@@ -49,13 +49,11 @@
4949
}
5050
</div>
5151
<div class="dropdown-menu">
52-
@if (ShowSearch)
53-
{
54-
<div class="search">
55-
<input type="text" class="form-control" @bind="@SearchText" @bind:event="oninput" />
56-
<i class="@SearchIconString"></i>
57-
</div>
58-
}
52+
<div class="@SearchClassString">
53+
<input type="text" class="form-control search-text" autocomplete="off" value="@SearchText" aria-label="search" />
54+
<i class="@SearchIconString"></i>
55+
<i class="@SearchLoadingIconString"></i>
56+
</div>
5957
@if (ShowToolbar)
6058
{
6159
<div class="toolbar">
@@ -68,7 +66,7 @@
6866
@ButtonTemplate
6967
</div>
7068
}
71-
@foreach (var itemGroup in GetData().GroupBy(i => i.GroupName))
69+
@foreach (var itemGroup in Rows.GroupBy(i => i.GroupName))
7270
{
7371
if (!string.IsNullOrEmpty(itemGroup.Key))
7472
{
@@ -104,5 +102,9 @@
104102
</DynamicElement>
105103
}
106104
}
105+
@if (Rows.Count == 0)
106+
{
107+
<div class="dropdown-item">@NoSearchDataText</div>
108+
}
107109
</div>
108110
</div>

src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ public partial class MultiSelect<TValue>
3838
.AddClass("d-none", SelectedItems.Count != 0)
3939
.Build();
4040

41+
private string? SearchClassString => CssBuilder.Default("search")
42+
.AddClass("show", ShowSearch)
43+
.Build();
44+
45+
/// <summary>
46+
/// 获得 SearchLoadingIcon 图标字符串
47+
/// </summary>
48+
private string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon")
49+
.AddClass(SearchLoadingIcon)
50+
.Build();
51+
4152
/// <summary>
4253
/// 获得/设置 绑定数据集
4354
/// </summary>
@@ -51,13 +62,6 @@ public partial class MultiSelect<TValue>
5162
[Parameter]
5263
public RenderFragment<SelectedItem>? ItemTemplate { get; set; }
5364

54-
/// <summary>
55-
/// 获得/设置 组件 PlaceHolder 文字 默认为 点击进行多选 ...
56-
/// </summary>
57-
[Parameter]
58-
[NotNull]
59-
public string? PlaceHolder { get; set; }
60-
6165
/// <summary>
6266
/// 获得/设置 是否显示关闭按钮 默认为 true 显示
6367
/// </summary>
@@ -191,10 +195,23 @@ public partial class MultiSelect<TValue>
191195
[NotNull]
192196
private IStringLocalizer<MultiSelect<TValue>>? Localizer { get; set; }
193197

198+
private List<SelectedItem>? _itemsCache;
199+
200+
private List<SelectedItem> Rows
201+
{
202+
get
203+
{
204+
_itemsCache ??= string.IsNullOrEmpty(SearchText) ? GetRowsByItems() : GetRowsBySearch();
205+
return _itemsCache;
206+
}
207+
}
208+
194209
private string? PreviousValue { get; set; }
195210

196211
private string? PlaceholderString => SelectedItems.Count == 0 ? PlaceHolder : null;
197212

213+
private string? ScrollIntoViewBehaviorString => ScrollIntoViewBehavior == ScrollIntoViewBehavior.Smooth ? null : ScrollIntoViewBehavior.ToDescriptionString();
214+
198215
/// <summary>
199216
/// OnParametersSet 方法
200217
/// </summary>
@@ -208,21 +225,22 @@ protected override void OnParametersSet()
208225
ClearText ??= Localizer[nameof(ClearText)];
209226
MinErrorMessage ??= Localizer[nameof(MinErrorMessage)];
210227
MaxErrorMessage ??= Localizer[nameof(MaxErrorMessage)];
228+
NoSearchDataText ??= Localizer[nameof(NoSearchDataText)];
211229

212230
DropdownIcon ??= IconTheme.GetIconByKey(ComponentIcons.MultiSelectDropdownIcon);
213231
ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.MultiSelectClearIcon);
214232

215233
ResetItems();
216-
OnSearchTextChanged ??= text => Items.Where(i => i.Text.Contains(text, StringComparison.OrdinalIgnoreCase));
217234
ResetRules();
218235

236+
_itemsCache = null;
219237
// 通过 Value 对集合进行赋值
220238
if (PreviousValue != CurrentValueAsString)
221239
{
222240
PreviousValue = CurrentValueAsString;
223241
var list = CurrentValueAsString.Split(',', StringSplitOptions.RemoveEmptyEntries);
224242
SelectedItems.Clear();
225-
SelectedItems.AddRange(GetData().Where(item => list.Any(i => i == item.Value)));
243+
SelectedItems.AddRange(Rows.Where(item => list.Any(i => i == item.Value)));
226244
}
227245
}
228246

@@ -241,7 +259,25 @@ protected override void OnAfterRender(bool firstRender)
241259
/// <inheritdoc/>
242260
/// </summary>
243261
/// <returns></returns>
244-
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ToggleRow));
262+
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { ConfirmMethodCallback = nameof(ConfirmSelectedItem), SearchMethodCallback = nameof(TriggerOnSearch), TriggerEditTag = nameof(TriggerEditTag), ToggleRow = nameof(ToggleRow) });
263+
264+
private List<SelectedItem> GetRowsByItems()
265+
{
266+
var items = new List<SelectedItem>();
267+
if (Items != null)
268+
{
269+
items.AddRange(Items);
270+
}
271+
return items;
272+
}
273+
274+
private List<SelectedItem> GetRowsBySearch()
275+
{
276+
var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems());
277+
return items.ToList();
278+
}
279+
280+
private IEnumerable<SelectedItem> FilterBySearchText(IEnumerable<SelectedItem> source) => source.Where(i => i.Text.Contains(SearchText, StringComparison));
245281

246282
/// <summary>
247283
/// FormatValueAsString 方法
@@ -254,6 +290,22 @@ protected override void OnAfterRender(bool firstRender)
254290

255291
private bool _isToggle;
256292

293+
/// <summary>
294+
/// 客户端回车回调方法
295+
/// </summary>
296+
/// <param name="index"></param>
297+
/// <returns></returns>
298+
[JSInvokable]
299+
public async Task ConfirmSelectedItem(int index)
300+
{
301+
var rows = Rows;
302+
if (index < rows.Count)
303+
{
304+
await ToggleRow(rows[index].Value);
305+
StateHasChanged();
306+
}
307+
}
308+
257309
/// <summary>
258310
/// 切换当前选项方法
259311
/// </summary>
@@ -270,7 +322,7 @@ public async Task ToggleRow(string val)
270322
}
271323
else
272324
{
273-
var d = GetData().FirstOrDefault(i => i.Value == val);
325+
var d = Rows.FirstOrDefault(i => i.Value == val);
274326
if (d != null)
275327
{
276328
SelectedItems.Add(d);
@@ -299,7 +351,7 @@ public async Task<bool> TriggerEditTag(string val)
299351
}
300352
else if (!string.IsNullOrEmpty(val))
301353
{
302-
ret = GetData().Find(i => i.Text.Equals(val, StringComparison.OrdinalIgnoreCase)) ?? new SelectedItem(val, val);
354+
ret = Rows.Find(i => i.Text.Equals(val, StringComparison.OrdinalIgnoreCase)) ?? new SelectedItem(val, val);
303355
}
304356
if (ret != null)
305357
{
@@ -405,7 +457,7 @@ public async Task Clear()
405457
public async Task SelectAll()
406458
{
407459
SelectedItems.Clear();
408-
SelectedItems.AddRange(GetData());
460+
SelectedItems.AddRange(Rows);
409461
await SetValue();
410462
}
411463

@@ -415,7 +467,7 @@ public async Task SelectAll()
415467
/// <returns></returns>
416468
public async Task InvertSelect()
417469
{
418-
var items = GetData().Where(item => !SelectedItems.Any(i => i.Value == item.Value)).ToList();
470+
var items = Rows.Where(item => !SelectedItems.Any(i => i.Value == item.Value)).ToList();
419471
SelectedItems.Clear();
420472
SelectedItems.AddRange(items);
421473
await SetValue();
@@ -460,16 +512,6 @@ private bool CheckCanEdit()
460512
return ret;
461513
}
462514

463-
private List<SelectedItem> GetData()
464-
{
465-
var data = Items;
466-
if (ShowSearch && !string.IsNullOrEmpty(SearchText))
467-
{
468-
data = OnSearchTextChanged(SearchText);
469-
}
470-
return data.ToList();
471-
}
472-
473515
/// <summary>
474516
/// 客户端检查完成时调用此方法
475517
/// </summary>
@@ -508,4 +550,18 @@ private void ResetItems()
508550
}
509551
}
510552
}
553+
554+
/// <summary>
555+
/// 客户端搜索栏回调方法
556+
/// </summary>
557+
/// <param name="searchText"></param>
558+
/// <returns></returns>
559+
[JSInvokable]
560+
public Task TriggerOnSearch(string searchText)
561+
{
562+
_itemsCache = null;
563+
SearchText = searchText;
564+
StateHasChanged();
565+
return Task.CompletedTask;
566+
}
511567
}

src/BootstrapBlazor/Components/Select/MultiSelect.razor.js

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
import { isDisabled, getTransitionDelayDurationFromElement } from "../../modules/utility.js"
2+
import { registerSelect, unregisterSelect } from "../../modules/base-select.js"
23
import Data from "../../modules/data.js"
34
import Popover from "../../modules/base-popover.js"
45
import EventHandler from "../../modules/event-handler.js"
56

6-
export function init(id, invoke, method) {
7+
export function init(id, invoke, options) {
78
const el = document.getElementById(id)
8-
9-
if (el == null) {
9+
if (el === null) {
1010
return
1111
}
1212

13+
const { toggleRow, triggerEditTag } = options;
14+
const search = el.querySelector(".search-text");
1315
const itemsElement = el.querySelector('.multi-select-items');
1416
const popover = Popover.init(el, {
1517
itemsElement,
1618
closeButtonSelector: '.multi-select-close'
1719
})
18-
1920
const ms = {
20-
el, invoke, method,
21+
el, invoke, options,
2122
itemsElement,
2223
closeButtonSelector: '.multi-select-close',
24+
search,
25+
keydownEl: [search, itemsElement],
2326
popover
2427
}
2528

@@ -43,34 +46,35 @@ export function init(id, invoke, method) {
4346
}
4447

4548
if (submit) {
46-
const ret = await invoke.invokeMethodAsync('TriggerEditTag', e.target.value);
49+
const ret = await invoke.invokeMethodAsync(triggerEditTag, e.target.value);
4750
if (ret) {
4851
e.target.value = '';
4952
}
5053
}
5154
});
5255

53-
if (!ms.popover.isPopover) {
56+
if (!popover.isPopover) {
5457
EventHandler.on(itemsElement, 'click', ms.closeButtonSelector, () => {
5558
const dropdown = bootstrap.Dropdown.getInstance(popover.toggleElement)
5659
if (dropdown && dropdown._isShown()) {
5760
dropdown.hide()
5861
}
5962
})
6063
}
61-
ms.popover.clickToggle = e => {
64+
popover.clickToggle = e => {
6265
const element = e.target.closest(ms.closeButtonSelector);
6366
if (element) {
6467
e.stopPropagation()
6568

66-
invoke.invokeMethodAsync(method, element.getAttribute('data-bb-val'))
69+
invoke.invokeMethodAsync(toggleRow, element.getAttribute('data-bb-val'))
6770
}
6871
}
69-
ms.popover.isDisabled = () => {
72+
popover.isDisabled = () => {
7073
return isDisabled(ms.popover.toggleElement)
7174
}
7275

73-
Data.set(id, ms)
76+
Data.set(id, ms);
77+
registerSelect(ms);
7478
}
7579

7680
export function show(id) {
@@ -99,8 +103,9 @@ export function dispose(id) {
99103
const ms = Data.get(id)
100104
Data.remove(id)
101105

102-
if (!ms.popover.isPopover) {
106+
const { popover } = ms;
107+
if (!popover.isPopover) {
103108
EventHandler.off(ms.itemsElement, 'click', ms.closeButtonSelector)
104109
}
105-
Popover.dispose(ms.popover)
110+
unregisterSelect(ms);
106111
}

src/BootstrapBlazor/Components/Select/Select.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
}
3333
<div class="dropdown-menu">
3434
<div class="@SearchClassString">
35-
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
35+
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="search">
3636
<i class="@SearchIconString"></i>
3737
<i class="@SearchLoadingIconString"></i>
3838
</div>

src/BootstrapBlazor/Components/Select/Select.razor.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,6 @@ public partial class Select<TValue> : ISelect, ILookup
102102
[Parameter]
103103
public Func<string, Task>? OnInputChangedCallback { get; set; }
104104

105-
/// <summary>
106-
/// 获得/设置 无搜索结果时显示文字
107-
/// </summary>
108-
[Parameter]
109-
public string? NoSearchDataText { get; set; }
110-
111-
/// <summary>
112-
/// 获得 PlaceHolder 属性
113-
/// </summary>
114-
[Parameter]
115-
public string? PlaceHolder { get; set; }
116-
117105
/// <summary>
118106
/// 获得/设置 是否可清除 默认 false
119107
/// </summary>

0 commit comments

Comments
 (0)