Add multiple select component

This commit is contained in:
Maxime Adler
2025-03-11 15:00:52 +01:00
parent 3ba79fdf03
commit 7d8f1c9544
17 changed files with 163 additions and 75 deletions

View File

@@ -35,6 +35,12 @@
<SearchInput @bind-Text=GameFilterParams.SearchName /> <SearchInput @bind-Text=GameFilterParams.SearchName />
<MultipleSelectList TItem="string"
Items="Plateforms"
@bind-Values=GameFilterParams.Plateforms
Theme="SelectListTheme.Filter"/>
</div> </div>
</EditForm> </EditForm>

View File

@@ -1,5 +1,5 @@
using GameIdeas.BlazorApp.Pages.Games.Models; using GameIdeas.BlazorApp.Pages.Games.Models;
using GameIdeas.BlazorApp.Shared.Components.Select; using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.Shared.Dto; using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Enum; using GameIdeas.Shared.Enum;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -24,6 +24,13 @@ public partial class GameFilter
new() { Item = game => game?.ReleaseDate, Label = "Date de parution" } new() { Item = game => game?.ReleaseDate, Label = "Date de parution" }
]; ];
private readonly IEnumerable<SelectElement<string>> Plateforms = [
new() { Item = "Steam", Label = "Steam" },
new() { Item = "GOG", Label = "GOG" },
new() { Item = "Epic games", Label = "Epic games" },
new() { Item = "Ubisoft", Label = "Ubisoft" },
];
private EditContext? EditContext; private EditContext? EditContext;
protected override void OnInitialized() protected override void OnInitialized()

View File

@@ -8,4 +8,7 @@ public class GameFilterParams
public SortType? SortType { get; set; } public SortType? SortType { get; set; }
public Func<GameDto?, object?>? SortProperty { get; set; } public Func<GameDto?, object?>? SortProperty { get; set; }
public string? SearchName { get; set; } public string? SearchName { get; set; }
public IEnumerable<string>? Plateforms { get; set; }
public IEnumerable<string>? Genres { get; set; }
} }

View File

@@ -7,6 +7,7 @@
"launchBrowser": true, "launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5172", "applicationUrl": "http://localhost:5172",
"launchUrl": "http://localhost:5172/Games",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@@ -17,6 +18,7 @@
"launchBrowser": true, "launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7060;http://localhost:5172", "applicationUrl": "https://localhost:7060;http://localhost:5172",
"launchUrl": "http://localhost:7060/Games",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@@ -1,29 +1,30 @@
<EditForm EditContext="EditContext"> <div class="search-container">
<div class="search-container"> <input type="text"
<InputText class="search-field" class="search-field"
@bind-Value=Text /> @bind=@Text
@bind:event="oninput"
@bind:after=HandleTextChanged />
@if (!string.IsNullOrEmpty(Text)) @if (!string.IsNullOrEmpty(Text))
{ {
<div class="clear-icon" @onclick=HandleClearClicked> <div class="clear-icon" @onclick=HandleClearClicked>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
</div>
}
<div type="submit" class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@if (Icon == SearchInputIcon.Search) <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
{
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" />
}
else if (Icon == SearchInputIcon.Dropdown)
{
<path style="fill: var(--violet)" d="M1 3H23L12 22" />
}
</svg> </svg>
</div> </div>
</div> }
</EditForm>
<div class="search-icon @(Enum.GetName(Icon)?.ToLower())" @onclick=HandleSearchClicked>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
@if (Icon == SearchInputIcon.Search)
{
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" />
}
else if (Icon == SearchInputIcon.Dropdown)
{
<path d="M1 3H23L12 22" />
}
</svg>
</div>
</div>

View File

