From afd686f81b9a9f97fa42bebb996e88217f89cf2b Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 4 Feb 2025 16:26:30 +0100 Subject: [PATCH] Update credential row UI (#541) --- .../chrome/src/contentScript.ts | 386 ++++++++++++------ 1 file changed, 251 insertions(+), 135 deletions(-) diff --git a/browser-extensions/chrome/src/contentScript.ts b/browser-extensions/chrome/src/contentScript.ts index 7c7a4ba08..727dd5cf1 100644 --- a/browser-extensions/chrome/src/contentScript.ts +++ b/browser-extensions/chrome/src/contentScript.ts @@ -202,128 +202,9 @@ function createPopup(input: HTMLInputElement, credentials: Credential[]) : void document.title ); - // Add credentials to popup if any matches found - if (filteredCredentials.length > 0) { - filteredCredentials.forEach(cred => { - const item = document.createElement('div'); - item.style.cssText = ` - padding: 8px 16px; - cursor: pointer; - display: flex; - align-items: center; - gap: 8px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; - transition: background-color 0.2s ease; - border-radius: 4px; - margin: 0 4px; - `; - - // Create container for credential info (logo + username) - const credentialInfo = document.createElement('div'); - credentialInfo.style.cssText = ` - display: flex; - align-items: center; - gap: 8px; - flex-grow: 1; - padding: 4px; - border-radius: 4px; - transition: background-color 0.2s ease; - `; - - const imgElement = document.createElement('img'); - imgElement.style.width = '16px'; - imgElement.style.height = '16px'; - - // Handle base64 image data - if (cred.Logo) { - try { - const base64Logo = base64Encode(cred.Logo); - imgElement.src = `data:image/x-icon;base64,${base64Logo}`; - } catch (error) { - console.error('Error setting logo:', error); - imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`; - } - } else { - imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`; - } - - credentialInfo.appendChild(imgElement); - credentialInfo.appendChild(document.createTextNode(cred.Username)); - - // Add popout icon - const popoutIcon = document.createElement('div'); - popoutIcon.style.cssText = ` - display: flex; - align-items: center; - padding: 4px; - opacity: 0.6; - border-radius: 4px; - `; - popoutIcon.innerHTML = ` - - - - - - `; - - // Add hover effects - popoutIcon.addEventListener('mouseenter', () => { - popoutIcon.style.opacity = '1'; - popoutIcon.style.backgroundColor = isDarkMode() ? '#ffffff' : '#000000'; - popoutIcon.style.color = isDarkMode() ? '#000000' : '#ffffff'; - }); - - popoutIcon.addEventListener('mouseleave', () => { - popoutIcon.style.opacity = '0.6'; - popoutIcon.style.backgroundColor = 'transparent'; - popoutIcon.style.color = isDarkMode() ? '#ffffff' : '#000000'; - }); - - // Handle popout click - popoutIcon.addEventListener('click', (e) => { - e.stopPropagation(); // Prevent credential fill - chrome.runtime.sendMessage({ - type: 'OPEN_POPUP_WITH_CREDENTIAL', - credentialId: cred.Id - }); - removeExistingPopup(); - }); - - item.appendChild(credentialInfo); - item.appendChild(popoutIcon); - - // Update hover effect for the entire item - item.addEventListener('mouseenter', () => { - item.style.backgroundColor = isDarkMode() ? '#2d3748' : '#f3f4f6'; - popoutIcon.style.opacity = '1'; - }); - - item.addEventListener('mouseleave', () => { - item.style.backgroundColor = 'transparent'; - popoutIcon.style.opacity = '0.6'; - }); - - // Update click handler to only trigger on credentialInfo - credentialInfo.addEventListener('click', () => { - fillCredential(cred); - removeExistingPopup(); - }); - - popup.appendChild(item); - }); - } else { - // Show "no matches found" message - const noMatches = document.createElement('div'); - noMatches.style.cssText = ` - padding: 8px 16px; - color: ${isDarkMode() ? '#9ca3af' : '#6b7280'}; - font-style: italic; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; - `; - noMatches.textContent = 'No matches found'; - popup.appendChild(noMatches); - } + // Add credentials to popup using the shared function + const credentialElements = createCredentialList(filteredCredentials, input); + credentialElements.forEach(element => popup.appendChild(element)); // Add divider const divider = document.createElement('div'); @@ -363,7 +244,7 @@ function createPopup(input: HTMLInputElement, credentials: Credential[]) : void - New Alias + New `; createButton.addEventListener('click', async () => { const serviceName = await createEditNamePopup(document.title); @@ -437,21 +318,59 @@ function createPopup(input: HTMLInputElement, credentials: Credential[]) : void `; setTimeout(() => { removeExistingPopup(); - }, 2000); + }, 200); } }); - // Search button - const searchButton = document.createElement('button'); - searchButton.style.cssText = createButton.style.cssText; - searchButton.innerHTML = ` - - - - - Search + // Create search input instead of button + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = 'Search vault...'; + searchInput.style.cssText = ` + flex: 2; + padding: 6px 12px; + border-radius: 4px; + background: ${isDarkMode() ? '#374151' : '#f3f4f6'}; + color: ${isDarkMode() ? '#e5e7eb' : '#374151'}; + font-size: 14px; + border: 1px solid ${isDarkMode() ? '#4b5563' : '#e5e7eb'}; + outline: none; `; + // Add focus styles + searchInput.addEventListener('focus', () => { + searchInput.style.borderColor = '#2563eb'; + searchInput.style.boxShadow = '0 0 0 2px rgba(37, 99, 235, 0.2)'; + }); + + searchInput.addEventListener('blur', () => { + searchInput.style.borderColor = isDarkMode() ? '#4b5563' : '#e5e7eb'; + searchInput.style.boxShadow = 'none'; + }); + + // Handle search input + let searchTimeout: NodeJS.Timeout; + searchInput.addEventListener('input', () => { + clearTimeout(searchTimeout); + const searchTerm = searchInput.value.toLowerCase(); + + // Request credentials from background script + chrome.runtime.sendMessage({ type: 'GET_CREDENTIALS_FOR_URL', url: window.location.href }, (response: CredentialResponse) => { + if (response.status === 'OK' && response.credentials) { + // Filter credentials based on search term + const filteredCredentials = response.credentials.filter(cred => + cred.ServiceName.toLowerCase().includes(searchTerm) || + cred.Username.toLowerCase().includes(searchTerm) || + cred.Email.toLowerCase().includes(searchTerm) || + cred.ServiceUrl?.toLowerCase().includes(searchTerm) + ); + + // Update popup content with filtered results + updatePopupContent(popup, filteredCredentials, input); + } + }); + }); + // Close button const closeButton = document.createElement('button'); closeButton.style.cssText = ` @@ -477,8 +396,8 @@ function createPopup(input: HTMLInputElement, credentials: Credential[]) : void removeExistingPopup(); }); + actionContainer.appendChild(searchInput); actionContainer.appendChild(createButton); - actionContainer.appendChild(searchButton); actionContainer.appendChild(closeButton); popup.appendChild(actionContainer); @@ -1108,4 +1027,201 @@ urlObserver.observe(document.body, { // Also listen for popstate events (back/forward navigation) window.addEventListener('popstate', () => { removeExistingPopup(); -}); \ No newline at end of file +}); + +/** + * Create credential list content for popup + */ +function createCredentialList(credentials: Credential[], input: HTMLInputElement): HTMLElement[] { + const elements: HTMLElement[] = []; + + if (credentials.length > 0) { + credentials.forEach(cred => { + const item = document.createElement('div'); + item.style.cssText = ` + padding: 8px 16px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; + transition: background-color 0.2s ease; + border-radius: 4px; + margin: 0 4px; + `; + + // Create container for credential info (logo + username) + const credentialInfo = document.createElement('div'); + credentialInfo.style.cssText = ` + display: flex; + align-items: center; + gap: 16px; + flex-grow: 1; + padding: 4px; + border-radius: 4px; + transition: background-color 0.2s ease; + `; + + const imgElement = document.createElement('img'); + imgElement.style.width = '20px'; + imgElement.style.height = '20px'; + + // Handle base64 image data + if (cred.Logo) { + try { + const base64Logo = base64Encode(cred.Logo); + imgElement.src = `data:image/x-icon;base64,${base64Logo}`; + } catch (error) { + console.error('Error setting logo:', error); + imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`; + } + } else { + imgElement.src = `data:image/x-icon;base64,${placeholderBase64}`; + } + + credentialInfo.appendChild(imgElement); + const credTextContainer = document.createElement('div'); + credTextContainer.style.cssText = ` + display: flex; + flex-direction: column; + flex-grow: 1; + min-width: 0; /* Enable text truncation in flex container */ + `; + + // Service name (primary text) + const serviceName = document.createElement('div'); + serviceName.style.cssText = ` + font-weight: 500; + white-space: nowrap; + overflow: hidden; + font-size: 14px; + text-overflow: ellipsis; + color: ${isDarkMode() ? '#f3f4f6' : '#111827'}; + `; + serviceName.textContent = cred.ServiceName; + + // Details container (secondary text) + const detailsContainer = document.createElement('div'); + detailsContainer.style.cssText = ` + font-size: 0.85em; + white-space: nowrap; + overflow: hidden; + font-size: 12px; + text-overflow: ellipsis; + color: ${isDarkMode() ? '#9ca3af' : '#6b7280'}; + `; + + // Combine full name (if available) and username + const details = []; + if (cred.Alias?.FirstName && cred.Alias?.LastName) { + details.push(`${cred.Alias.FirstName} ${cred.Alias.LastName}`); + } + details.push(cred.Username); + detailsContainer.textContent = details.join(' ยท '); + + credTextContainer.appendChild(serviceName); + credTextContainer.appendChild(detailsContainer); + credentialInfo.appendChild(credTextContainer); + + // Add popout icon + const popoutIcon = document.createElement('div'); + popoutIcon.style.cssText = ` + display: flex; + align-items: center; + padding: 4px; + opacity: 0.6; + border-radius: 4px; + `; + popoutIcon.innerHTML = ` + + + + + + `; + + // Add hover effects + popoutIcon.addEventListener('mouseenter', () => { + popoutIcon.style.opacity = '1'; + popoutIcon.style.backgroundColor = isDarkMode() ? '#ffffff' : '#000000'; + popoutIcon.style.color = isDarkMode() ? '#000000' : '#ffffff'; + }); + + popoutIcon.addEventListener('mouseleave', () => { + popoutIcon.style.opacity = '0.6'; + popoutIcon.style.backgroundColor = 'transparent'; + popoutIcon.style.color = isDarkMode() ? '#ffffff' : '#000000'; + }); + + // Handle popout click + popoutIcon.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent credential fill + chrome.runtime.sendMessage({ + type: 'OPEN_POPUP_WITH_CREDENTIAL', + credentialId: cred.Id + }); + removeExistingPopup(); + }); + + item.appendChild(credentialInfo); + item.appendChild(popoutIcon); + + // Update hover effect for the entire item + item.addEventListener('mouseenter', () => { + item.style.backgroundColor = isDarkMode() ? '#2d3748' : '#f3f4f6'; + popoutIcon.style.opacity = '1'; + }); + + item.addEventListener('mouseleave', () => { + item.style.backgroundColor = 'transparent'; + popoutIcon.style.opacity = '0.6'; + }); + + // Update click handler to only trigger on credentialInfo + credentialInfo.addEventListener('click', () => { + fillCredential(cred); + removeExistingPopup(); + }); + + elements.push(item); + }); + } else { + const noMatches = document.createElement('div'); + noMatches.style.cssText = ` + padding: 8px 16px; + color: ${isDarkMode() ? '#9ca3af' : '#6b7280'}; + font-style: italic; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; + `; + noMatches.textContent = 'No matches found'; + elements.push(noMatches); + } + + return elements; +} + +// Update updatePopupContent to use the new function +function updatePopupContent(popup: HTMLElement, credentials: Credential[], input: HTMLInputElement) { + // Store the action container + const actionContainer = popup.lastElementChild; + + // Clear all content except the action container + while (popup.firstChild && popup.firstChild !== actionContainer) { + popup.removeChild(popup.firstChild); + } + + // Add credentials using the shared function + const credentialElements = createCredentialList(credentials, input); + credentialElements.forEach(element => { + popup.insertBefore(element, actionContainer); + }); + + // Add divider before action container + const divider = document.createElement('div'); + divider.style.cssText = ` + height: 1px; + background: ${isDarkMode() ? '#374151' : '#e5e7eb'}; + margin: 8px 0; + `; + popup.insertBefore(divider, actionContainer); +} \ No newline at end of file