Skip to content

Commit

Permalink
feat(MultiSelect): redesign search feature (#5162)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
ArgoZhang and densen2014 authored Jan 20, 2025
1 parent da7715e commit e0f6516
Show file tree
Hide file tree
Showing 16 changed files with 350 additions and 194 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
.mul-select-item {
display: flex;
flex: 1;
align-items: center;
margin: 0 0.5rem;
}

.mul-select-item span {
flex: 1;
margin-inline-start: 0.5rem;
}
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.2.8-beta05</Version>
<Version>9.2.8</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
20 changes: 11 additions & 9 deletions src/BootstrapBlazor/Components/Select/MultiSelect.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
<BootstrapLabel required="@Required" for="@Id" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText"></BootstrapLabel>
}
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id">
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id" data-bb-scroll-behavior="@ScrollIntoViewBehaviorString">
<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">
@if(!CheckCanEdit())
{
Expand Down Expand Up @@ -49,13 +49,11 @@
}
</div>
<div class="dropdown-menu">
@if (ShowSearch)
{
<div class="search">
<input type="text" class="form-control" @bind="@SearchText" @bind:event="oninput" />
<i class="@SearchIconString"></i>
</div>
}
<div class="@SearchClassString">
<input type="text" class="form-control search-text" autocomplete="off" value="@SearchText" aria-label="search" />
<i class="@SearchIconString"></i>
<i class="@SearchLoadingIconString"></i>
</div>
@if (ShowToolbar)
{
<div class="toolbar">
Expand All @@ -68,7 +66,7 @@
@ButtonTemplate
</div>
}
@foreach (var itemGroup in GetData().GroupBy(i => i.GroupName))
@foreach (var itemGroup in Rows.GroupBy(i => i.GroupName))
{
if (!string.IsNullOrEmpty(itemGroup.Key))
{
Expand Down Expand Up @@ -104,5 +102,9 @@
</DynamicElement>
}
}
@if (Rows.Count == 0)
{
<div class="dropdown-item">@NoSearchDataText</div>
}
</div>
</div>
104 changes: 80 additions & 24 deletions src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public partial class MultiSelect<TValue>
.AddClass("d-none", SelectedItems.Count != 0)
.Build();

private string? SearchClassString => CssBuilder.Default("search")
.AddClass("show", ShowSearch)
.Build();

/// <summary>
/// 获得 SearchLoadingIcon 图标字符串
/// </summary>
private string? SearchLoadingIconString => CssBuilder.Default("icon searching-icon")
.AddClass(SearchLoadingIcon)
.Build();

/// <summary>
/// 获得/设置 绑定数据集
/// </summary>
Expand All @@ -51,13 +62,6 @@ public partial class MultiSelect<TValue>
[Parameter]
public RenderFragment<SelectedItem>? ItemTemplate { get; set; }

/// <summary>
/// 获得/设置 组件 PlaceHolder 文字 默认为 点击进行多选 ...
/// </summary>
[Parameter]
[NotNull]
public string? PlaceHolder { get; set; }

/// <summary>
/// 获得/设置 是否显示关闭按钮 默认为 true 显示
/// </summary>
Expand Down Expand Up @@ -191,10 +195,23 @@ public partial class MultiSelect<TValue>
[NotNull]
private IStringLocalizer<MultiSelect<TValue>>? Localizer { get; set; }

private List<SelectedItem>? _itemsCache;

private List<SelectedItem> Rows
{
get
{
_itemsCache ??= string.IsNullOrEmpty(SearchText) ? GetRowsByItems() : GetRowsBySearch();
return _itemsCache;
}
}

private string? PreviousValue { get; set; }

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

private string? ScrollIntoViewBehaviorString => ScrollIntoViewBehavior == ScrollIntoViewBehavior.Smooth ? null : ScrollIntoViewBehavior.ToDescriptionString();

