feat(sidepanel): 添加密码可见性切换功能及相关测试

This commit is contained in:
QLHazyCoder
2026-04-28 17:01:06 +08:00
parent 8007a99b72
commit 93acb557ce
5 changed files with 197 additions and 16 deletions
+39 -3
View File
@@ -1,4 +1,15 @@
(function attachSidepanelFormDialog(globalScope) {
const FORM_EYE_OPEN_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
const FORM_EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
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) => {
+4
View File
@@ -2747,6 +2747,10 @@ header {
width: 100%;
}
.modal-form-row .input-with-icon {
width: 100%;
}
.modal-form-alert {
margin-top: -4px;
margin-bottom: 14px;
+65 -13
View File
@@ -184,7 +184,13 @@
</div>
<div class="data-row" id="row-sub2api-password" style="display:none;">
<span class="data-label">密码</span>
<input type="password" id="input-sub2api-password" class="data-input" placeholder="请输入 SUB2API 登录密码" />
<div class="input-with-icon">
<input type="password" id="input-sub2api-password" class="data-input data-input-with-icon"
placeholder="请输入 SUB2API 登录密码" />
<button id="btn-toggle-sub2api-password" class="input-icon-btn" type="button"
data-password-toggle="input-sub2api-password" data-show-label="显示 SUB2API 密码"
data-hide-label="隐藏 SUB2API 密码" aria-label="显示 SUB2API 密码" title="显示 SUB2API 密码"></button>
</div>
</div>
<div class="data-row" id="row-sub2api-group" style="display:none;">
<span class="data-label">分组</span>
@@ -358,8 +364,14 @@
</div>
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
<span class="data-label">管理密钥</span>
<input type="password" id="input-codex2api-admin-key" class="data-input"
placeholder="请输入 Codex2API Admin Secret" />
<div class="input-with-icon">
<input type="password" id="input-codex2api-admin-key" class="data-input data-input-with-icon"
placeholder="请输入 Codex2API Admin Secret" />
<button id="btn-toggle-codex2api-admin-key" class="input-icon-btn" type="button"
data-password-toggle="input-codex2api-admin-key" data-show-label="显示 Codex2API 管理密钥"
data-hide-label="隐藏 Codex2API 管理密钥" aria-label="显示 Codex2API 管理密钥"
title="显示 Codex2API 管理密钥"></button>
</div>
</div>
<div class="data-row" id="row-custom-password">
<span class="data-label">账户密码</span>
@@ -572,7 +584,13 @@
</div>
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
<span class="data-label">接码 API</span>
<input type="password" id="input-hero-sms-api-key" class="data-input mono" placeholder="请输入 HeroSMS API Key" />
<div class="input-with-icon">
<input type="password" id="input-hero-sms-api-key" class="data-input data-input-with-icon mono"
placeholder="请输入 HeroSMS API Key" />
<button id="btn-toggle-hero-sms-api-key" class="input-icon-btn" type="button"
data-password-toggle="input-hero-sms-api-key" data-show-label="显示 HeroSMS API Key"
data-hide-label="隐藏 HeroSMS API Key" aria-label="显示 HeroSMS API Key" title="显示 HeroSMS API Key"></button>
</div>
</div>
<div class="data-row">
<span class="data-label">OAuth</span>
@@ -603,13 +621,23 @@
</div>
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
<span class="data-label">Admin Auth</span>
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
placeholder="Cloudflare Temp Email admin password" />
<div class="input-with-icon">
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
placeholder="Cloudflare Temp Email admin password" />
<button id="btn-toggle-temp-email-admin-auth" class="input-icon-btn" type="button"
data-password-toggle="input-temp-email-admin-auth" data-show-label="显示 Admin Auth"
data-hide-label="隐藏 Admin Auth" aria-label="显示 Admin Auth" title="显示 Admin Auth"></button>
</div>
</div>
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
<span class="data-label">Custom Auth</span>
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
<div class="input-with-icon">
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
<button id="btn-toggle-temp-email-custom-auth" class="input-icon-btn" type="button"
data-password-toggle="input-temp-email-custom-auth" data-show-label="显示 Custom Auth"
data-hide-label="隐藏 Custom Auth" aria-label="显示 Custom Auth" title="显示 Custom Auth"></button>
</div>
</div>
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
<span class="data-label">邮件接收</span>
@@ -683,12 +711,24 @@
</div>
<div class="data-row">
<span class="data-label">邮箱密码</span>
<input type="password" id="input-hotmail-password" class="data-input" placeholder="可选,仅用于记录" />
<div class="input-with-icon">
<input type="password" id="input-hotmail-password" class="data-input data-input-with-icon"
placeholder="可选,仅用于记录" />
<button id="btn-toggle-hotmail-password" class="input-icon-btn" type="button"
data-password-toggle="input-hotmail-password" data-show-label="显示 Hotmail 密码"
data-hide-label="隐藏 Hotmail 密码" aria-label="显示 Hotmail 密码" title="显示 Hotmail 密码"></button>
</div>
</div>
<div class="data-row">
<span class="data-label">刷新令牌</span>
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
placeholder="必填,粘贴刷新令牌(refresh token" />
<div class="input-with-icon">
<input type="password" id="input-hotmail-refresh-token" class="data-input data-input-with-icon mono"
placeholder="必填,粘贴刷新令牌(refresh token" />
<button id="btn-toggle-hotmail-refresh-token" class="input-icon-btn" type="button"
data-password-toggle="input-hotmail-refresh-token" data-show-label="显示 Hotmail 刷新令牌"
data-hide-label="隐藏 Hotmail 刷新令牌" aria-label="显示 Hotmail 刷新令牌"
title="显示 Hotmail 刷新令牌"></button>
</div>
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
@@ -730,7 +770,13 @@
</div>
<div class="data-row">
<span class="data-label">密码</span>
<input type="password" id="input-mail2925-password" class="data-input" placeholder="2925 登录密码" />
<div class="input-with-icon">
<input type="password" id="input-mail2925-password" class="data-input data-input-with-icon"
placeholder="2925 登录密码" />
<button id="btn-toggle-mail2925-password" class="input-icon-btn" type="button"
data-password-toggle="input-mail2925-password" data-show-label="显示 2925 密码"
data-hide-label="隐藏 2925 密码" aria-label="显示 2925 密码" title="显示 2925 密码"></button>
</div>
</div>
<div class="data-row hotmail-actions-row">
<span class="data-label"></span>
@@ -760,7 +806,13 @@
</div>
<div class="data-row">
<span class="data-label">API Key</span>
<input type="password" id="input-luckmail-api-key" class="data-input mono" placeholder="请输入 LuckMail API Key" />
<div class="input-with-icon">
<input type="password" id="input-luckmail-api-key" class="data-input data-input-with-icon mono"
placeholder="请输入 LuckMail API Key" />
<button id="btn-toggle-luckmail-api-key" class="input-icon-btn" type="button"
data-password-toggle="input-luckmail-api-key" data-show-label="显示 LuckMail API Key"
data-hide-label="隐藏 LuckMail API Key" aria-label="显示 LuckMail API Key" title="显示 LuckMail API Key"></button>
</div>
</div>
<div class="data-row">
<span class="data-label">Base URL</span>
+50
View File
@@ -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);
@@ -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(/<input\b[^>]*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'/);
});