Skip to content

Commit 24a709d

Browse files
authored
🆕 feat(component): add an expandable autocomplete component (#779)
1 parent 7b90af6 commit 24a709d

File tree

9 files changed

+278
-39
lines changed

9 files changed

+278
-39
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
@namespace Masa.Stack.Components
2+
@using System.Linq.Expressions
3+
@using Masa.Blazor.Presets
4+
@using Microsoft.AspNetCore.Components.Web.Virtualization
5+
@typeparam TItem
6+
@typeparam TItemValue
7+
@inject I18n I18n
8+
9+
<MAutocomplete Value="@Value"
10+
ValueChanged="@ValueChanged"
11+
ValueExpression="@ValueExpression"
12+
Items="@Items"
13+
ItemText="@ItemText"
14+
ItemValue="@ItemValue"
15+
ItemDisabled="@ItemDisabled"
16+
Dense="@Dense"
17+
Outlined="@Outlined"
18+
Filled="@Filled"
19+
Solo="@Solo"
20+
SoloInverted="@SoloInverted"
21+
HideDetails="@HideDetails"
22+
Chips="@Chips"
23+
SmallChips="@SmallChips"
24+
Label="@Label"
25+
Class="@(("s-expandable-autocomplete " + Class).Trim())"
26+
Style="@Style"
27+
Multiple>
28+
<AppendOuterContent>
29+
<MButton IconName="mdi-arrow-expand"
30+
Class="my-auto"
31+
Small="@Dense"
32+
OnClickStopPropagation
33+
OnClick="@OnExpand"/>
34+
</AppendOuterContent>
35+
</MAutocomplete>
36+
37+
<PDrawer @bind-Value="expanded" Title="@I18n.T("HasSelected", Value.Count, _items.Count)">
38+
<MTextField @bind-Value="Search" Label="@I18n.T("Search")" Dense
39+
HideDetails="true" Class="mb-2" Outlined Clearable></MTextField>
40+
<MSimpleTable FixedHeader Height="@("calc(100vh - 159px)")">
41+
<thead>
42+
<tr>
43+
<th class="text-center" style="width: 40px">
44+
<MSimpleCheckbox Indeterminate="@Indeterminate"
45+
Value="@IsAllSelected"
46+
ValueChanged="SelectAll"
47+
Color="primary"/>
48+
</th>
49+
<th class="text-left">
50+
@I18n.T("Label")
51+
</th>
52+
</tr>
53+
</thead>
54+
<tbody>
55+
<Virtualize Items="@_filteredItems" ItemSize="48">
56+
<ItemContent>
57+
<tr>
58+
<td class="text-center">
59+
<MSimpleCheckbox Value="@Value.Contains(context.Value)"
60+
ValueChanged="@((value) => Select(context.Value, value))"
61+
Color="primary"/>
62+
</td>
63+
<td>@context.Label</td>
64+
</tr>
65+
</ItemContent>
66+
</Virtualize>
67+
</tbody>
68+
</MSimpleTable>
69+
</PDrawer>
70+
71+
@code {
72+
73+
#region Parameters
74+
75+
[Parameter] public List<TItemValue?> Value { get; set; } = [];
76+
77+
[Parameter] public EventCallback<List<TItemValue?>> ValueChanged { get; set; }
78+
79+
[Parameter] public Expression<Func<List<TItemValue?>>>? ValueExpression { get; set; }
80+
81+
[EditorRequired]
82+
[Parameter] public IList<TItem> Items { get; set; } = [];
83+
84+
[Parameter]
85+
public Func<TItem, bool>? ItemDisabled { get; set; }
86+
87+
[Parameter]
88+
[EditorRequired]
89+
public Func<TItem, string>? ItemText { get; set; }
90+
91+
[Parameter]
92+
[EditorRequired]
93+
public Func<TItem, TItemValue?>? ItemValue { get; set; }
94+
95+
[Parameter] public bool Dense { get; set; }
96+
97+
[Parameter] public bool Outlined { get; set; }
98+
99+
[Parameter] public bool Chips { get; set; }
100+
101+
[Parameter] public bool SmallChips { get; set; }
102+
103+
[Parameter] public string? Label { get; set; }
104+
105+
[Parameter] public string? Class { get; set; }
106+
107+
[Parameter] public string? Style { get; set; }
108+
109+
[Parameter] public bool Filled { get; set; }
110+
111+
[Parameter] public bool Solo { get; set; }
112+
113+
[Parameter] public bool SoloInverted { get; set; }
114+
115+
[Parameter] public StringBoolean? HideDetails { get; set; }
116+
117+
#endregion
118+
119+
private string? _search;
120+
121+
private string? Search
122+
{
123+
get => _search;
124+
set
125+
{
126+
_search = value;
127+
UpdateFilterItems();
128+
}
129+
}
130+
131+
private IList<TItem> _previousItems = [];
132+
private HashSet<InternalItem> _items = [];
133+
134+
protected override void OnParametersSet()
135+
{
136+
base.OnParametersSet();
137+
138+
if (!Equals(_previousItems, Items) || _items.Count == 0)
139+
{
140+
_previousItems = Items;
141+
if (ItemValue is null)
142+
{
143+
return;
144+
}
145+
146+
_items = Items.Select(item => new InternalItem(ItemText?.Invoke(item), ItemValue.Invoke(item)))
147+
.ToHashSet();
148+
UpdateFilterItems();
149+
}
150+
}
151+
152+
private bool expanded;
153+
154+
private bool IsAllSelected => _filteredValues.Count > 0 && _filteredValues.All(v => Value.Contains(v));
155+
private bool Indeterminate => _filteredValues.Any(v => Value.Contains(v)) && !IsAllSelected;
156+
157+
private HashSet<InternalItem> _filteredItems = [];
158+
private HashSet<TItemValue?> _filteredValues = [];
159+
160+
private record InternalItem(string? Label, TItemValue? Value);
161+
162+
private void UpdateFilterItems()
163+
{
164+
_filteredItems = string.IsNullOrWhiteSpace(Search)
165+
? _items.ToHashSet()
166+
: _items.Where(item => item.Label?.Contains(Search, StringComparison.OrdinalIgnoreCase) == true).ToHashSet();
167+
168+
_filteredValues = _filteredItems.Select(item => item.Value).ToHashSet();
169+
}
170+
171+
private void OnExpand()
172+
{
173+
expanded = !expanded;
174+
}
175+
176+
private async Task Select(TItemValue? item, bool value)
177+
{
178+
if (value)
179+
{
180+
if (!Value.Contains(item))
181+
{
182+
await ValueChanged.InvokeAsync([..Value, item]);
183+
}
184+
}
185+
else
186+
{
187+
List<TItemValue?> selected = [..Value];
188+
selected.Remove(item);
189+
await ValueChanged.InvokeAsync(selected);
190+
}
191+
}
192+
193+
private async Task SelectAll(bool value)
194+
{
195+
if (_filteredValues.Count == 0)
196+
{
197+
return;
198+
}
199+
200+
if (value)
201+
{
202+
HashSet<TItemValue?> selected = [..Value, .._filteredValues];
203+
204+
await ValueChanged.InvokeAsync(selected.ToList());
205+
}
206+
else
207+
{
208+
List<TItemValue?> selected = [..Value];
209+
foreach (var filteredValue in _filteredValues)
210+
{
211+
selected.Remove(filteredValue);
212+
}
213+
214+
await ValueChanged.InvokeAsync(selected);
215+
}
216+
}
217+
218+
}

src/Masa.Stack.Components/Extensions/ServiceCollectionExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,13 @@ private static void AddMasaStackComponentsService(IServiceCollection services, M
103103
options.ConfigureTheme(theme =>
104104
{
105105
theme.Themes.Light.Primary = "#4318FF";
106-
theme.Themes.Light.Accent = "#4318FF";
107-
theme.Themes.Light.Error = "#FF5252";
106+
theme.Themes.Light.Accent = "#006c4f";
107+
theme.Themes.Light.Error = "#ba1a1a";
108108
theme.Themes.Light.Success = "#00B42A";
109-
theme.Themes.Light.Warning = "#FF7D00";
109+
theme.Themes.Light.Warning = "#FF5252";
110110
theme.Themes.Light.Info = "#37A7FF";
111111
theme.Themes.Light.Surface = "#F0F3FA";
112+
// theme.Themes.Light.UserDefined["reminder"] = "#FF7D00";
112113
});
113114
options.Defaults = new Dictionary<string, IDictionary<string, object?>?>
114115
{

src/Masa.Stack.Components/Locales/en-US.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,7 @@
133133
"404Tips": "The requested URL was not found.",
134134
"BackToHome": "Back to home",
135135
"NavigationLayerLabel": "Menu display",
136-
"Layer": "layer"
136+
"Layer": "layer",
137+
"HasSelected": "Has selected {0}/{1} items",
138+
"Label": "Label"
137139
}

src/Masa.Stack.Components/Locales/ja-JP.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,5 +218,7 @@
218218
"404Tips": "要求されたURLを見つけることができません。",
219219
"BackToHome": "ホームに戻る",
220220
"NavigationLayerLabel": "メニューディスプレイ",
221-
"Layer": "レイヤー"
221+
"Layer": "レイヤー",
222+
"HasSelected": "{0}/{1} アイテムが選択されています",
223+
"Label": "ラベル"
222224
}

src/Masa.Stack.Components/Locales/ru-RU.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,5 +218,7 @@
218218
"404Tips": "Не найден запрашиваемый URL.",
219219
"BackToHome": "Вернуться на главную",
220220
"NavigationLayerLabel": "Меню Показать",
221-
"Layer": "Слой"
221+
"Layer": "Слой",
222+
"HasSelected": "Выбрано {0}/{1} элементов",
223+
"Label": "Метка"
222224
}