/// <summary>
/// OnParametersSet 方法
/// </summary>
Expand All @@ -208,21 +225,22 @@ protected override void OnParametersSet()
ClearText ??= Localizer[nameof(ClearText)];
MinErrorMessage ??= Localizer[nameof(MinErrorMessage)];
MaxErrorMessage ??= Localizer[nameof(MaxErrorMessage)];
NoSearchDataText ??= Localizer[nameof(NoSearchDataText)];

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

ResetItems();
OnSearchTextChanged ??= text => Items.Where(i => i.Text.Contains(text, StringComparison.OrdinalIgnoreCase));
ResetRules();

_itemsCache = null;
// 通过 Value 对集合进行赋值
if (PreviousValue != CurrentValueAsString)
{
PreviousValue = CurrentValueAsString;
var list = CurrentValueAsString.Split(',', StringSplitOptions.RemoveEmptyEntries);
SelectedItems.Clear();
SelectedItems.AddRange(GetData().Where(item => list.Any(i => i == item.Value)));
SelectedItems.AddRange(Rows.Where(item => list.Any(i => i == item.Value)));
}
}

Expand All @@ -241,7 +259,25 @@ protected override void OnAfterRender(bool firstRender)
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ToggleRow));
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, new { ConfirmMethodCallback = nameof(ConfirmSelectedItem), SearchMethodCallback = nameof(TriggerOnSearch), TriggerEditTag = nameof(TriggerEditTag), ToggleRow = nameof(ToggleRow) });

private List<SelectedItem> GetRowsByItems()
{
var items = new List<SelectedItem>();
if (Items != null)
{
items.AddRange(Items);
}
return items;
}

private List<SelectedItem> GetRowsBySearch()
{
var items = OnSearchTextChanged?.Invoke(SearchText) ?? FilterBySearchText(GetRowsByItems());
return items.ToList();
}

private IEnumerable<SelectedItem> FilterBySearchText(IEnumerable<SelectedItem> source) => source.Where(i => i.Text.Contains(SearchText, StringComparison));

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

private bool _isToggle;

/// <summary>
/// 客户端回车回调方法
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
[JSInvokable]
public async Task ConfirmSelectedItem(int index)
{
var rows = Rows;
if (index < rows.Count)
{
await ToggleRow(rows[index].Value);
StateHasChanged();
}
}

/// <summary>
/// 切换当前选项方法
/// </summary>
Expand All @@ -270,7 +322,7 @@ public async Task ToggleRow(string val)
}
else
{
var d = GetData().FirstOrDefault(i => i.Value == val);
var d = Rows.FirstOrDefault(i => i.Value == val);
if (d != null)
{
SelectedItems.Add(d);
Expand Down Expand Up @@ -299,7 +351,7 @@ public async Task<bool> TriggerEditTag(string val)
}
else if (!string.IsNullOrEmpty(val))
{
ret = GetData().Find(i => i.Text.Equals(val, StringComparison.OrdinalIgnoreCase)) ?? new SelectedItem(val, val);
ret = Rows.Find(i => i.Text.Equals(val, StringComparison.OrdinalIgnoreCase)) ?? new SelectedItem(val, val);
}
if (ret != null)
{
Expand Down Expand Up @@ -405,7 +457,7 @@ public async Task Clear()
public async Task SelectAll()
{
SelectedItems.Clear();
SelectedItems.AddRange(GetData());
SelectedItems.AddRange(Rows);
await SetValue();
}

Expand All @@ -415,7 +467,7 @@ public async Task SelectAll()
/// <returns></returns>
public async Task InvertSelect()
{
var items = GetData().Where(item => !SelectedItems.Any(i => i.Value == item.Value)).ToList();
var items = Rows.Where(item => !SelectedItems.Any(i => i.Value == item.Value)).ToList();
SelectedItems.Clear();
SelectedItems.AddRange(items);
await SetValue();
Expand Down Expand Up @@ -460,16 +512,6 @@ private bool CheckCanEdit()
return ret;
}

private List<SelectedItem> GetData()
{
var data = Items;
if (ShowSearch && !string.IsNullOrEmpty(SearchText))
{
data = OnSearchTextChanged(SearchText);
}
return data.ToList();
}

/// <summary>
/// 客户端检查完成时调用此方法
/// </summary>
Expand Down Expand Up @@ -508,4 +550,18 @@ private void ResetItems()
}
}
}