@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Threading.Tasks;
namespace GameIdeas.BlazorApp.Shared.Components.Search; namespace GameIdeas.BlazorApp.Shared.Components.Search;
@@ -8,23 +6,32 @@ public partial class SearchInput
{ {
[Parameter] public string? Text { get; set; } [Parameter] public string? Text { get; set; }
[Parameter] public EventCallback<string> TextChanged { get; set; } [Parameter] public EventCallback<string> TextChanged { get; set; }
[Parameter] public EventCallback ClearClicked { get; set; }
[Parameter] public EventCallback SearchClicked { get; set; }
[Parameter] public SearchInputIcon Icon { get; set; } [Parameter] public SearchInputIcon Icon { get; set; }
private EditContext? EditContext;
protected override void OnInitialized() protected override void OnInitialized()
{ {
Text = string.Empty; Text = string.Empty;
EditContext = new EditContext(Text);
EditContext.OnFieldChanged += async (s, e) =>
{
await TextChanged.InvokeAsync(Text);
};
} }
private void HandleClearClicked() public void SetText(string str)
{
Text = str;
}
private async Task HandleTextChanged()
{
await TextChanged.InvokeAsync(Text);
}
private async Task HandleClearClicked()
{ {
Text = string.Empty; Text = string.Empty;
await ClearClicked.InvokeAsync();
}
private async Task HandleSearchClicked()
{
await SearchClicked.InvokeAsync();
} }
} }

View File

@@ -51,3 +51,8 @@
.search-icon svg { .search-icon svg {
fill: var(--white); fill: var(--white);
} }
.search-icon.dropdown svg {
fill: var(--violet);
transform: scale(.8, .5);
}

View File

@@ -1,3 +1,4 @@
using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace GameIdeas.BlazorApp.Shared.Components.Select.Components; namespace GameIdeas.BlazorApp.Shared.Components.Select.Components;
@@ -11,6 +12,7 @@ public partial class SelectListElement<TItem>
{ {
if (Value != null) if (Value != null)
{ {
Value.IsSelected = true;
await ValueChanged.InvokeAsync(Value); await ValueChanged.InvokeAsync(Value);
} }
} }

View File

@@ -28,15 +28,14 @@
.navigation { .navigation {
padding: 4px 8px; padding: 4px 8px;
} }
.navigation .selected {
display: none;
}
.navigation:hover { .navigation:hover {
background: var(--violet-selected); background: var(--violet-selected);
} }
.navigation .selected {
display: none;
}
/***** Sort Theme *****/ /***** Sort Theme *****/
.sort { .sort {
padding: 2px 6px; padding: 2px 6px;
@@ -46,9 +45,23 @@
background: var(--low-white); background: var(--low-white);
} }
.sort .select-label { .sort .select-label {
text-wrap: nowrap; text-wrap: nowrap;
margin-right: 6px; margin-right: 6px;
}
/***** Filter Theme *****/
.filter {
padding: 2px 6px;
} }
.filter:hover {
background: var(--low-white);
}
.filter .select-label {
text-wrap: nowrap;
margin-right: 6px;
}

View File