src/Masa.Stack.Components/Locales/zh-CN.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,5 +219,7 @@
219219
"404Tips": "找不到请求的URL。",
220220
"BackToHome": "回到主页",
221221
"NavigationLayerLabel": "菜单显示",
222-
"Layer": ""
222+
"Layer": "",
223+
"HasSelected": "已选择 {0}/{1} 条数据",
224+
"Label": "标签"
223225
}

src/Masa.Stack.Components/wwwroot/css/app.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,3 +1020,14 @@ h1:focus {
10201020
padding-left: 300px !important;
10211021
}
10221022
}
1023+
1024+
/* expandable-autocomplete */
1025+
1026+
.s-expandable-autocomplete .m-input__append-outer {
1027+
margin-top: 10px !important;
1028+
margin-left: 4px !important;
1029+
}
1030+
1031+
.s-expandable-autocomplete.m-input--dense .m-input__append-outer {
1032+
margin-top: 6px !important;
1033+
}

tests/Masa.Stack.Components.Standalone.TestApp/Pages/Index.razor

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,36 @@
22

33
<h1>Hello world!</h1>
44

5-
<div style="height:150vh">
5+
<SExpandableAutocomplete @bind-Value="_values"
6+
Items="_items"
7+
ItemText="r => r"
8+
ItemValue="r => r"
9+
Outlined
10+
Chips
11+
Dense
12+
SmallChips
13+
Label="Outlined">
14+
</SExpandableAutocomplete>
615