/// <summary>
/// 客户端搜索栏回调方法
/// </summary>
/// <param name="searchText"></param>
/// <returns></returns>
[JSInvokable]
public Task TriggerOnSearch(string searchText)
{
_itemsCache = null;
SearchText = searchText;
StateHasChanged();
return Task.CompletedTask;
}
}
31 changes: 18 additions & 13 deletions src/BootstrapBlazor/Components/Select/MultiSelect.razor.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { isDisabled, getTransitionDelayDurationFromElement } from "../../modules/utility.js"
import { registerSelect, unregisterSelect } from "../../modules/base-select.js"
import Data from "../../modules/data.js"
import Popover from "../../modules/base-popover.js"
import EventHandler from "../../modules/event-handler.js"

export function init(id, invoke, method) {
export function init(id, invoke, options) {
const el = document.getElementById(id)

if (el == null) {
if (el === null) {
return
}

const { toggleRow, triggerEditTag } = options;
const search = el.querySelector(".search-text");
const itemsElement = el.querySelector('.multi-select-items');
const popover = Popover.init(el, {
itemsElement,
closeButtonSelector: '.multi-select-close'
})

const ms = {
el, invoke, method,
el, invoke, options,
itemsElement,
closeButtonSelector: '.multi-select-close',
search,
keydownEl: [search, itemsElement],
popover
}

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

if (submit) {
const ret = await invoke.invokeMethodAsync('TriggerEditTag', e.target.value);
const ret = await invoke.invokeMethodAsync(triggerEditTag, e.target.value);
if (ret) {
e.target.value = '';
}
}
});

if (!ms.popover.isPopover) {
if (!popover.isPopover) {
EventHandler.on(itemsElement, 'click', ms.closeButtonSelector, () => {
const dropdown = bootstrap.Dropdown.getInstance(popover.toggleElement)
if (dropdown && dropdown._isShown()) {
dropdown.hide()
}
})
}
ms.popover.clickToggle = e => {
popover.clickToggle = e => {
const element = e.target.closest(ms.closeButtonSelector);
if (element) {
e.stopPropagation()

invoke.invokeMethodAsync(method, element.getAttribute('data-bb-val'))
invoke.invokeMethodAsync(toggleRow, element.getAttribute('data-bb-val'))
}
}
ms.popover.isDisabled = () => {
popover.isDisabled = () => {
return isDisabled(ms.popover.toggleElement)
}

Data.set(id, ms)
Data.set(id, ms);
registerSelect(ms);
}

export function show(id) {
Expand Down Expand Up @@ -99,8 +103,9 @@ export function dispose(id) {
const ms = Data.get(id)
Data.remove(id)

if (!ms.popover.isPopover) {
const { popover } = ms;
if (!popover.isPopover) {
EventHandler.off(ms.itemsElement, 'click', ms.closeButtonSelector)
}
Popover.dispose(ms.popover)
unregisterSelect(ms);
}
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Select/Select.razor
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
}
<div class="dropdown-menu">
<div class="@SearchClassString">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="Search">
<input type="text" class="search-text form-control" autocomplete="off" value="@SearchText" aria-label="search">
<i class="@SearchIconString"></i>
<i class="@SearchLoadingIconString"></i>
</div>
Expand Down
12 changes: 0 additions & 12 deletions src/BootstrapBlazor/Components/Select/Select.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,6 @@ public partial class Select<TValue> : ISelect, ILookup
[Parameter]
public Func<string, Task>? OnInputChangedCallback { get; set; }

/// <summary>
/// 获得/设置 无搜索结果时显示文字
/// </summary>
[Parameter]
public string? NoSearchDataText { get; set; }

/// <summary>
/// 获得 PlaceHolder 属性
/// </summary>
[Parameter]
public string? PlaceHolder { get; set; }

/// <summary>
/// 获得/设置 是否可清除 默认 false
/// </summary>
Expand Down
Loading

0 comments on commit e0f6516

Please sign in to comment.