Skip to content

Commit a200c0f

Browse files
ArgoZhangice6
andauthored
feat(MultiSelect): add IsEditable parameter (#5086)
* feat: 增加 IsEditable 参数 * doc: 更新示例 * feat: 增加 IsEditable 参数 * refactor: 重构代码 * test: 增加单元测试 * refactor: 更新 Placeholder 逻辑 * doc: 增加本地化 --------- Co-Authored-By: ice6 <[email protected]>
1 parent 575f399 commit a200c0f

File tree

10 files changed

+225
-9
lines changed

10 files changed

+225
-9
lines changed

src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,17 @@
242242
</div>
243243
</DemoBlock>
244244

245+
<DemoBlock Title="@Localizer["MultiSelectIsEditableTitle"]" Introduction="@Localizer["MultiSelectIsEditableIntro"]" Name="IsEditable">
246+
<section ignore>
247+
@((MarkupString)Localizer["MultiSelectIsEditableDescription"].Value)
248+
</section>
249+
<div class="row g-3">
250+
<div class="col-12">
251+
<MultiSelect TValue="string" Items="@EditableItems" IsEditable="true" Max="2" EditSubmitKey="EditSubmitKey.Space" />
252+
</div>
253+
</div>
254+
</DemoBlock>
255+
245256
<AttributeTable Items="@GetAttributes()" />
246257

247258
<EventTable Items="@GetEvents()" />

src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public partial class MultiSelects
1919

2020
private Foo Foo { get; set; } = new Foo();
2121

22+
[NotNull]
23+
private List<SelectedItem>? EditableItems { get; set; }
24+
2225
[NotNull]
2326
private List<SelectedItem>? Items1 { get; set; }
2427

@@ -129,6 +132,7 @@ protected override void OnInitialized()
129132
Items7 = GenerateItems();
130133
Items8 = GenerateItems();
131134
TemplateItems = GenerateItems();
135+
EditableItems = GenerateItems();
132136

133137
// 初始化数据
134138
DataSource =

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2990,7 +2990,10 @@
29902990
"MultiSelectMaxMinMin": "Select at least two options",
29912991
"MultiSelectSearchLog": "Search for text",
29922992
"MultiSelectVeryLongTextDisplayText": "Extra long text",
2993-
"MultiSelectOptionChangeLog": "Select the collection of items"
2993+
"MultiSelectOptionChangeLog": "Select the collection of items",
2994+
"MultiSelectIsEditableTitle": "Editable",
2995+
"MultiSelectIsEditableIntro": "Make the component editable by setting the <code>IsEditable</code> parameter",
2996+
"MultiSelectIsEditableDescription": "By setting the <code>EditSubmitKey</code> parameter, you can specify whether to submit via <kbd>Enter</kbd> or <kbd>Space</kbd>"
29942997
},
29952998
"BootstrapBlazor.Server.Components.Samples.Radios": {
29962999
"RadiosTitle": "Radio",

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2990,7 +2990,10 @@
29902990
"MultiSelectMaxMinMin": "最少选择两个选项",
29912991
"MultiSelectSearchLog": "搜索文字",
29922992
"MultiSelectVeryLongTextDisplayText": "超长文字",
2993-
"MultiSelectOptionChangeLog": "选中项集合"
2993+
"MultiSelectOptionChangeLog": "选中项集合",
2994+
"MultiSelectIsEditableTitle": "可编辑",
2995+
"MultiSelectIsEditableIntro": "通过设置 <code>IsEditable</code> 参数,使组件可编辑",
2996+
"MultiSelectIsEditableDescription": "通过设置 <code>EditSubmitKey</code> 参数可以指定通过 <kbd>Enter</kbd> 还是 <kbd>Space</kbd> 进行提交"
29942997
},
29952998
"BootstrapBlazor.Server.Components.Samples.Radios": {
29962999
"RadiosTitle": "Radio 单选框",

src/BootstrapBlazor/Components/Select/MultiSelect.razor

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
}
1010
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id">
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">
12-
<div class="@PlaceHolderClassString">@PlaceHolder</div>
12+
@if(!CheckCanEdit())
13+
{
14+
<div class="@PlaceHolderClassString">@PlaceHolder</div>
15+
}
1316
<div class="multi-select-items">
1417
@if (DisplayTemplate != null)
1518
{
@@ -35,6 +38,10 @@
3538
}
3639
}
3740
}
41+
@if (CheckCanEdit())
42+
{
43+
<input type="text" class="multi-select-input" autocomplete="off" tabindex="0" placeholder="@PlaceholderString" data-bb-trigger-key="@EditSubmitKeyString" />
44+
}
3845
</div>
3946
@if (!IsSingleLine)
4047
{

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public partial class MultiSelect<TValue>
1818
private static string? ClassString => CssBuilder.Default("select dropdown multi-select")
1919
.Build();
2020

21+
private string? EditSubmitKeyString => EditSubmitKey == EditSubmitKey.Space ? EditSubmitKey.ToDescriptionString() : null;
22+
2123
private string? ToggleClassString => CssBuilder.Default("dropdown-toggle scroll")
2224
.AddClass($"border-{Color.ToDescriptionString()}", Color != Color.None && !IsDisabled)
2325
.AddClass("is-fixed", IsFixedHeight)
@@ -86,6 +88,26 @@ public partial class MultiSelect<TValue>
8688
[Parameter]
8789
public bool IsSingleLine { get; set; }
8890

91+
/// <summary>
92+
/// 获得/设置 是否可编辑 默认 false
93+
/// </summary>
94+
[Parameter]
95+
public bool IsEditable { get; set; }
96+
97+
/// <summary>
98+
/// 获得/设置 编辑模式下输入选项更新后回调方法 默认 null
99+
/// <para>返回 <see cref="SelectedItem"/> 实例时输入选项生效,返回 null 时选项不生效进行舍弃操作,建议在回调方法中自行提示</para>
100+
/// </summary>
101+
/// <remarks>设置 <see cref="IsEditable"/> 后生效</remarks>
102+
[Parameter]
103+
public Func<string, Task<SelectedItem>>? OnEditCallback { get; set; }
104+
105+
/// <summary>
106+
/// 获得/设置 编辑提交按键 默认 Enter
107+
/// </summary>
108+
[Parameter]
109+
public EditSubmitKey EditSubmitKey { get; set; }
110+
89111
/// <summary>
90112
/// 获得/设置 扩展按钮模板
91113
/// </summary>
@@ -171,6 +193,8 @@ public partial class MultiSelect<TValue>
171193

172194
private string? PreviousValue { get; set; }
173195

196+
private string? PlaceholderString => SelectedItems.Count == 0 ? PlaceHolder : null;
197+
174198
/// <summary>
175199
/// OnParametersSet 方法
176200
/// </summary>
@@ -259,6 +283,37 @@ public async Task ToggleRow(string val)
259283
}
260284
}
261285

