From f3dabc3a393d530708ebe0cd784e5d61b4572e4b Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 18 Sep 2025 13:46:21 +0200 Subject: [PATCH] Update last email/username placeholder to work like suggestions (#1247) --- .../src/entrypoints/contentScript/Popup.ts | 174 ++++++++++++++---- .../src/entrypoints/contentScript/style.css | 56 ++++++ apps/browser-extension/src/utils/Constants.ts | 4 +- 3 files changed, 197 insertions(+), 37 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts index fe867c672..b2ba444b1 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts @@ -3,7 +3,7 @@ import { sendMessage } from 'webext-bridge/content-script'; import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/Filter'; import { fillCredential } from '@/entrypoints/contentScript/Form'; -import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY, AUTOFILL_MATCHING_MODE_KEY } from '@/utils/Constants'; +import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants'; import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator'; import type { Credential } from '@/utils/dist/shared/models/vault'; import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator'; @@ -762,9 +762,9 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon // Close existing popup removeExistingPopup(rootContainer); - // Load last used values - const lastEmail = await storage.getItem(LAST_CUSTOM_EMAIL_KEY) as string ?? ''; - const lastUsername = await storage.getItem(LAST_CUSTOM_USERNAME_KEY) as string ?? ''; + // Load history + const emailHistory = await storage.getItem(CUSTOM_EMAIL_HISTORY_KEY) as string[] ?? []; + const usernameHistory = await storage.getItem(CUSTOM_USERNAME_HISTORY_KEY) as string[] ?? []; return new Promise((resolve) => { (async (): Promise => { @@ -888,8 +888,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon id="custom-email" class="av-create-popup-input" placeholder="${enterEmailAddressText}" - data-default-value="${lastEmail}" > +
@@ -898,8 +898,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon id="custom-username" class="av-create-popup-input" placeholder="${enterUsernameText}" - data-default-value="${lastUsername}" > +
@@ -970,41 +970,141 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon const passwordPreview = popup.querySelector('#password-preview') as HTMLInputElement; const regenerateBtn = popup.querySelector('#regenerate-password') as HTMLButtonElement; const toggleVisibilityBtn = popup.querySelector('#toggle-password-visibility') as HTMLButtonElement; + const emailSuggestions = popup.querySelector('#email-suggestions') as HTMLElement; + const usernameSuggestions = popup.querySelector('#username-suggestions') as HTMLElement; /** - * Setup default value for input with placeholder styling. + * Update history with new value (max 2 unique entries) */ - const setupDefaultValue = (input: HTMLInputElement) : void => { - const defaultValue = input.dataset.defaultValue; - if (defaultValue) { - input.value = defaultValue; - input.classList.add('av-create-popup-input-default'); + const updateHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY, maxItems: number = 2): Promise => { + const history = await storage.getItem(historyKey) as string[] ?? []; + + // Remove the value if it already exists + const filteredHistory = history.filter((item: string) => item !== value); + + // Add the new value at the beginning + if (value.trim()) { + filteredHistory.unshift(value); } + + // Keep only the first maxItems + const updatedHistory = filteredHistory.slice(0, maxItems); + + // Save the updated history + await storage.setItem(historyKey, updatedHistory); + + return updatedHistory; }; - setupDefaultValue(customEmail); - setupDefaultValue(customUsername); + /** + * Remove item from history + */ + const removeFromHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY): Promise => { + const history = await storage.getItem(historyKey) as string[] ?? []; + const updatedHistory = history.filter((item: string) => item !== value); + await storage.setItem(historyKey, updatedHistory); + return updatedHistory; + }; - // Handle input changes - customEmail.addEventListener('input', () => { - const value = customEmail.value.trim(); - if (value || value === '') { - customEmail.classList.remove('av-create-popup-input-default'); - storage.setItem(LAST_CUSTOM_EMAIL_KEY, value); + /** + * Format suggestions HTML as pill-style buttons + */ + const formatSuggestionsHtml = async (history: string[], currentValue: string): Promise => { + // Filter out the current value from history and limit to 2 items + const filteredHistory = history + .filter(item => item.toLowerCase() !== currentValue.toLowerCase()) + .slice(0, 2); + + if (filteredHistory.length === 0) { + return ''; + } + + // Build HTML with pill-style buttons + return filteredHistory.map(item => + ` + ${item} + × + ` + ).join(' '); + }; + + /** + * Update suggestions display + */ + const updateSuggestions = async (input: HTMLInputElement, suggestionsContainer: HTMLElement, history: string[]): Promise => { + const currentValue = input.value.trim(); + const html = await formatSuggestionsHtml(history, currentValue); + suggestionsContainer.innerHTML = html; + suggestionsContainer.style.display = html ? 'flex' : 'none'; + }; + + // Initial display of suggestions + await updateSuggestions(customEmail, emailSuggestions, emailHistory); + await updateSuggestions(customUsername, usernameSuggestions, usernameHistory); + + // Handle email input + customEmail.addEventListener('input', async () => { + await updateSuggestions(customEmail, emailSuggestions, emailHistory); + }); + + // Handle username input + customUsername.addEventListener('input', async () => { + await updateSuggestions(customUsername, usernameSuggestions, usernameHistory); + }); + + // Handle suggestion clicks for email + emailSuggestions.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const target = e.target as HTMLElement; + + // Check if delete button was clicked + if (target.classList.contains('av-suggestion-pill-delete')) { + const value = target.dataset.value; + if (value) { + const updatedHistory = await removeFromHistory(value, CUSTOM_EMAIL_HISTORY_KEY); + emailHistory.splice(0, emailHistory.length, ...updatedHistory); + await updateSuggestions(customEmail, emailSuggestions, emailHistory); + } } else { - customEmail.classList.add('av-create-popup-input-default'); - storage.setItem(LAST_CUSTOM_EMAIL_KEY, ''); + // Check if pill or pill text was clicked + let pillElement = target.closest('.av-suggestion-pill') as HTMLElement; + if (pillElement) { + const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement; + const value = textElement?.dataset.value; + if (value) { + customEmail.value = value; + await updateSuggestions(customEmail, emailSuggestions, emailHistory); + } + } } }); - customUsername.addEventListener('input', () => { - const value = customUsername.value.trim(); - if (value || value === '') { - customUsername.classList.remove('av-create-popup-input-default'); - storage.setItem(LAST_CUSTOM_USERNAME_KEY, value); + // Handle suggestion clicks for username + usernameSuggestions.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const target = e.target as HTMLElement; + + // Check if delete button was clicked + if (target.classList.contains('av-suggestion-pill-delete')) { + const value = target.dataset.value; + if (value) { + const updatedHistory = await removeFromHistory(value, CUSTOM_USERNAME_HISTORY_KEY); + usernameHistory.splice(0, usernameHistory.length, ...updatedHistory); + await updateSuggestions(customUsername, usernameSuggestions, usernameHistory); + } } else { - customUsername.classList.add('av-create-popup-input-default'); - storage.setItem(LAST_CUSTOM_USERNAME_KEY, ''); + // Check if pill or pill text was clicked + let pillElement = target.closest('.av-suggestion-pill') as HTMLElement; + if (pillElement) { + const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement; + const value = textElement?.dataset.value; + if (value) { + customUsername.value = value; + await updateSuggestions(customUsername, usernameSuggestions, usernameHistory); + } + } } }); @@ -1372,12 +1472,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon if (serviceName) { const email = customEmail.value.trim(); const username = customUsername.value.trim(); - const hasDefaultEmail = customEmail.classList.contains('av-create-popup-input-default'); - const hasDefaultUsername = customUsername.classList.contains('av-create-popup-input-default'); - - // If using default values, use the dataset values - const finalEmail = hasDefaultEmail ? customEmail.dataset.defaultValue : email; - const finalUsername = hasDefaultUsername ? customUsername.dataset.defaultValue : username; + const finalEmail = email; + const finalUsername = username; if (!finalEmail && !finalUsername) { // Add error styling to fields @@ -1424,6 +1520,14 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon return; } + // Update history when saving + if (finalEmail) { + await updateHistory(finalEmail, CUSTOM_EMAIL_HISTORY_KEY); + } + if (finalUsername) { + await updateHistory(finalUsername, CUSTOM_USERNAME_HISTORY_KEY); + } + closePopup({ serviceName, isCustomCredential: true, diff --git a/apps/browser-extension/src/entrypoints/contentScript/style.css b/apps/browser-extension/src/entrypoints/contentScript/style.css index eb4f48231..5fa946e06 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/style.css +++ b/apps/browser-extension/src/entrypoints/contentScript/style.css @@ -539,6 +539,62 @@ body { box-shadow: 0 0 0 1px #ef4444 !important; } +/* Field Suggestions - Pill Style */ +.av-field-suggestions { + margin-top: 8px; + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.av-suggestion-pill { + display: inline-flex; + align-items: center; + background: #4b5563; + border: 1px solid #6b7280; + border-radius: 16px; + padding: 4px 8px 4px 12px; + font-size: 13px; + color: #e5e7eb; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; +} + +.av-suggestion-pill:hover { + background: #6b7280; + border-color: #9ca3af; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.av-suggestion-pill-text { + display: inline-block; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.av-suggestion-pill-delete { + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 6px; + padding: 0 2px; + color: #9ca3af; + font-size: 16px; + font-weight: bold; + cursor: pointer; + transition: color 0.2s; + border-left: 1px solid #6b7280; + padding-left: 6px; +} + +.av-suggestion-pill-delete:hover { + color: #ef4444; +} + .av-create-popup-error-text { color: #ef4444; font-size: 0.875rem; diff --git a/apps/browser-extension/src/utils/Constants.ts b/apps/browser-extension/src/utils/Constants.ts index c1166f7c0..6ea4e113e 100644 --- a/apps/browser-extension/src/utils/Constants.ts +++ b/apps/browser-extension/src/utils/Constants.ts @@ -9,5 +9,5 @@ export const AUTO_LOCK_TIMEOUT_KEY = 'local:aliasvault_auto_lock_timeout'; export const AUTOFILL_MATCHING_MODE_KEY = 'local:aliasvault_autofill_matching_mode'; // TODO: store these settings in the actual vault when updating the datamodel for roadmap v1.0. -export const LAST_CUSTOM_EMAIL_KEY = 'local:aliasvault_last_custom_email'; -export const LAST_CUSTOM_USERNAME_KEY = 'local:aliasvault_last_custom_username'; \ No newline at end of file +export const CUSTOM_EMAIL_HISTORY_KEY = 'local:aliasvault_custom_email_history'; +export const CUSTOM_USERNAME_HISTORY_KEY = 'local:aliasvault_custom_username_history';