From 93acb557ce0a20e03283d99838e3f7bc5de19903 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 28 Apr 2026 17:01:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(sidepanel):=20=E6=B7=BB=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=8F=AF=E8=A7=81=E6=80=A7=E5=88=87=E6=8D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/form-dialog.js | 42 ++++++++++- sidepanel/sidepanel.css | 4 ++ sidepanel/sidepanel.html | 78 +++++++++++++++++---- sidepanel/sidepanel.js | 50 +++++++++++++ tests/sidepanel-password-visibility.test.js | 39 +++++++++++ 5 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 tests/sidepanel-password-visibility.test.js diff --git a/sidepanel/form-dialog.js b/sidepanel/form-dialog.js index cf3db7c..0e44133 100644 --- a/sidepanel/form-dialog.js +++ b/sidepanel/form-dialog.js @@ -1,4 +1,15 @@ (function attachSidepanelFormDialog(globalScope) { + const FORM_EYE_OPEN_ICON = ''; + const FORM_EYE_CLOSED_ICON = ''; + + function syncPasswordToggleButton(button, input, labels) { + if (!button || !input) return; + const isHidden = input.type === 'password'; + button.innerHTML = isHidden ? FORM_EYE_OPEN_ICON : FORM_EYE_CLOSED_ICON; + button.setAttribute('aria-label', isHidden ? labels.show : labels.hide); + button.title = isHidden ? labels.show : labels.hide; + } + function createFormDialog(context = {}) { const { overlay = null, @@ -62,7 +73,8 @@ const label = documentRef.createElement('label'); label.className = 'modal-form-label'; - label.textContent = String(field.label || field.key || '').trim(); + const labelText = String(field.label || field.key || '').trim(); + label.textContent = labelText; wrapper.appendChild(label); let input = null; @@ -82,7 +94,9 @@ } else { input = documentRef.createElement('input'); input.type = field.type === 'password' ? 'password' : 'text'; - input.className = 'data-input'; + input.className = field.type === 'password' + ? 'data-input data-input-with-icon' + : 'data-input'; } const normalizedValue = Object.prototype.hasOwnProperty.call(values, field.key) @@ -106,7 +120,29 @@ input.dataset.fieldKey = String(field.key || ''); label.htmlFor = field.key; input.id = field.key; - wrapper.appendChild(input); + if (field.type === 'password') { + const inputShell = documentRef.createElement('div'); + inputShell.className = 'input-with-icon'; + inputShell.appendChild(input); + + const toggleButton = documentRef.createElement('button'); + toggleButton.className = 'input-icon-btn'; + toggleButton.type = 'button'; + const labels = { + show: String(field.showPasswordLabel || `\u663e\u793a${labelText || '\u5bc6\u7801'}`), + hide: String(field.hidePasswordLabel || `\u9690\u85cf${labelText || '\u5bc6\u7801'}`), + }; + syncPasswordToggleButton(toggleButton, input, labels); + toggleButton.addEventListener('click', () => { + input.type = input.type === 'password' ? 'text' : 'password'; + syncPasswordToggleButton(toggleButton, input, labels); + }); + + inputShell.appendChild(toggleButton); + wrapper.appendChild(inputShell); + } else { + wrapper.appendChild(input); + } if (field.type !== 'textarea') { input.addEventListener('keydown', (event) => { diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index f3f3d94..afb0681 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2747,6 +2747,10 @@ header { width: 100%; } +.modal-form-row .input-with-icon { + width: 100%; +} + .modal-form-alert { margin-top: -4px; margin-bottom: 14px; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 5f7d6ab..5cc38cb 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -184,7 +184,13 @@
账户密码 @@ -572,7 +584,13 @@
OAuth @@ -603,13 +621,23 @@
邮箱密码 - +
+ + +
刷新令牌 - +
+ + +
@@ -730,7 +770,13 @@
密码 - +
+ + +
@@ -760,7 +806,13 @@
API Key - +
+ + +
Base URL diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index da8786a..79abb57 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -4524,6 +4524,54 @@ function syncToggleButtonLabel(button, input, labels) { button.title = isHidden ? labels.show : labels.hide; } +function getPasswordToggleLabels(button) { + if (!button) { + return { + show: '\u663e\u793a\u5185\u5bb9', + hide: '\u9690\u85cf\u5185\u5bb9', + }; + } + const show = button.dataset?.showLabel + || button.getAttribute('aria-label') + || button.title + || '\u663e\u793a\u5185\u5bb9'; + const hide = button.dataset?.hideLabel + || String(show).replace(/^\u663e\u793a/, '\u9690\u85cf') + || '\u9690\u85cf\u5185\u5bb9'; + return { show, hide }; +} + +function syncPasswordVisibilityToggle(button) { + const targetId = String(button?.dataset?.passwordToggle || '').trim(); + const input = targetId ? document.getElementById(targetId) : null; + if (!button || !input) return; + syncToggleButtonLabel(button, input, getPasswordToggleLabels(button)); +} + +function syncPasswordVisibilityToggles(root = document) { + root.querySelectorAll?.('[data-password-toggle]').forEach(syncPasswordVisibilityToggle); +} + +function bindPasswordVisibilityToggles(root = document) { + root.querySelectorAll?.('[data-password-toggle]').forEach((button) => { + if (button.dataset?.passwordToggleBound === 'true') { + syncPasswordVisibilityToggle(button); + return; + } + if (button.dataset) { + button.dataset.passwordToggleBound = 'true'; + } + syncPasswordVisibilityToggle(button); + button.addEventListener('click', () => { + const targetId = String(button.dataset?.passwordToggle || '').trim(); + const input = targetId ? document.getElementById(targetId) : null; + if (!input) return; + input.type = input.type === 'password' ? 'text' : 'password'; + syncPasswordVisibilityToggle(button); + }); + }); +} + async function copyTextToClipboard(text) { const value = String(text || '').trim(); if (!value) { @@ -6981,6 +7029,7 @@ document.addEventListener('scroll', () => { // ============================================================ initializeManualStepActions(); +bindPasswordVisibilityToggles(); initTheme(); initHotmailListExpandedState(); initMail2925ListExpandedState(); @@ -7001,6 +7050,7 @@ loadHeroSmsCountries().catch((err) => { syncIpProxyApiUrlToggleLabel(); syncIpProxyUsernameToggleLabel(); syncIpProxyPasswordToggleLabel(); + syncPasswordVisibilityToggles(); updatePanelModeUI(); updateButtonStates(); updateStatusDisplay(latestState); diff --git a/tests/sidepanel-password-visibility.test.js b/tests/sidepanel-password-visibility.test.js new file mode 100644 index 0000000..8bc2f25 --- /dev/null +++ b/tests/sidepanel-password-visibility.test.js @@ -0,0 +1,39 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +test('sidepanel password inputs expose visibility toggles', () => { + const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8'); + const passwordInputIds = Array.from( + html.matchAll(/]*type="password"[^>]*id="([^"]+)"/g), + (match) => match[1] + ); + const legacyToggleIds = new Map([ + ['input-vps-url', 'btn-toggle-vps-url'], + ['input-vps-password', 'btn-toggle-vps-password'], + ['input-ip-proxy-username', 'btn-toggle-ip-proxy-username'], + ['input-ip-proxy-password', 'btn-toggle-ip-proxy-password'], + ['input-ip-proxy-api-url', 'btn-toggle-ip-proxy-api-url'], + ['input-password', 'btn-toggle-password'], + ]); + + assert.ok(passwordInputIds.length > 0); + for (const inputId of passwordInputIds) { + const hasDataToggle = html.includes(`data-password-toggle="${inputId}"`); + const legacyToggleId = legacyToggleIds.get(inputId); + const hasLegacyToggle = legacyToggleId ? html.includes(`id="${legacyToggleId}"`) : false; + assert.equal( + hasDataToggle || hasLegacyToggle, + true, + `${inputId} should have a visibility toggle button` + ); + } +}); + +test('shared form dialog adds visibility toggles for password fields', () => { + const source = fs.readFileSync('sidepanel/form-dialog.js', 'utf8'); + + assert.match(source, /field\.type === 'password'[\s\S]*data-input-with-icon/); + assert.match(source, /syncPasswordToggleButton\(toggleButton,\s*input,\s*labels\)/); + assert.match(source, /input\.type = input\.type === 'password' \? 'text' : 'password'/); +});