7-
<SActions Dense>
8-
<ChildContent>
9-
<MButton Small Outlined Color="primary">Details</MButton>
10-
</ChildContent>
11-
<DropdownContent>
12-
<MButton LeftIconName="mdi-account">Account</MButton>
13-
<MButton LeftIconName="mdi-cog">Settings</MButton>
14-
<MButton LeftIconName="mdi-lock">Privacy</MButton>
15-
<MDivider/>
16-
<MButton>Star</MButton>
17-
<MDivider/>
18-
<MButton Color="error" LeftIconName="mdi-delete">Delete</MButton>
19-
</DropdownContent>
20-
</SActions>
16+
@code{
2117

22-
<MCard>
23-
<SActions Dense>
24-
<ChildContent>
25-
<MButton Small Outlined Color="primary">Details</MButton>
26-
</ChildContent>
27-
<DropdownContent>
28-
<MButton LeftIconName="mdi-account">Account</MButton>
29-
<MButton LeftIconName="mdi-cog">Settings</MButton>
30-
<MButton LeftIconName="mdi-lock">Privacy</MButton>
31-
<MDivider/>
32-
<MButton>Star</MButton>
33-
<MDivider/>
34-
<MButton Color="error" LeftIconName="mdi-delete">Delete</MButton>
35-
</DropdownContent>
36-
</SActions>
37-
</MCard>
38-
</div>
18+
private List<string> _items = new List<string>()
19+
{
20+
"foo", "bar", "fizz", "buzz", "hello", "world", "lorem", "ipsum",
21+
"sit", "amet", "consectetur", "adipiscing", "elit",
22+
"sed", "do", "eiusmod", "tempor", "incididunt", "labore",
23+
"et", "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud", "exercitation",
24+
"ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
25+
"consequat", "duis", "aute", "irure",
26+
"reprehenderit", "voluptate", "velit",
27+
"esse", "cillum", "eu", "fugiat",
28+
"nulla", "pariatur", "excepteur", "sint",
29+
};
30+
31+
private List<string> _values = new List<string>
32+
{
33+
"foo", "bar"
34+
};
35+
36+
private string _value = "foo";
37+
}

tests/Masa.Stack.Components.Standalone.TestApp/Pages/_Host.cshtml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<link href="css/site.css" rel="stylesheet"/>
1414
<link href="Masa.Stack.Components.Standalone.TestApp.styles.css" rel="stylesheet"/>
1515
<link href="https://cdn.masastack.com/npm/@("@mdi")/[email protected]/css/materialdesignicons.min.css" rel="stylesheet">
16+
<link href="_content/Masa.Stack.Components/css/standard.css" rel="stylesheet">
17+
<link href="_content/Masa.Stack.Components/css/app.css" rel="stylesheet">
1618

1719
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
1820
</head>

0 commit comments

Comments
 (0)