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 @@
分组
@@ -358,8 +364,14 @@
账户密码
@@ -572,7 +584,13 @@
OAuth
@@ -603,13 +621,23 @@
邮件接收
@@ -683,12 +711,24 @@
@@ -730,7 +770,13 @@
@@ -760,7 +806,13 @@
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'/);
+});