mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 15:41:40 -04:00
196 lines
7.1 KiB
Plaintext
196 lines
7.1 KiB
Plaintext
@inject DbService DbService
|
|
@inject NavigationManager NavigationManager
|
|
@inject KeyboardShortcutService KeyboardShortcutService
|
|
@inject JsInteropService JsInteropService
|
|
@implements IAsyncDisposable
|
|
|
|
<ClickOutsideHandler OnClose="OnClose" ContentId="searchWidgetContainer">
|
|
<div class="relative" id="searchWidgetContainer">
|
|
<input
|
|
id="searchWidget"
|
|
type="text"
|
|
placeholder="Search vault..."
|
|
autocomplete="off"
|
|
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
|
|
@bind-value="SearchTerm"
|
|
@oninput="SearchTermChanged"
|
|
@onfocus="OnFocus"
|
|
@onkeydown="HandleKeyDown"/>
|
|
|
|
@if (ShowHelpText)
|
|
{
|
|
<div class="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 p-2 text-sm text-gray-600 dark:text-gray-400">
|
|
@if (string.IsNullOrEmpty(SearchTerm))
|
|
{
|
|
<p>Type a term to search for, this can be the service name, description or email address.</p>
|
|
}
|
|
else if (SearchTerm.Length == 1)
|
|
{
|
|
<p>Please type more chars</p>
|
|
}
|
|
else
|
|
{
|
|
<p>Searching for "@SearchTerm"</p>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (ShowResults && SearchTerm.Length >= 2)
|
|
{
|
|
@if (SearchResults.Any())
|
|
{
|
|
<div class="absolute z-10 w-screen left-0 sm:left-auto sm:w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
|
|
@for (int i = 0; i < SearchResults.Count; i++)
|
|
{
|
|
var result = SearchResults[i];
|
|
<div
|
|
class="search-result @(i == SelectedIndex ? "bg-gray-100 dark:bg-gray-700" : "") px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center"
|
|
@onclick="() => SelectResult(result)">
|
|
<DisplayFavicon FaviconBytes="@result.Service.Logo" Width="24" />
|
|
<div class="ml-2">
|
|
<div>@result.Service.Name</div>
|
|
@if (!string.IsNullOrEmpty(result.Alias.Email))
|
|
{
|
|
<span class="text-gray-500">(@result.Alias.Email)</span>
|
|
}
|
|
else if (!string.IsNullOrEmpty(result.Username))
|
|
{
|
|
<span class="text-gray-500">(@result.Username)</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="absolute z-10 w-screen left-0 sm:left-auto sm:w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
|
|
<div class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
No results found
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
</ClickOutsideHandler>
|
|
|
|
@code {
|
|
private string SearchTerm { get; set; } = string.Empty;
|
|
private List<Credential> SearchResults { get; set; } = new();
|
|
private bool ShowResults { get; set; }
|
|
private bool ShowHelpText { get; set; }
|
|
private int SelectedIndex { get; set; } = -1;
|
|
|
|
/// <inheritdoc />
|
|
async ValueTask IAsyncDisposable.DisposeAsync()
|
|
{
|
|
await KeyboardShortcutService.UnregisterShortcutAsync("gs");
|
|
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
|
NavigationManager.LocationChanged -= ResetSearchField;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
await KeyboardShortcutService.RegisterShortcutAsync("gs", FocusSearchField);
|
|
await KeyboardShortcutService.RegisterShortcutAsync("gf", FocusSearchField);
|
|
NavigationManager.LocationChanged += ResetSearchField;
|
|
}
|
|
}
|
|
|
|
private void OnFocus()
|
|
{
|
|
ShowHelpText = true;
|
|
ShowResults = true;
|
|
}
|
|
|
|
private void OnClose()
|
|
{
|
|
ShowHelpText = false;
|
|
ShowResults = false;
|
|
}
|
|
|
|
private async Task SearchTermChanged(ChangeEventArgs e)
|
|
{
|
|
SearchTerm = e.Value?.ToString() ?? string.Empty;
|
|
await PerformSearch();
|
|
}
|
|
|
|
private async Task PerformSearch()
|
|
{
|
|
var context = await DbService.GetDbContextAsync();
|
|
if (SearchTerm.Length >= 2)
|
|
{
|
|
var searchTerms = SearchTerm.Trim().ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var query = context.Credentials
|
|
.Include(x => x.Service)
|
|
.Include(x => x.Alias)
|
|
.Where(x => !x.IsDeleted)
|
|
.AsQueryable();
|
|
|
|
foreach (var term in searchTerms)
|
|
{
|
|
query = query.Where(x =>
|
|
(x.Service.Name != null && EF.Functions.Like(x.Service.Name.ToLower(), $"%{term}%")) ||
|
|
(x.Alias.Email != null && EF.Functions.Like(x.Alias.Email.ToLower(), $"%{term}%")) ||
|
|
(x.Username != null && EF.Functions.Like(x.Username.ToLower(), $"%{term}%")) ||
|
|
(x.Service.Url != null && EF.Functions.Like(x.Service.Url.ToLower(), $"%{term}%"))
|
|
);
|
|
}
|
|
|
|
SearchResults = await query.Take(10).ToListAsync();
|
|
|
|
// Select first entry by default so when pressing enter, the first result is immediately selected.
|
|
SelectedIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
SearchResults.Clear();
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task HandleKeyDown(KeyboardEventArgs e)
|
|
{
|
|
switch (e.Key)
|
|
{
|
|
case "ArrowDown":
|
|
SelectedIndex = Math.Min(SelectedIndex + 1, SearchResults.Count - 1);
|
|
break;
|
|
case "ArrowUp":
|
|
SelectedIndex = Math.Max(SelectedIndex - 1, -1);
|
|
break;
|
|
case "Enter":
|
|
if (SelectedIndex >= 0 && SelectedIndex < SearchResults.Count)
|
|
{
|
|
await SelectResult(SearchResults[SelectedIndex]);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async Task SelectResult(Credential credential)
|
|
{
|
|
await JsInteropService.BlurElementById("searchWidget");
|
|
NavigationManager.NavigateTo($"/credentials/{credential.Id}");
|
|
}
|
|
|
|
private void ResetSearchField(object? sender, LocationChangedEventArgs e)
|
|
{
|
|
SearchTerm = string.Empty;
|
|
SearchResults.Clear();
|
|
StateHasChanged();
|
|
OnClose();
|
|
}
|
|
|
|
private async Task FocusSearchField()
|
|
{
|
|
await JsInteropService.FocusElementById("searchWidget");
|
|
}
|
|
}
|