@@ -1,4 +1,4 @@
namespace GameIdeas.BlazorApp.Shared.Components.Select; namespace GameIdeas.BlazorApp.Shared.Components.Select.Models;
public class SelectElement<TItem> public class SelectElement<TItem>
{ {

View File

@@ -1,4 +1,4 @@
namespace GameIdeas.BlazorApp.Shared.Components.Select; namespace GameIdeas.BlazorApp.Shared.Components.Select.Models;
public enum SelectListTheme public enum SelectListTheme
{ {

View File

@@ -2,13 +2,15 @@
@using GameIdeas.BlazorApp.Shared.Components.Select.Components @using GameIdeas.BlazorApp.Shared.Components.Select.Components
@typeparam TItem @typeparam TItem
<div class="select-list" @onclick=HandleButtonClicked> <div class="select-list">
<div class="select-button"> <div class="select-button" @onfocusin=HandleFocusIn>
<SearchInput Icon="SearchInputIcon.Dropdown" <SearchInput @ref=SearchInput
Text="Text" Icon="SearchInputIcon.Dropdown"
TextChanged="HandleTextChanged"/> TextChanged="HandleTextChanged"
ClearClicked="HandleTextChanged"
SearchClicked="Open" />
</div> </div>
<div @ref=Container @onfocusout=HandleFocusOut <div @onfocusout=HandleFocusOut
class="select-container @(AlignRight ? "align-right" : "")" class="select-container @(AlignRight ? "align-right" : "")"
tabindex="1000"> tabindex="1000">

View File

@@ -1,3 +1,5 @@
using GameIdeas.BlazorApp.Shared.Components.Search;
using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace GameIdeas.BlazorApp.Shared.Components.Select; namespace GameIdeas.BlazorApp.Shared.Components.Select;
@@ -11,41 +13,31 @@ public partial class MultipleSelectList<TItem>
[Parameter] public bool AlignRight { get; set; } [Parameter] public bool AlignRight { get; set; }
private bool ContentVisile = false; private bool ContentVisile = false;
private DateTime ContentLastFocusOut = DateTime.Now; private SearchInput? SearchInput;
private ElementReference Container;
private string? Text;
public async Task OpenAsync() public void Open() => ContentVisile = true;
{
if (DateTime.Now - ContentLastFocusOut >= TimeSpan.FromSeconds(0.2))
{
await Container.FocusAsync();
ContentVisile = true;
}
}
public void Close() => ContentVisile = false; public void Close() => ContentVisile = false;
private async Task HandleButtonClicked() => await OpenAsync(); private void HandleFocusOut() => Close();
private void HandleFocusOut() private void HandleFocusIn() => Open();
{
ContentLastFocusOut = DateTime.Now;
ContentVisile = true;
}
private async Task HandleItemClicked(SelectElement<TItem> selectedValue) private async Task HandleItemClicked(SelectElement<TItem> selectedValue)
{ {
selectedValue.IsSelected = !selectedValue.IsSelected; selectedValue.IsSelected = !selectedValue.IsSelected;
Values = Items.Where(x => x.IsSelected && x.Item != null).Select(x => x.Item!); Values = Items.Where(x => x.IsSelected && x.Item != null).Select(x => x.Item!);
Text = string.Join(", ", Values); SearchInput?.SetText(string.Join(", ", Values));
await ValuesChanged.InvokeAsync(Values); await ValuesChanged.InvokeAsync(Values);
} }
private void HandleTextChanged(string args)
private void HandleTextChanged()
{ {
foreach (var item in Items) foreach (var item in Items)
{ {
item.IsSelected = false; item.IsSelected = false;
} }
Close();
} }
} }

View File

@@ -1 +1,49 @@
@import "SelectList.razor.css"; .select-list {
position: relative;
}
.select-container {
margin-top: 4px;
position: absolute;
}
.align-right {
right: 0;
}
.select-content {
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: var(--small-radius);
animation-name: fade-in;
animation-duration: 0.4s;
}
.line {
margin: 2px 6px;
border-bottom: 2px solid var(--low-white);
}
/***** Navigation Theme *****/
.select-content.navigation {
background: var(--violet);
box-shadow: var(--drop-shadow);
}
/***** Sort Theme *****/
.select-content.sort {
background: var(--semi-black);
box-shadow: var(--drop-shadow);
padding: 4px 0;
}
/***** Filter Theme *****/
.select-content.filter {
background: var(--semi-black);
box-shadow: var(--drop-shadow);
padding: 4px 0;
min-width: 150px;
}

View File

@@ -1,3 +1,4 @@
using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace GameIdeas.BlazorApp.Shared.Components.Select; namespace GameIdeas.BlazorApp.Shared.Components.Select;
@@ -34,7 +35,7 @@ public partial class SelectList<TItem>
private void HandleFocusOut() private void HandleFocusOut()
{ {
ContentLastFocusOut = DateTime.Now; ContentLastFocusOut = DateTime.Now;
ContentVisile = true; ContentVisile = false;
} }
private async Task HandleItemClicked(SelectElement<TItem> selectedValue) private async Task HandleItemClicked(SelectElement<TItem> selectedValue)

View File

@@ -38,4 +38,3 @@
box-shadow: var(--drop-shadow); box-shadow: var(--drop-shadow);
padding: 4px 0; padding: 4px 0;
} }

View File

@@ -1,6 +1,6 @@
using GameIdeas.BlazorApp.Pages.Games.Models; using GameIdeas.BlazorApp.Pages.Games.Models;
using GameIdeas.BlazorApp.Shared.Components.Account; using GameIdeas.BlazorApp.Shared.Components.Account;
using GameIdeas.BlazorApp.Shared.Components.Select; using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.Resources; using GameIdeas.Resources;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;