286+
/// <summary>
287+
/// 客户端编辑提交数据回调方法
288+
/// </summary>
289+
/// <param name="val"></param>
290+
/// <returns></returns>
291+
[JSInvokable]
292+
public async Task<bool> TriggerEditTag(string val)
293+
{
294+
SelectedItem? ret = null;
295+
val = val.Trim();
296+
if (OnEditCallback != null)
297+
{
298+
ret = await OnEditCallback.Invoke(val);
299+
}
300+
else if (!string.IsNullOrEmpty(val))
301+
{
302+
ret = GetData().Find(i => i.Text.Equals(val, StringComparison.OrdinalIgnoreCase)) ?? new SelectedItem(val, val);
303+
}
304+
if (ret != null)
305+
{
306+
if (SelectedItems.Find(i => i.Text.Equals(val, StringComparison.OrdinalIgnoreCase)) == null)
307+
{
308+
SelectedItems.Add(ret);
309+
}
310+
// 更新选中值
311+
_isToggle = true;
312+
await SetValue();
313+
}
314+
return ret != null;
315+
}
316+
262317
private string? GetValueString(SelectedItem item) => IsPopover ? item.Value : null;
263318

264319
private int _min;
@@ -390,14 +445,29 @@ private bool CheckCanSelect(SelectedItem item)
390445
return !ret;
391446
}
392447

393-
private IEnumerable<SelectedItem> GetData()
448+
private bool CheckCanEdit()
449+
{
450+
var ret = IsEditable;
451+
if (ret == false)
452+
{
453+
return false;
454+
}
455+
456+
if (Max > 0)
457+
{
458+
ret = SelectedItems.Count < Max;
459+
}
460+
return ret;
461+
}
462+
463+
private List<SelectedItem> GetData()
394464
{
395465
var data = Items;
396466
if (ShowSearch && !string.IsNullOrEmpty(SearchText))
397467
{
398468
data = OnSearchTextChanged(SearchText);
399469
}
400-
return data;
470+
return data.ToList();
401471
}
402472

403473
/// <summary>

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,48 @@ export function init(id, invoke, method) {
1010
return
1111
}
1212

13+
const itemsElement = el.querySelector('.multi-select-items');
1314
const popover = Popover.init(el, {
14-
itemsElement: el.querySelector('.multi-select-items'),
15+
itemsElement,
1516
closeButtonSelector: '.multi-select-close'
1617
})
1718

1819
const ms = {
1920
el, invoke, method,
20-
itemsElement: el.querySelector('.multi-select-items'),
21+
itemsElement,
2122
closeButtonSelector: '.multi-select-close',
2223
popover
2324
}
2425

