mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-25 01:52:12 -04:00
Add password visibility toggles (#773)
This commit is contained in:
@@ -2,16 +2,15 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div class="relative">
|
||||
<InputText @attributes="AdditionalAttributes"
|
||||
@ref="inputComponent"
|
||||
id="@Id"
|
||||
Value="@Value"
|
||||
ValueChanged="ValueChanged"
|
||||
ValueExpression="ValueExpression"
|
||||
type="@(_showPassword ? "text" : "password")"
|
||||
placeholder="@Placeholder"
|
||||
autocomplete="off"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
<input @attributes="AdditionalAttributes"
|
||||
@ref="inputElement"
|
||||
id="@Id"
|
||||
value="@Value"
|
||||
@oninput="OnInputChanged"
|
||||
type="@(_showPassword ? "text" : "password")"
|
||||
placeholder="@Placeholder"
|
||||
autocomplete="off"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
|
||||
<button type="button"
|
||||
@onclick="TogglePasswordVisibility"
|
||||
@@ -35,7 +34,7 @@
|
||||
|
||||
@code {
|
||||
private bool _showPassword = false;
|
||||
private InputText? inputComponent;
|
||||
private ElementReference inputElement;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the input field.
|
||||
@@ -59,7 +58,7 @@
|
||||
/// Gets or sets the expression that identifies the value property.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public required Expression<Func<string>> ValueExpression { get; set; }
|
||||
public Expression<Func<string>>? ValueExpression { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the placeholder text for the input field.
|
||||
@@ -73,6 +72,15 @@
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object?>? AdditionalAttributes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Handles input changes and triggers the ValueChanged callback immediately.
|
||||
/// </summary>
|
||||
private async Task OnInputChanged(ChangeEventArgs e)
|
||||
{
|
||||
var newValue = e.Value?.ToString();
|
||||
await ValueChanged.InvokeAsync(newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the password visibility.
|
||||
/// </summary>
|
||||
@@ -89,19 +97,11 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
if (inputComponent?.Element != null)
|
||||
{
|
||||
await inputComponent.Element.Value.FocusAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to JS focus if component element is not available
|
||||
await JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('{Id}')?.focus()");
|
||||
}
|
||||
await inputElement.FocusAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Final fallback to JS focus if ElementReference focus fails
|
||||
// Fallback to JS focus if ElementReference focus fails
|
||||
await JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('{Id}')?.focus()");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
@using System.Timers
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using AliasVault.Client.Main.Components.Shared
|
||||
@using AliasVault.Client.Main.Components.Loading
|
||||
@using AliasVault.Client.Main.Constants
|
||||
@using AliasVault.Client.Auth.Components
|
||||
|
||||
<div class="w-full mx-auto">
|
||||
@if (_isLoading)
|
||||
@@ -39,13 +41,15 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="">
|
||||
<EditFormRow Id="password" Label="@Localizer["MasterPasswordLabel"]" @bind-Value="Password" Type="password" Placeholder="@Localizer["MasterPasswordPlaceholder"]" OnFocus="@OnPasswordInputFocus"/>
|
||||
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["MasterPasswordLabel"]</label>
|
||||
<PasswordInputField Id="password" @bind-Value="Password" Placeholder="@Localizer["MasterPasswordPlaceholder"]" @onfocus="@OnPasswordInputFocus" />
|
||||
</div>
|
||||
|
||||
<PasswordStrengthIndicator Password="@Password" />
|
||||
|
||||
<div class="mt-4">
|
||||
<EditFormRow Id="confirmPassword" Label="@Localizer["ConfirmMasterPasswordLabel"]" @bind-Value="ConfirmPassword" Type="password" Placeholder="@Localizer["ConfirmMasterPasswordPlaceholder"]" OnFocus="@OnPasswordInputFocus" />
|
||||
<label for="confirmPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmMasterPasswordLabel"]</label>
|
||||
<PasswordInputField Id="confirmPassword" @bind-Value="ConfirmPassword" Placeholder="@Localizer["ConfirmMasterPasswordPlaceholder"]" @onfocus="@OnPasswordInputFocus" />
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
@@ -73,7 +77,7 @@
|
||||
if (_password != value)
|
||||
{
|
||||
_password = value;
|
||||
ValidatePassword();
|
||||
ValidatePasswordWithDebounce();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +90,7 @@
|
||||
if (_confirmPassword != value)
|
||||
{
|
||||
_confirmPassword = value;
|
||||
ValidatePassword();
|
||||
ValidatePasswordImmediate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +119,7 @@
|
||||
_loadingTimer.AutoReset = false;
|
||||
_loadingTimer.Start();
|
||||
|
||||
_debounceTimer = new Timer(300);
|
||||
_debounceTimer = new Timer(800);
|
||||
_debounceTimer.Elapsed += async (sender, e) => await ValidatePasswordDebounced();
|
||||
_debounceTimer.AutoReset = false;
|
||||
}
|
||||
@@ -141,9 +145,9 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the password immediately.
|
||||
/// Validates the password with debounce (for main password field).
|
||||
/// </summary>
|
||||
private void ValidatePassword()
|
||||
private void ValidatePasswordWithDebounce()
|
||||
{
|
||||
_errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
@@ -152,6 +156,14 @@
|
||||
_debounceTimer?.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the password immediately (for confirm password field).
|
||||
/// </summary>
|
||||
private async void ValidatePasswordImmediate()
|
||||
{
|
||||
await ValidatePasswordDebounced();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the password after input has stopped.
|
||||
/// </summary>
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
/// Gets or sets the CSS class for the confirm button.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ConfirmButtonClass { get; set; } = "bg-orange-600 hover:bg-orange-500 dark:bg-orange-700 dark:hover:bg-orange-600";
|
||||
public string ConfirmButtonClass { get; set; } = "bg-primary-600 hover:bg-primary-700";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the modal is in a loading state.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using AliasVault.Client.Auth.Components
|
||||
|
||||
<FormModal
|
||||
IsOpen="@IsOpen"
|
||||
@@ -30,13 +31,10 @@
|
||||
<label for="password-confirm" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
|
||||
@SharedLocalizer["Password"]
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password-confirm"
|
||||
@bind="_password"
|
||||
@bind:event="oninput"
|
||||
placeholder="@Localizer["EnterPasswordPlaceholder"]"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
<PasswordInputField
|
||||
Id="password-confirm"
|
||||
@bind-Value="_password"
|
||||
Placeholder="@Localizer["EnterPasswordPlaceholder"]" />
|
||||
</div>
|
||||
</ChildContent>
|
||||
</FormModal>
|
||||
|
||||
@@ -1,73 +1,78 @@
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using AliasVault.RazorComponents.Services
|
||||
@using AliasVault.Client.Main.Components.Shared
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@inject IStringLocalizerFactory LocalizerFactory
|
||||
|
||||
@if (IsOpen)
|
||||
{
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 w-full max-w-md">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">@Title</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">@Description</p>
|
||||
<FormModal
|
||||
IsOpen="@IsOpen"
|
||||
Title="@Title"
|
||||
MaxWidth="md"
|
||||
ShowDefaultFooter="false"
|
||||
OnClose="@OnClose"
|
||||
SubmitOnEnter="false">
|
||||
<Icon>
|
||||
<svg class="h-6 w-6 text-orange-600 dark:text-orange-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
</Icon>
|
||||
<ChildContent>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
@Description
|
||||
</p>
|
||||
|
||||
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 rounded-lg text-sm">
|
||||
@ErrorMessage
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<AlertMessageError Message="@ErrorMessage" HasTopMargin="false" />
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
@Localizer["ExportPasswordLabel"]
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@bind="_exportPassword"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="HandleKeyDown"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
autofocus />
|
||||
<div class="mb-4">
|
||||
<label for="exportPassword" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
|
||||
@Localizer["ExportPasswordLabel"]
|
||||
</label>
|
||||
<PasswordInputField
|
||||
Id="exportPassword"
|
||||
@bind-Value="_exportPassword"
|
||||
Placeholder=""
|
||||
@onkeydown="HandleKeyDown"
|
||||
autofocus="true" />
|
||||
|
||||
<PasswordStrengthIndicator Password="@_exportPassword" OnStrengthChanged="@HandleStrengthChanged" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
@Localizer["ConfirmExportPasswordLabel"]
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@bind="_confirmPassword"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="HandleKeyDown"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_confirmPassword) && _exportPassword != _confirmPassword)
|
||||
{
|
||||
<p class="mt-1 text-xs text-red-600 dark:text-red-400">@Localizer["PasswordsDoNotMatch"]</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@onclick="OnClose"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg">
|
||||
@SharedLocalizer["Cancel"]
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@onclick="HandleSubmit"
|
||||
disabled="@(!IsPasswordValid())"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-lg">
|
||||
@Localizer["CreateEncryptedExportButton"]
|
||||
</button>
|
||||
</div>
|
||||
<PasswordStrengthIndicator Password="@_exportPassword" OnStrengthChanged="@HandleStrengthChanged" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="confirmExportPassword" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
|
||||
@Localizer["ConfirmExportPasswordLabel"]
|
||||
</label>
|
||||
<PasswordInputField
|
||||
Id="confirmExportPassword"
|
||||
@bind-Value="_confirmPassword"
|
||||
Placeholder=""
|
||||
@onkeydown="HandleKeyDown" />
|
||||
|
||||
@if (!string.IsNullOrEmpty(_confirmPassword) && _exportPassword != _confirmPassword)
|
||||
{
|
||||
<p class="mt-1 text-xs text-red-600 dark:text-red-400">@Localizer["PasswordsDoNotMatch"]</p>
|
||||
}
|
||||
</div>
|
||||
</ChildContent>
|
||||
<FooterContent>
|
||||
<button
|
||||
type="button"
|
||||
@onclick="HandleSubmit"
|
||||
disabled="@(!IsPasswordValid())"
|
||||
class="inline-flex w-full justify-center rounded-md bg-primary-600 hover:bg-primary-700 px-3 py-2 text-sm font-semibold text-white shadow-sm sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
@Localizer["CreateEncryptedExportButton"]
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@onclick="OnClose"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-gray-700 dark:text-white dark:ring-gray-600 dark:hover:bg-gray-600">
|
||||
@SharedLocalizer["Cancel"]
|
||||
</button>
|
||||
</FooterContent>
|
||||
</FormModal>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@using AliasVault.Shared.Models.WebApi.Favicon
|
||||
@using AliasClientDb
|
||||
@using AliasClientDb.Models
|
||||
@using AliasVault.Client.Auth.Components
|
||||
|
||||
<div @onclick="OpenImportModal" data-import-service="@ServiceName" class="flex flex-col p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200">
|
||||
<div class="flex items-center">
|
||||
@@ -105,16 +106,15 @@
|
||||
{
|
||||
<div class="mb-4">
|
||||
<p class="mb-4 text-gray-700 dark:text-gray-300">@Localizer["EncryptedFilePasswordPrompt"]</p>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label for="decryptionPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
@Localizer["DecryptionPasswordLabel"]
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@bind="_decryptionPassword"
|
||||
@bind:event="oninput"
|
||||
<PasswordInputField
|
||||
Id="decryptionPassword"
|
||||
@bind-Value="_decryptionPassword"
|
||||
Placeholder=""
|
||||
@onkeydown="HandlePasswordKeyDown"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
|
||||
autofocus />
|
||||
autofocus="true" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4 p-3 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg">
|
||||
@@ -846,7 +846,7 @@
|
||||
else
|
||||
{
|
||||
// Create new logo from embedded bytes
|
||||
var logo = new Logo
|
||||
var logo = new AliasClientDb.Logo
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Source = domain,
|
||||
@@ -896,7 +896,7 @@
|
||||
else if (ExtractedFavicons.TryGetValue(domain, out var favicon))
|
||||
{
|
||||
// Create new logo from extracted favicon
|
||||
var logo = new Logo
|
||||
var logo = new AliasClientDb.Logo
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Source = domain,
|
||||
|
||||
@@ -2029,6 +2029,11 @@ video {
|
||||
background-color: rgb(251 146 60 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-orange-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -2932,6 +2937,16 @@ video {
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-orange-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-orange-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(154 52 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:from-primary-600:hover {
|
||||
--tw-gradient-from: #d68338 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position);
|
||||
@@ -3842,6 +3857,11 @@ video {
|
||||
background-color: rgb(127 29 29 / 0.5);
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-orange-700:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:from-primary-500:hover:is(.dark *) {
|
||||
--tw-gradient-from: #f49541 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position);
|
||||
|
||||
Reference in New Issue
Block a user