feat(sidepanel): 添加密码可见性切换功能及相关测试
This commit is contained in:
@@ -1,4 +1,15 @@
|
|||||||
(function attachSidepanelFormDialog(globalScope) {
|
(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 = {}) {
|
function createFormDialog(context = {}) {
|
||||||
const {
|
const {
|
||||||
overlay = null,
|
overlay = null,
|
||||||
@@ -62,7 +73,8 @@
|
|||||||
|
|
||||||
const label = documentRef.createElement('label');
|
const label = documentRef.createElement('label');
|
||||||
label.className = 'modal-form-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);
|
wrapper.appendChild(label);
|
||||||
|
|
||||||
let input = null;
|
let input = null;
|
||||||
@@ -82,7 +94,9 @@
|
|||||||
} else {
|
} else {
|
||||||
input = documentRef.createElement('input');
|
input = documentRef.createElement('input');
|
||||||
input.type = field.type === 'password' ? 'password' : 'text';
|
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)
|
const normalizedValue = Object.prototype.hasOwnProperty.call(values, field.key)
|
||||||
@@ -106,7 +120,29 @@
|
|||||||
input.dataset.fieldKey = String(field.key || '');
|
input.dataset.fieldKey = String(field.key || '');
|
||||||
label.htmlFor = field.key;
|
label.htmlFor = field.key;
|
||||||
input.id = 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') {
|
if (field.type !== 'textarea') {
|
||||||
input.addEventListener('keydown', (event) => {
|
input.addEventListener('keydown', (event) => {
|
||||||
|
|||||||
@@ -2747,6 +2747,10 @@ header {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-form-row .input-with-icon {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-form-alert {
|
.modal-form-alert {
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
|||||||
+65
-13
@@ -184,7 +184,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-sub2api-password" style="display:none;">
|
<div class="data-row" id="row-sub2api-password" style="display:none;">
|
||||||
<span class="data-label">密码</span>
|
<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>
|
||||||
<div class="data-row" id="row-sub2api-group" style="display:none;">
|
<div class="data-row" id="row-sub2api-group" style="display:none;">
|
||||||
<span class="data-label">分组</span>
|
<span class="data-label">分组</span>
|
||||||
@@ -358,8 +364,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
|
<div class="data-row" id="row-codex2api-admin-key" style="display:none;">
|
||||||
<span class="data-label">管理密钥</span>
|
<span class="data-label">管理密钥</span>
|
||||||
<input type="password" id="input-codex2api-admin-key" class="data-input"
|
<div class="input-with-icon">
|
||||||
placeholder="请输入 Codex2API Admin Secret" />
|
<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>
|
||||||
<div class="data-row" id="row-custom-password">
|
<div class="data-row" id="row-custom-password">
|
||||||
<span class="data-label">账户密码</span>
|
<span class="data-label">账户密码</span>
|
||||||
@@ -572,7 +584,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
<div class="data-row" id="row-hero-sms-api-key" style="display:none;">
|
||||||
<span class="data-label">接码 API</span>
|
<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>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">OAuth</span>
|
<span class="data-label">OAuth</span>
|
||||||
@@ -603,13 +621,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
<div class="data-row" id="row-temp-email-admin-auth" style="display:none;">
|
||||||
<span class="data-label">Admin Auth</span>
|
<span class="data-label">Admin Auth</span>
|
||||||
<input type="password" id="input-temp-email-admin-auth" class="data-input data-input-with-icon"
|
<div class="input-with-icon">
|
||||||
placeholder="Cloudflare Temp Email admin password" />
|
<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>
|
||||||
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
<div class="data-row" id="row-temp-email-custom-auth" style="display:none;">
|
||||||
<span class="data-label">Custom Auth</span>
|
<span class="data-label">Custom Auth</span>
|
||||||
<input type="password" id="input-temp-email-custom-auth" class="data-input data-input-with-icon"
|
<div class="input-with-icon">
|
||||||
placeholder="仅当站点启用了访问密码时再填写;这是额外鉴权,不替代 Admin Auth。" />
|
<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>
|
||||||
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
<div class="data-row" id="row-temp-email-receive-mailbox" style="display:none;">
|
||||||
<span class="data-label">邮件接收</span>
|
<span class="data-label">邮件接收</span>
|
||||||
@@ -683,12 +711,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">邮箱密码</span>
|
<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>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">刷新令牌</span>
|
<span class="data-label">刷新令牌</span>
|
||||||
<input type="password" id="input-hotmail-refresh-token" class="data-input mono"
|
<div class="input-with-icon">
|
||||||
placeholder="必填,粘贴刷新令牌(refresh token)" />
|
<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>
|
||||||
<div class="data-row hotmail-actions-row">
|
<div class="data-row hotmail-actions-row">
|
||||||
<span class="data-label"></span>
|
<span class="data-label"></span>
|
||||||
@@ -730,7 +770,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">密码</span>
|
<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>
|
||||||
<div class="data-row hotmail-actions-row">
|
<div class="data-row hotmail-actions-row">
|
||||||
<span class="data-label"></span>
|
<span class="data-label"></span>
|
||||||
@@ -760,7 +806,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">API Key</span>
|
<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>
|
||||||
<div class="data-row">
|
<div class="data-row">
|
||||||
<span class="data-label">Base URL</span>
|
<span class="data-label">Base URL</span>
|
||||||
|
|||||||
@@ -4524,6 +4524,54 @@ function syncToggleButtonLabel(button, input, labels) {
|
|||||||
button.title = isHidden ? labels.show : labels.hide;
|
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) {
|
async function copyTextToClipboard(text) {
|
||||||
const value = String(text || '').trim();
|
const value = String(text || '').trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -6981,6 +7029,7 @@ document.addEventListener('scroll', () => {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
initializeManualStepActions();
|
initializeManualStepActions();
|
||||||
|
bindPasswordVisibilityToggles();
|
||||||
initTheme();
|
initTheme();
|
||||||
initHotmailListExpandedState();
|
initHotmailListExpandedState();
|
||||||
initMail2925ListExpandedState();
|
initMail2925ListExpandedState();
|
||||||
@@ -7001,6 +7050,7 @@ loadHeroSmsCountries().catch((err) => {
|
|||||||
syncIpProxyApiUrlToggleLabel();
|
syncIpProxyApiUrlToggleLabel();
|
||||||
syncIpProxyUsernameToggleLabel();
|
syncIpProxyUsernameToggleLabel();
|
||||||
syncIpProxyPasswordToggleLabel();
|
syncIpProxyPasswordToggleLabel();
|
||||||
|
syncPasswordVisibilityToggles();
|
||||||
updatePanelModeUI();
|
updatePanelModeUI();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
updateStatusDisplay(latestState);
|
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'/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user