feat(MilkStream.Client): add album search dropdown component
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
@using Butter.Dtos.Album
|
||||
@inject AlbumService albumService
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div class="position-relative">
|
||||
<input type="text" class="form-control"
|
||||
placeholder="@Placeholder"
|
||||
value="@Query"
|
||||
@ref="albumInputRef"
|
||||
@oninput="OnInput"
|
||||
@onfocus="OnFocus"
|
||||
@onblur="OnBlur"
|
||||
@onkeydown="OnKeyDown" />
|
||||
@if (showAlbumDropdown) {
|
||||
<div class="dropdown-menu show album-dropdown-menu"
|
||||
@ref="albumDropdownRef">
|
||||
@if (isSearching) {
|
||||
<span class="dropdown-item disabled">Searching…</span>
|
||||
} else if (Query.Length < 2) {
|
||||
<span class="dropdown-item disabled">Type at least 2 characters to search</span>
|
||||
} else if (searchResults is null || searchResults.Count == 0) {
|
||||
<span class="dropdown-item disabled">No matching albums</span>
|
||||
} else {
|
||||
@foreach (var album in searchResults) {
|
||||
<button type="button"
|
||||
class="dropdown-item @(SelectedAlbumId == album.Id ? "active" : "")"
|
||||
@onclick="() => SelectAlbum(album)">
|
||||
@album.Name
|
||||
@if (!string.IsNullOrEmpty(album.PersonName)) {
|
||||
<small class="text-muted ms-1">@album.PersonName</small>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets or sets the current search text (two-way bound so the owner can prefill/reset it).
|
||||
/// </summary>
|
||||
[Parameter] public string Query { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback fired as <see cref="Query"/> changes.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<string> QueryChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently selected album ID, used to highlight the active result.
|
||||
/// </summary>
|
||||
[Parameter] public Guid? SelectedAlbumId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback fired when an album is selected.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<AlbumPreviewDto> OnAlbumSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the input placeholder text.
|
||||
/// </summary>
|
||||
[Parameter] public string Placeholder { get; set; } = "Search albums…";
|
||||
|
||||
bool showAlbumDropdown;
|
||||
CancellationTokenSource? blurCts;
|
||||
List<AlbumPreviewDto>? searchResults;
|
||||
bool isSearching;
|
||||
int searchVersion;
|
||||
ElementReference albumInputRef;
|
||||
ElementReference albumDropdownRef;
|
||||
bool pendingPosition;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (pendingPosition && showAlbumDropdown) {
|
||||
pendingPosition = false;
|
||||
await JSRuntime.InvokeVoidAsync("masonryObserver.positionDropdown", albumInputRef, albumDropdownRef);
|
||||
}
|
||||
}
|
||||
|
||||
void OnFocus() {
|
||||
blurCts?.Cancel();
|
||||
showAlbumDropdown = true;
|
||||
pendingPosition = true;
|
||||
if (Query.Length >= 2) {
|
||||
_ = Search(Query);
|
||||
}
|
||||
}
|
||||
|
||||
async Task OnInput(ChangeEventArgs e) {
|
||||
Query = e.Value?.ToString() ?? string.Empty;
|
||||
await QueryChanged.InvokeAsync(Query);
|
||||
showAlbumDropdown = true;
|
||||
pendingPosition = true;
|
||||
if (Query.Length >= 2) {
|
||||
await Search(Query);
|
||||
} else {
|
||||
searchResults = null;
|
||||
isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
async Task Search(string query) {
|
||||
var version = ++searchVersion;
|
||||
isSearching = true;
|
||||
searchResults = await albumService.GetAlbumsAsync(page: 0, pageSize: 20, search: query);
|
||||
if (version == searchVersion) {
|
||||
isSearching = false;
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
async Task OnBlur() {
|
||||
blurCts?.Cancel();
|
||||
blurCts = new CancellationTokenSource();
|
||||
try {
|
||||
await Task.Delay(150, blurCts.Token);
|
||||
showAlbumDropdown = false;
|
||||
} catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
void OnKeyDown(KeyboardEventArgs e) {
|
||||
if (e.Key is "Escape") {
|
||||
showAlbumDropdown = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectAlbum(AlbumPreviewDto album) {
|
||||
Query = album.Name;
|
||||
_ = QueryChanged.InvokeAsync(Query);
|
||||
showAlbumDropdown = false;
|
||||
searchResults = null;
|
||||
_ = OnAlbumSelected.InvokeAsync(album);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.album-dropdown-menu {
|
||||
display: block;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
Reference in New Issue
Block a user