26+
EventHandler.on(itemsElement, 'click', '.multi-select-input', e => {
27+
const handler = setTimeout(() => {
28+
clearTimeout(handler);
29+
e.target.focus();
30+
}, 50);
31+
});
32+
33+
EventHandler.on(itemsElement, 'keyup', '.multi-select-input', async e => {
34+
const triggerSpace = e.target.getAttribute('data-bb-trigger-key') === 'space';
35+
let submit = false;
36+
if (triggerSpace) {
37+
if (e.code === 'Space') {
38+
submit = true;
39+
}
40+
}
41+
else if (e.code === 'Enter' || e.code === 'NumPadEnter') {
42+
submit = true;
43+
}
44+
45+
if (submit) {
46+
const ret = await invoke.invokeMethodAsync('TriggerEditTag', e.target.value);
47+
if (ret) {
48+
e.target.value = '';
49+
}
50+
}
51+
});
52+
2553
if (!ms.popover.isPopover) {
26-
EventHandler.on(ms.itemsElement, 'click', ms.closeButtonSelector, () => {
54+
EventHandler.on(itemsElement, 'click', ms.closeButtonSelector, () => {
2755
const dropdown = bootstrap.Dropdown.getInstance(popover.toggleElement)
2856
if (dropdown && dropdown._isShown()) {
2957
dropdown.hide()

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
--bb-multi-select-button-hover-bg-color: rgba(var(--bs-body-color-rgb), .3);
66
--bb-multi-select-item-margin-x: 3px;
77
--bb-multi-select-item-margin-y: 3px;
8-
--bb-multi-select-item-padding: 2px 6px;
98
--bb-multi-select-item-max-width: 130px;
9+
--bb-multi-select-item-padding: 2px 6px;
1010
width: 100%;
1111
position: relative;
1212

@@ -81,6 +81,10 @@
8181
.multi-select-item-group {
8282
display: inline-flex;
8383
position: relative;
84+
85+
+ .multi-select-input {
86+
padding: 3px 6px;
87+
}
8488
}
8589

8690
.multi-select-close {
@@ -101,6 +105,18 @@
101105
line-height: var(--bb-height);
102106
position: absolute;
103107
}
108+
109+
.multi-select-input {
110+
border: none;
111+
outline: none;
112+
appearance: none;
113+
padding: 3px 12px;
114+
background-color: transparent;
115+
flex: 1;
116+
width: 1%;
117+
min-width: 1rem;
118+
margin-block-end: var(--bb-multi-select-item-margin-y);
119+
}
104120
}
105121

106122
.dropdown-menu .toolbar {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
using System.ComponentModel;
7+
8+
namespace BootstrapBlazor.Components;
9+
10+
/// <summary>
11+
/// EditSubmitKeys 枚举
12+
/// </summary>
13+
public enum EditSubmitKey
14+
{
15+
/// <summary>
16+
/// Enter 键
17+
/// </summary>
18+
[Description("enter")]
19+
Enter,
20+
21+
/// <summary>
22+
/// Space 键
23+
/// </summary>
24+
[Description("space")]
25+
Space
26+
}

test/UnitTest/Components/MultiSelectTest.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,54 @@ public void MinMax_Ok()
3333
});
3434
}
3535

36+
[Fact]
37+
public async Task IsEditable_Ok()
38+
{
39+
var cut = Context.RenderComponent<MultiSelect<string>>(pb =>
40+
{
41+
pb.Add(a => a.Max, 2);
42+
pb.Add(a => a.Items, new List<SelectedItem>
43+
{
44+
new("1", "Test 1"),
45+
new("2", "Test 2"),
46+
new("3", "Test 3"),
47+
new("4", "Test 4"),
48+
});
49+
});
50+
Assert.DoesNotContain("class=\"multi-select-input\"", cut.Markup);
51+
52+
cut.SetParametersAndRender(pb =>
53+
{
54+
pb.Add(a => a.IsEditable, true);
55+
});
56+
Assert.Contains("class=\"multi-select-input\"", cut.Markup);
57+
Assert.DoesNotContain("data-bb-trigger-key", cut.Markup);
58+
59+
cut.SetParametersAndRender(pb =>
60+
{
61+
pb.Add(a => a.EditSubmitKey, EditSubmitKey.Space);
62+
});
63+
Assert.Contains("data-bb-trigger-key=\"space\"", cut.Markup);
64+
65+
await cut.InvokeAsync(() => cut.Instance.TriggerEditTag("123"));
66+
Assert.Equal("123", cut.Instance.Value);
67+
68+
await cut.InvokeAsync(() => cut.Instance.TriggerEditTag("123"));
69+
Assert.Equal("123", cut.Instance.Value);
70+
71+
cut.SetParametersAndRender(pb =>
72+
{
73+
pb.Add(a => a.OnEditCallback, async v =>
74+
{
75+
await Task.Delay(10);
76+
return new SelectedItem("test", "456");
77+
});
78+
});
79+
await cut.InvokeAsync(() => cut.Instance.TriggerEditTag("456"));
80+
Assert.Equal("123,test", cut.Instance.Value);
81+
Assert.DoesNotContain("class=\"multi-select-input\"", cut.Markup);
82+
}
83+
3684
[Fact]
3785
public void IsFixedHeight_Ok()
3886
{

0 commit comments

Comments
 (0)