修改添加通用组件

This commit is contained in:
QLHazyCoder
2026-05-05 03:35:19 +08:00
parent ee3be58088
commit c62e1746f8
7 changed files with 627 additions and 183 deletions
+228
View File
@@ -0,0 +1,228 @@
(function attachSidepanelEditableListPicker(globalScope) {
const editableListPickers = [];
function splitEditableListValues(value = '') {
return String(value || '')
.split(/[\r\n,,、]+/)
.map((name) => name.trim())
.filter(Boolean);
}
function normalizeEditableListValues(...sources) {
const values = [];
const seen = new Set();
const append = (value) => {
if (Array.isArray(value)) {
value.forEach(append);
return;
}
for (const item of splitEditableListValues(value)) {
const key = item.toLowerCase();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
values.push(item);
}
};
sources.forEach(append);
return values;
}
function createEditableListPicker(config = {}) {
const {
root,
input,
trigger,
current,
menu,
emptyLabel = '请先添加',
fallbackItems = [],
minItems = 0,
deleteLabel = '删除',
itemLabel = '项目',
normalizeItems = normalizeEditableListValues,
normalizeValue = (value) => String(value || '').trim(),
getItemValue = (item) => String(item || '').trim(),
getItemLabel = (item) => getItemValue(item),
getItemDeleteLabel = (item) => getItemLabel(item),
onDelete = null,
onDeleteError = null,
} = config;
const picker = {
root,
input,
trigger,
current,
menu,
items: [],
open: false,
};
const getFallbackItems = () => normalizeItems(fallbackItems);
const getNormalizedItemValue = (item) => normalizeValue(getItemValue(item));
const findItemByValue = (value) => {
const normalized = normalizeValue(value);
return picker.items.find((item) => getNormalizedItemValue(item) === normalized) || null;
};
const reportDeleteError = (error) => {
const fallbackMessage = `${deleteLabel}${itemLabel}失败。`;
if (typeof onDeleteError === 'function') {
onDeleteError(error, fallbackMessage);
return;
}
if (typeof globalScope.showToast === 'function') {
globalScope.showToast(error?.message || fallbackMessage, 'error');
}
};
picker.setOpen = (open) => {
picker.open = Boolean(open) && !trigger?.disabled;
if (menu) {
menu.hidden = !picker.open;
}
if (trigger) {
trigger.setAttribute('aria-expanded', picker.open ? 'true' : 'false');
trigger.classList?.toggle('is-open', picker.open);
}
};
picker.close = () => {
picker.setOpen(false);
};
picker.setVisible = (visible) => {
if (root) {
root.style.display = visible ? '' : 'none';
}
if (!visible) {
picker.close();
}
};
picker.setSelection = (value, options = {}) => {
const fallback = getNormalizedItemValue(picker.items[0]) || getNormalizedItemValue(getFallbackItems()[0]) || '';
const selected = normalizeValue(value) || fallback;
const selectedItem = findItemByValue(selected);
if (input) {
input.value = selected;
if (options.emit && typeof input.dispatchEvent === 'function') {
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if (current) {
current.textContent = selectedItem ? getItemLabel(selectedItem) : (selected || emptyLabel);
}
};
picker.render = (items = [], selectedValue = '') => {
const normalizedItems = normalizeItems(items);
picker.items = normalizedItems.length ? normalizedItems : getFallbackItems();
const inputValue = normalizeValue(input?.value);
const selected = normalizeValue(selectedValue)
|| (findItemByValue(inputValue) ? inputValue : '')
|| getNormalizedItemValue(picker.items[0])
|| '';
if (trigger) {
trigger.disabled = picker.items.length === 0;
}
if (picker.items.length === 0) {
if (menu) {
menu.innerHTML = '';
}
picker.setSelection('', { emit: false });
picker.close();
return;
}
if (
!menu
|| typeof menu.appendChild !== 'function'
|| typeof globalScope.document === 'undefined'
|| typeof globalScope.document.createElement !== 'function'
) {
picker.setSelection(selected, { emit: false });
return;
}
menu.innerHTML = '';
picker.items.forEach((item) => {
const itemValue = getNormalizedItemValue(item);
const row = globalScope.document.createElement('div');
row.className = 'editable-list-option-row';
const option = globalScope.document.createElement('button');
option.type = 'button';
option.className = 'editable-list-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', itemValue === selected ? 'true' : 'false');
option.textContent = getItemLabel(item);
option.addEventListener('click', () => {
picker.setSelection(itemValue, { emit: true });
picker.close();
});
const deleteButton = globalScope.document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'editable-list-delete';
deleteButton.textContent = deleteLabel;
deleteButton.title = `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`;
deleteButton.setAttribute('aria-label', `${deleteLabel}${itemLabel} ${getItemDeleteLabel(item)}`);
deleteButton.disabled = picker.items.length <= minItems;
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (typeof onDelete === 'function') {
Promise.resolve(onDelete(itemValue, item)).catch(reportDeleteError);
}
});
row.appendChild(option);
row.appendChild(deleteButton);
menu.appendChild(row);
});
picker.setSelection(selected, { emit: false });
};
trigger?.addEventListener('click', (event) => {
event.stopPropagation();
editableListPickers.forEach((item) => {
if (item !== picker) {
item.close();
}
});
picker.setOpen(!picker.open);
});
trigger?.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
picker.close();
}
});
menu?.addEventListener('click', (event) => {
event.stopPropagation();
});
editableListPickers.push(picker);
return picker;
}
function closeEditableListPickers() {
editableListPickers.forEach((picker) => picker.close());
}
function isClickInsideEditableListPicker(target) {
return editableListPickers.some((picker) => Boolean(picker.root?.contains(target)));
}
globalScope.SidepanelEditableListPicker = {
closeEditableListPickers,
createEditableListPicker,
isClickInsideEditableListPicker,
normalizeEditableListValues,
splitEditableListValues,
};
})(window);
+118 -1
View File
@@ -9,6 +9,7 @@
} = context;
let actionInFlight = false;
let payPalAccountPicker = null;
function getPayPalAccounts(currentState = state.getLatestState()) {
return helpers.getPayPalAccounts(currentState);
@@ -27,6 +28,58 @@
)).join('');
}
function getPayPalAccountValue(account = {}) {
return String(account?.id || '').trim();
}
function getPayPalAccountLabel(account = {}) {
return String(account?.email || '(未命名账号)');
}
function normalizePickerPayPalAccounts(accounts = []) {
return Array.isArray(accounts)
? accounts.filter((account) => getPayPalAccountValue(account))
: [];
}
function getPayPalAccountPicker() {
if (payPalAccountPicker) {
return payPalAccountPicker;
}
const pickerModule = helpers.editableListPicker || globalScope.SidepanelEditableListPicker;
const createEditableListPicker = pickerModule?.createEditableListPicker;
if (
typeof createEditableListPicker !== 'function'
|| !dom.payPalAccountPickerRoot
|| !dom.btnPayPalAccountMenu
|| !dom.payPalAccountCurrent
|| !dom.payPalAccountMenu
) {
return null;
}
payPalAccountPicker = createEditableListPicker({
root: dom.payPalAccountPickerRoot,
input: dom.selectPayPalAccount,
trigger: dom.btnPayPalAccountMenu,
current: dom.payPalAccountCurrent,
menu: dom.payPalAccountMenu,
emptyLabel: '请先添加 PayPal 账号',
itemLabel: '账号',
normalizeItems: normalizePickerPayPalAccounts,
normalizeValue: (value) => String(value || '').trim(),
getItemValue: getPayPalAccountValue,
getItemLabel: getPayPalAccountLabel,
getItemDeleteLabel: getPayPalAccountLabel,
onDelete: (accountId) => handleDeletePayPalAccount(accountId),
onDeleteError: (error, fallbackMessage) => {
helpers.showToast(error?.message || fallbackMessage, 'error');
},
});
return payPalAccountPicker;
}
function applyPayPalAccountMutation(account) {
if (!account?.id) return;
const latestState = state.getLatestState();
@@ -43,10 +96,17 @@
const latestState = state.getLatestState();
const accounts = getPayPalAccounts(latestState);
const currentId = getCurrentPayPalAccountId(latestState);
const selectedId = accounts.some((account) => account.id === currentId) ? currentId : '';
const picker = getPayPalAccountPicker();
if (picker) {
picker.render(accounts, selectedId);
return;
}
dom.selectPayPalAccount.innerHTML = buildSelectOptions(accounts);
dom.selectPayPalAccount.disabled = accounts.length === 0;
dom.selectPayPalAccount.value = accounts.some((account) => account.id === currentId) ? currentId : '';
dom.selectPayPalAccount.value = selectedId;
}
async function syncSelectedPayPalAccount(options = {}) {
@@ -83,6 +143,62 @@
return response.account || null;
}
async function handleDeletePayPalAccount(accountId) {
if (actionInFlight) return;
const targetId = String(accountId || '').trim();
if (!targetId) {
return;
}
const latestState = state.getLatestState();
const accounts = getPayPalAccounts(latestState);
const targetAccount = accounts.find((account) => account.id === targetId);
if (!targetAccount) {
return;
}
actionInFlight = true;
if (dom.btnAddPayPalAccount) {
dom.btnAddPayPalAccount.disabled = true;
}
try {
const nextAccounts = accounts.filter((account) => account.id !== targetId);
const currentId = getCurrentPayPalAccountId(latestState);
const nextCurrentAccount = currentId === targetId
? (nextAccounts[0] || null)
: (nextAccounts.find((account) => account.id === currentId) || null);
const payload = {
paypalAccounts: nextAccounts,
currentPayPalAccountId: nextCurrentAccount?.id || '',
paypalEmail: String(nextCurrentAccount?.email || '').trim(),
paypalPassword: String(nextCurrentAccount?.password || ''),
};
const response = await runtime.sendMessage({
type: 'SAVE_SETTING',
source: 'sidepanel',
payload,
});
if (response?.error) {
throw new Error(response.error);
}
state.syncLatestState({
...payload,
currentPayPalAccountId: payload.currentPayPalAccountId || null,
});
renderPayPalAccounts();
helpers.showToast(`已删除 PayPal 账号:${targetAccount.email || targetId}`, 'success', 1600);
} finally {
actionInFlight = false;
if (dom.btnAddPayPalAccount) {
dom.btnAddPayPalAccount.disabled = false;
}
}
}
async function openPayPalAccountDialog() {
if (typeof helpers.openFormDialog !== 'function') {
throw new Error('表单弹窗能力未加载,请刷新扩展后重试。');
@@ -180,6 +296,7 @@
return {
bindPayPalEvents,
handleDeletePayPalAccount,
renderPayPalAccounts,
syncSelectedPayPalAccount,
};
+14 -14
View File
@@ -1971,13 +1971,13 @@ header {
.data-select:focus { border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
[data-theme="dark"] .data-select { color-scheme: dark; }
.sub2api-group-picker {
.editable-list-picker {
position: relative;
flex: 1;
min-width: 0;
}
.sub2api-group-trigger {
.editable-list-trigger {
width: 100%;
display: flex;
align-items: center;
@@ -1987,7 +1987,7 @@ header {
text-align: left;
}
.sub2api-group-caret {
.editable-list-caret {
width: 8px;
height: 8px;
border-right: 1.5px solid currentColor;
@@ -1997,11 +1997,11 @@ header {
opacity: 0.7;
}
.sub2api-group-trigger.is-open .sub2api-group-caret {
.editable-list-trigger.is-open .editable-list-caret {
transform: translateY(2px) rotate(225deg);
}
.sub2api-group-menu {
.editable-list-menu {
position: absolute;
z-index: 70;
top: calc(100% + 4px);
@@ -2016,15 +2016,15 @@ header {
box-shadow: var(--shadow-md);
}
.sub2api-group-option-row {
.editable-list-option-row {
display: flex;
align-items: center;
gap: 4px;
min-height: 30px;
}
.sub2api-group-option,
.sub2api-group-delete {
.editable-list-option,
.editable-list-delete {
border: none;
border-radius: var(--radius-sm);
background: transparent;
@@ -2033,7 +2033,7 @@ header {
transition: background var(--transition), color var(--transition);
}
.sub2api-group-option {
.editable-list-option {
flex: 1;
min-width: 0;
padding: 6px 8px;
@@ -2045,24 +2045,24 @@ header {
white-space: nowrap;
}
.sub2api-group-option[aria-selected="true"] {
.editable-list-option[aria-selected="true"] {
color: var(--blue);
background: var(--blue-soft);
}
.sub2api-group-option:hover,
.sub2api-group-delete:hover:not(:disabled) {
.editable-list-option:hover,
.editable-list-delete:hover:not(:disabled) {
color: var(--text-primary);
background: var(--bg-hover);
}
.sub2api-group-delete {
.editable-list-delete {
flex: 0 0 auto;
padding: 5px 7px;
font-size: 11px;
}
.sub2api-group-delete:disabled {
.editable-list-delete:disabled {
cursor: not-allowed;
opacity: 0.42;
}
+32 -9
View File
@@ -199,13 +199,13 @@
<span class="data-label">分组</span>
<div class="data-inline">
<input type="hidden" id="input-sub2api-group" value="codex" />
<div id="sub2api-group-picker" class="sub2api-group-picker">
<button id="btn-sub2api-group-menu" class="data-select sub2api-group-trigger" type="button"
<div id="sub2api-group-picker" class="editable-list-picker">
<button id="btn-sub2api-group-menu" class="data-select editable-list-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false">
<span id="sub2api-group-current" class="truncate">codex</span>
<span class="sub2api-group-caret" aria-hidden="true"></span>
<span class="editable-list-caret" aria-hidden="true"></span>
</button>
<div id="sub2api-group-menu" class="sub2api-group-menu" role="listbox" hidden></div>
<div id="sub2api-group-menu" class="editable-list-menu" role="listbox" hidden></div>
</div>
<button id="btn-add-sub2api-group" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div>
@@ -267,9 +267,15 @@
<div class="data-row" id="row-paypal-account" style="display:none;">
<span class="data-label">PayPal 账号</span>
<div class="data-inline">
<select id="select-paypal-account" class="data-select">
<option value="">请先添加 PayPal 账号</option>
</select>
<input type="hidden" id="select-paypal-account" value="" />
<div id="paypal-account-picker" class="editable-list-picker">
<button id="btn-paypal-account-menu" class="data-select editable-list-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false">
<span id="paypal-account-current" class="truncate">请先添加 PayPal 账号</span>
<span class="editable-list-caret" aria-hidden="true"></span>
</button>
<div id="paypal-account-menu" class="editable-list-menu" role="listbox" hidden></div>
</div>
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
</div>
</div>
@@ -459,7 +465,15 @@
<div class="data-row" id="row-cf-domain" style="display:none;">
<span class="data-label">CF 域名</span>
<div class="data-inline">
<select id="select-cf-domain" class="data-select"></select>
<input type="hidden" id="select-cf-domain" value="" />
<div id="cf-domain-picker" class="editable-list-picker">
<button id="btn-cf-domain-menu" class="data-select editable-list-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false">
<span id="cf-domain-current" class="truncate">请先添加域名</span>
<span class="editable-list-caret" aria-hidden="true"></span>
</button>
<div id="cf-domain-menu" class="editable-list-menu" role="listbox" hidden></div>
</div>
<input type="text" id="input-cf-domain" class="data-input" placeholder="例如 yourdomain.xyz"
style="display:none;" />
<button id="btn-cf-domain-mode" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
@@ -633,7 +647,15 @@
<div class="data-row" id="row-temp-email-domain" style="display:none;">
<span class="data-label">Temp 域名</span>
<div class="data-inline">
<select id="select-temp-email-domain" class="data-select"></select>
<input type="hidden" id="select-temp-email-domain" value="" />
<div id="temp-email-domain-picker" class="editable-list-picker">
<button id="btn-temp-email-domain-menu" class="data-select editable-list-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false">
<span id="temp-email-domain-current" class="truncate">请先添加域名</span>
<span class="editable-list-caret" aria-hidden="true"></span>
</button>
<div id="temp-email-domain-menu" class="editable-list-menu" role="listbox" hidden></div>
</div>
<input type="text" id="input-temp-email-domain" class="data-input" placeholder="例如 mail.example.com"
style="display:none;" />
<button id="btn-temp-email-domain-mode" class="btn btn-outline btn-sm" type="button">添加</button>
@@ -1595,6 +1617,7 @@
<script src="contribution-content-update-service.js"></script>
<script src="account-pool-ui.js"></script>
<script src="form-dialog.js"></script>
<script src="editable-list-picker.js"></script>
<script src="hotmail-manager.js"></script>
<script src="mail-2925-manager.js"></script>
<script src="paypal-manager.js"></script>
+139 -158
View File
@@ -95,7 +95,7 @@ const rowSub2ApiPassword = document.getElementById('row-sub2api-password');
const inputSub2ApiPassword = document.getElementById('input-sub2api-password');
const rowSub2ApiGroup = document.getElementById('row-sub2api-group');
const inputSub2ApiGroup = document.getElementById('input-sub2api-group');
const sub2ApiGroupPicker = document.getElementById('sub2api-group-picker');
const sub2ApiGroupPickerRoot = document.getElementById('sub2api-group-picker');
const btnSub2ApiGroupMenu = document.getElementById('btn-sub2api-group-menu');
const sub2ApiGroupCurrent = document.getElementById('sub2api-group-current');
const sub2ApiGroupMenu = document.getElementById('sub2api-group-menu');
@@ -178,6 +178,10 @@ const btnGpcCardKeyPurchase = document.getElementById('btn-gpc-card-key-purchase
const plusPaymentMethodCaption = document.getElementById('plus-payment-method-caption');
const rowPayPalAccount = document.getElementById('row-paypal-account');
const selectPayPalAccount = document.getElementById('select-paypal-account');
const payPalAccountPickerRoot = document.getElementById('paypal-account-picker');
const btnPayPalAccountMenu = document.getElementById('btn-paypal-account-menu');
const payPalAccountCurrent = document.getElementById('paypal-account-current');
const payPalAccountMenu = document.getElementById('paypal-account-menu');
const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account');
const rowGpcHelperApi = document.getElementById('row-gpc-helper-api');
const inputGpcHelperApi = document.getElementById('input-gpc-helper-api');
@@ -240,6 +244,10 @@ const rowTempEmailRandomSubdomainToggle = document.getElementById('row-temp-emai
const inputTempEmailUseRandomSubdomain = document.getElementById('input-temp-email-use-random-subdomain');
const rowTempEmailDomain = document.getElementById('row-temp-email-domain');
const selectTempEmailDomain = document.getElementById('select-temp-email-domain');
const tempEmailDomainPickerRoot = document.getElementById('temp-email-domain-picker');
const btnTempEmailDomainMenu = document.getElementById('btn-temp-email-domain-menu');
const tempEmailDomainCurrent = document.getElementById('temp-email-domain-current');
const tempEmailDomainMenu = document.getElementById('temp-email-domain-menu');
const inputTempEmailDomain = document.getElementById('input-temp-email-domain');
const btnTempEmailDomainMode = document.getElementById('btn-temp-email-domain-mode');
const cloudflareTempEmailSection = document.getElementById('cloudflare-temp-email-section');
@@ -339,6 +347,10 @@ const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
const rowCfDomain = document.getElementById('row-cf-domain');
const selectCfDomain = document.getElementById('select-cf-domain');
const cfDomainPickerRoot = document.getElementById('cf-domain-picker');
const btnCfDomainMenu = document.getElementById('btn-cf-domain-menu');
const cfDomainCurrent = document.getElementById('cf-domain-current');
const cfDomainMenu = document.getElementById('cf-domain-menu');
const inputCfDomain = document.getElementById('input-cf-domain');
const btnCfDomainMode = document.getElementById('btn-cf-domain-mode');
const inputRunCount = document.getElementById('input-run-count');
@@ -1099,36 +1111,40 @@ let currentContributionContentSnapshot = null;
let contributionContentSnapshotRequestInFlight = null;
const DEFAULT_SUB2API_GROUP_OPTIONS = ['codex', 'openai-plus'];
let sub2ApiGroupMenuOpen = false;
function splitSub2ApiGroupNames(value = '') {
return String(value || '')
.split(/[\r\n,,、]+/)
.map((name) => name.trim())
.filter(Boolean);
}
const editableListPickerModule = window.SidepanelEditableListPicker || {};
const normalizeEditableListValues = editableListPickerModule.normalizeEditableListValues
|| ((...sources) => {
const values = [];
const seen = new Set();
const append = (value) => {
const items = Array.isArray(value)
? value
: String(value || '').split(/[\r\n,,、]+/);
items.forEach((item) => {
const normalized = String(item || '').trim();
const key = normalized.toLowerCase();
if (key && !seen.has(key)) {
seen.add(key);
values.push(normalized);
}
});
};
sources.forEach(append);
return values;
});
const createEditableListPicker = editableListPickerModule.createEditableListPicker
|| (() => ({
close() { },
render() { },
setOpen() { },
setSelection() { },
setVisible() { },
}));
const closeEditableListPickers = editableListPickerModule.closeEditableListPickers || (() => { });
const isClickInsideEditableListPicker = editableListPickerModule.isClickInsideEditableListPicker || (() => false);
function normalizeSub2ApiGroupOptions(...sources) {
const groups = [];
const seen = new Set();
const append = (value) => {
if (Array.isArray(value)) {
value.forEach(append);
return;
}
for (const name of splitSub2ApiGroupNames(value)) {
const key = name.toLowerCase();
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
groups.push(name);
}
};
sources.forEach(append);
return groups;
return normalizeEditableListValues(...sources);
}
function getSelectedSub2ApiGroupName() {
@@ -1144,29 +1160,46 @@ function getSub2ApiGroupOptionsState(state = latestState) {
return options.length ? options : [...DEFAULT_SUB2API_GROUP_OPTIONS];
}
function setSub2ApiGroupMenuOpen(open) {
sub2ApiGroupMenuOpen = Boolean(open);
if (sub2ApiGroupMenu) {
sub2ApiGroupMenu.hidden = !sub2ApiGroupMenuOpen;
}
if (btnSub2ApiGroupMenu) {
btnSub2ApiGroupMenu.setAttribute('aria-expanded', sub2ApiGroupMenuOpen ? 'true' : 'false');
btnSub2ApiGroupMenu.classList?.toggle('is-open', sub2ApiGroupMenuOpen);
}
}
const sub2ApiGroupPicker = createEditableListPicker({
root: sub2ApiGroupPickerRoot,
input: inputSub2ApiGroup,
trigger: btnSub2ApiGroupMenu,
current: sub2ApiGroupCurrent,
menu: sub2ApiGroupMenu,
fallbackItems: DEFAULT_SUB2API_GROUP_OPTIONS,
minItems: 1,
itemLabel: '分组',
onDelete: handleDeleteSub2ApiGroup,
onDeleteError: (error) => showToast(error?.message || '删除 SUB2API 分组失败。', 'error'),
});
function setSub2ApiGroupSelection(groupName, options = {}) {
const selected = String(groupName || '').trim() || DEFAULT_SUB2API_GROUP_OPTIONS[0];
if (inputSub2ApiGroup) {
inputSub2ApiGroup.value = selected;
if (options.emit && typeof inputSub2ApiGroup.dispatchEvent === 'function') {
inputSub2ApiGroup.dispatchEvent(new Event('change', { bubbles: true }));
}
}
if (sub2ApiGroupCurrent) {
sub2ApiGroupCurrent.textContent = selected;
}
}
const cfDomainPicker = createEditableListPicker({
root: cfDomainPickerRoot,
input: selectCfDomain,
trigger: btnCfDomainMenu,
current: cfDomainCurrent,
menu: cfDomainMenu,
emptyLabel: '请先添加域名',
itemLabel: '域名',
normalizeItems: normalizeCloudflareDomains,
normalizeValue: normalizeCloudflareDomainValue,
onDelete: handleDeleteCloudflareDomain,
onDeleteError: (error) => showToast(error?.message || '删除 Cloudflare 域名失败。', 'error'),
});
const tempEmailDomainPicker = createEditableListPicker({
root: tempEmailDomainPickerRoot,
input: selectTempEmailDomain,
trigger: btnTempEmailDomainMenu,
current: tempEmailDomainCurrent,
menu: tempEmailDomainMenu,
emptyLabel: '请先添加域名',
itemLabel: '域名',
normalizeItems: normalizeCloudflareTempEmailDomains,
normalizeValue: normalizeCloudflareTempEmailDomainValue,
onDelete: handleDeleteCloudflareTempEmailDomain,
onDeleteError: (error) => showToast(error?.message || '删除 Cloudflare Temp Email 域名失败。', 'error'),
});
function renderSub2ApiGroupOptions(state = latestState, selectedValue = '') {
if (!inputSub2ApiGroup) {
@@ -1182,52 +1215,7 @@ function renderSub2ApiGroupOptions(state = latestState, selectedValue = '') {
options.unshift(selected);
}
if (
!sub2ApiGroupMenu
|| typeof sub2ApiGroupMenu.appendChild !== 'function'
|| typeof document === 'undefined'
|| typeof document.createElement !== 'function'
) {
setSub2ApiGroupSelection(selected || options[0] || DEFAULT_SUB2API_GROUP_OPTIONS[0]);
return;
}
sub2ApiGroupMenu.innerHTML = '';
options.forEach((name) => {
const row = document.createElement('div');
row.className = 'sub2api-group-option-row';
const option = document.createElement('button');
option.type = 'button';
option.className = 'sub2api-group-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', name === selected ? 'true' : 'false');
option.textContent = name;
option.addEventListener('click', () => {
setSub2ApiGroupSelection(name, { emit: true });
setSub2ApiGroupMenuOpen(false);
});
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'sub2api-group-delete';
deleteButton.textContent = '删除';
deleteButton.title = `删除分组 ${name}`;
deleteButton.setAttribute('aria-label', `删除分组 ${name}`);
deleteButton.disabled = options.length <= 1;
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
handleDeleteSub2ApiGroup(name).catch((error) => {
showToast(error?.message || '删除 SUB2API 分组失败。', 'error');
});
});
row.appendChild(option);
row.appendChild(deleteButton);
sub2ApiGroupMenu.appendChild(row);
});
setSub2ApiGroupSelection(selected || options[0] || DEFAULT_SUB2API_GROUP_OPTIONS[0]);
sub2ApiGroupPicker.render(options, selected || options[0] || DEFAULT_SUB2API_GROUP_OPTIONS[0]);
}
let customEmailPoolEntriesState = [];
@@ -2730,58 +2718,20 @@ function renderCloudflareDomainOptions(preferredDomain = '') {
const preferred = normalizeCloudflareDomainValue(preferredDomain);
const { domains, activeDomain } = getCloudflareDomainsFromState();
const selected = preferred || activeDomain;
selectCfDomain.innerHTML = '';
if (domains.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = '请先添加域名';
selectCfDomain.appendChild(option);
selectCfDomain.disabled = true;
selectCfDomain.value = '';
return;
}
for (const domain of domains) {
const option = document.createElement('option');
option.value = domain;
option.textContent = domain;
selectCfDomain.appendChild(option);
}
selectCfDomain.disabled = false;
selectCfDomain.value = domains.includes(selected) ? selected : domains[0];
cfDomainPicker.render(domains, domains.includes(selected) ? selected : domains[0] || '');
}
function renderCloudflareTempEmailDomainOptions(preferredDomain = '') {
const preferred = normalizeCloudflareTempEmailDomainValue(preferredDomain);
const { domains, activeDomain } = getCloudflareTempEmailDomainsFromState();
const selected = preferred || activeDomain;
selectTempEmailDomain.innerHTML = '';
if (domains.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = '请先添加域名';
selectTempEmailDomain.appendChild(option);
selectTempEmailDomain.disabled = true;
selectTempEmailDomain.value = '';
return;
}
for (const domain of domains) {
const option = document.createElement('option');
option.value = domain;
option.textContent = domain;
selectTempEmailDomain.appendChild(option);
}
selectTempEmailDomain.disabled = false;
selectTempEmailDomain.value = domains.includes(selected) ? selected : domains[0];
tempEmailDomainPicker.render(domains, domains.includes(selected) ? selected : domains[0] || '');
}
function setCloudflareDomainEditMode(editing, options = {}) {
const { clearInput = false } = options;
cloudflareDomainEditMode = Boolean(editing);
selectCfDomain.style.display = cloudflareDomainEditMode ? 'none' : '';
cfDomainPicker.setVisible(!cloudflareDomainEditMode);
inputCfDomain.style.display = cloudflareDomainEditMode ? '' : 'none';
btnCfDomainMode.textContent = cloudflareDomainEditMode ? '保存' : '添加';
if (cloudflareDomainEditMode) {
@@ -2797,7 +2747,7 @@ function setCloudflareDomainEditMode(editing, options = {}) {
function setCloudflareTempEmailDomainEditMode(editing, options = {}) {
const { clearInput = false } = options;
cloudflareTempEmailDomainEditMode = Boolean(editing);
selectTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? 'none' : '';
tempEmailDomainPicker.setVisible(!cloudflareTempEmailDomainEditMode);
inputTempEmailDomain.style.display = cloudflareTempEmailDomainEditMode ? '' : 'none';
btnTempEmailDomainMode.textContent = cloudflareTempEmailDomainEditMode ? '保存' : '添加';
if (cloudflareTempEmailDomainEditMode) {
@@ -9528,6 +9478,46 @@ async function saveCloudflareTempEmailDomainSettings(domains, activeDomain, opti
}
}
async function handleDeleteCloudflareDomain(domain) {
const targetDomain = normalizeCloudflareDomainValue(domain);
if (!targetDomain) {
return;
}
const { domains, activeDomain } = getCloudflareDomainsFromState();
const nextDomains = domains.filter((item) => item !== targetDomain);
if (nextDomains.length === domains.length) {
return;
}
const currentDomain = normalizeCloudflareDomainValue(selectCfDomain.value || activeDomain);
const nextActiveDomain = currentDomain === targetDomain
? (nextDomains[0] || '')
: (nextDomains.includes(currentDomain) ? currentDomain : nextDomains[0] || '');
await saveCloudflareDomainSettings(nextDomains, nextActiveDomain, { silent: true });
showToast(`已删除 Cloudflare 域名:${targetDomain}`, 'success', 1600);
}
async function handleDeleteCloudflareTempEmailDomain(domain) {
const targetDomain = normalizeCloudflareTempEmailDomainValue(domain);
if (!targetDomain) {
return;
}
const { domains, activeDomain } = getCloudflareTempEmailDomainsFromState();
const nextDomains = domains.filter((item) => item !== targetDomain);
if (nextDomains.length === domains.length) {
return;
}
const currentDomain = normalizeCloudflareTempEmailDomainValue(selectTempEmailDomain.value || activeDomain);
const nextActiveDomain = currentDomain === targetDomain
? (nextDomains[0] || '')
: (nextDomains.includes(currentDomain) ? currentDomain : nextDomains[0] || '');
await saveCloudflareTempEmailDomainSettings(nextDomains, nextActiveDomain, { silent: true });
showToast(`已删除 Cloudflare Temp Email 域名:${targetDomain}`, 'success', 1600);
}
async function handleAddSub2ApiGroup() {
if (!sharedFormDialog?.open) {
showToast('表单弹窗未加载,请刷新扩展后重试。', 'error');
@@ -9606,7 +9596,7 @@ async function handleDeleteSub2ApiGroup(groupName) {
sub2apiGroupNames: nextGroups,
});
renderSub2ApiGroupOptions(latestState, nextSelectedGroup);
setSub2ApiGroupMenuOpen(true);
sub2ApiGroupPicker.setOpen(true);
markSettingsDirty(true);
await saveSettings({ silent: true }).catch(() => { });
showToast(`已删除 SUB2API 分组:${targetName}`, 'success', 1600);
@@ -10060,9 +10050,14 @@ const payPalManager = window.SidepanelPayPalManager?.createPayPalManager({
},
dom: {
btnAddPayPalAccount,
btnPayPalAccountMenu,
payPalAccountCurrent,
payPalAccountMenu,
payPalAccountPickerRoot,
selectPayPalAccount,
},
helpers: {
editableListPicker: editableListPickerModule,
escapeHtml,
getPayPalAccounts,
openFormDialog: (options) => {
@@ -11703,18 +11698,6 @@ inputSub2ApiGroup.addEventListener('change', () => {
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => { });
});
btnSub2ApiGroupMenu?.addEventListener('click', (event) => {
event.stopPropagation();
setSub2ApiGroupMenuOpen(!sub2ApiGroupMenuOpen);
});
btnSub2ApiGroupMenu?.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
setSub2ApiGroupMenuOpen(false);
}
});
sub2ApiGroupMenu?.addEventListener('click', (event) => {
event.stopPropagation();
});
btnAddSub2ApiGroup?.addEventListener('click', () => {
handleAddSub2ApiGroup().catch((error) => {
showToast(error?.message || '添加 SUB2API 分组失败。', 'error');
@@ -13478,7 +13461,7 @@ document.addEventListener('click', (event) => {
const clickedInsideFiveSimCountryMenu = Boolean(fiveSimCountryMenuShell?.contains(event.target));
const clickedInsideNexSmsCountryMenu = Boolean(nexSmsCountryMenuShell?.contains(event.target));
const clickedInsideProviderOrderMenu = Boolean(phoneSmsProviderOrderMenuShell?.contains(event.target));
const clickedInsideSub2ApiGroupPicker = Boolean(sub2ApiGroupPicker?.contains(event.target));
const clickedInsideEditableListPicker = isClickInsideEditableListPicker(event.target);
if (configMenuOpen && !clickedInsideConfigMenu) {
closeConfigMenu();
@@ -13500,8 +13483,8 @@ document.addEventListener('click', (event) => {
if (providerOrderMenuOpen && !clickedInsideProviderOrderMenu) {
setPhoneSmsProviderOrderMenuOpen(false);
}
if (sub2ApiGroupMenuOpen && !clickedInsideSub2ApiGroupPicker) {
setSub2ApiGroupMenuOpen(false);
if (!clickedInsideEditableListPicker) {
closeEditableListPickers();
}
});
@@ -13524,9 +13507,7 @@ document.addEventListener('keydown', (event) => {
if (btnPhoneSmsProviderOrderMenu?.getAttribute('aria-expanded') === 'true') {
setPhoneSmsProviderOrderMenuOpen(false);
}
if (sub2ApiGroupMenuOpen) {
setSub2ApiGroupMenuOpen(false);
}
closeEditableListPickers();
});
window.addEventListener('resize', () => {
@@ -141,9 +141,13 @@ test('sidepanel html contains account records overlay and manager script', () =>
assert.match(html, /id="btn-toggle-account-records-selection"/);
assert.match(html, /id="btn-delete-selected-account-records"/);
assert.match(html, /id="input-sub2api-default-proxy"/);
assert.match(html, /src="editable-list-picker\.js"/);
assert.match(html, /id="sub2api-group-picker"/);
assert.match(html, /id="input-sub2api-group" value="codex"/);
assert.match(html, /id="btn-add-sub2api-group"/);
assert.match(html, /id="paypal-account-picker"/);
assert.match(html, /id="cf-domain-picker"/);
assert.match(html, /id="temp-email-domain-picker"/);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(managerIndex < sidepanelIndex);
+92 -1
View File
@@ -5,13 +5,16 @@ const fs = require('node:fs');
test('sidepanel loads reusable form dialog and paypal manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const formDialogIndex = html.indexOf('<script src="form-dialog.js"></script>');
const editableListPickerIndex = html.indexOf('<script src="editable-list-picker.js"></script>');
const managerIndex = html.indexOf('<script src="paypal-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(formDialogIndex, -1);
assert.notEqual(editableListPickerIndex, -1);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(formDialogIndex < managerIndex);
assert.ok(formDialogIndex < editableListPickerIndex);
assert.ok(editableListPickerIndex < managerIndex);
assert.ok(managerIndex < sidepanelIndex);
});
@@ -22,6 +25,9 @@ test('sidepanel html contains paypal select and GoPay controls', () => {
assert.match(html, /id="select-plus-payment-method"/);
assert.match(html, /id="row-paypal-account"/);
assert.match(html, /id="select-paypal-account"/);
assert.match(html, /id="paypal-account-picker"/);
assert.match(html, /id="btn-paypal-account-menu"/);
assert.match(html, /id="paypal-account-menu"/);
assert.match(html, /id="btn-add-paypal-account"/);
assert.match(html, /id="row-gopay-phone"/);
assert.match(html, /id="input-gopay-phone"/);
@@ -139,3 +145,88 @@ test('paypal manager saves a paypal account and selects it immediately', async (
assert.equal(selectNode.disabled, false);
assert.match(events.at(-1)?.message || '', /已保存 PayPal 账号/);
});
test('paypal manager uses editable picker and deletes obsolete account', async () => {
const source = fs.readFileSync('sidepanel/paypal-manager.js', 'utf8');
const windowObject = {};
const api = new Function('window', `${source}; return window.SidepanelPayPalManager;`)(windowObject);
let latestState = {
paypalAccounts: [
{ id: 'pp-1', email: 'old@example.com', password: 'old-secret' },
{ id: 'pp-2', email: 'next@example.com', password: 'next-secret' },
],
currentPayPalAccountId: 'pp-1',
paypalEmail: 'old@example.com',
paypalPassword: 'old-secret',
};
const events = [];
const renderCalls = [];
let pickerConfig = null;
const selectNode = {
value: '',
addEventListener() {},
};
const manager = api.createPayPalManager({
state: {
getLatestState: () => latestState,
syncLatestState(updates) {
latestState = { ...latestState, ...updates };
},
},
dom: {
btnAddPayPalAccount: { disabled: false, addEventListener() {} },
btnPayPalAccountMenu: {},
payPalAccountCurrent: {},
payPalAccountMenu: {},
payPalAccountPickerRoot: {},
selectPayPalAccount: selectNode,
},
helpers: {
editableListPicker: {
createEditableListPicker(config) {
pickerConfig = config;
return {
render(items, selectedValue) {
renderCalls.push({ items, selectedValue });
selectNode.value = selectedValue;
},
};
},
},
escapeHtml: (value) => String(value || ''),
getPayPalAccounts: (state) => Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [],
openFormDialog: async () => null,
showToast(message, tone) {
events.push({ type: 'toast', message, tone });
},
},
runtime: {
sendMessage: async (message) => {
events.push({ type: 'message', message });
if (message.type === 'SAVE_SETTING') {
return { ok: true };
}
throw new Error(`unexpected message ${message.type}`);
},
},
});
manager.renderPayPalAccounts();
assert.equal(renderCalls.at(-1).selectedValue, 'pp-1');
assert.equal(pickerConfig.getItemLabel(latestState.paypalAccounts[0]), 'old@example.com');
await pickerConfig.onDelete('pp-1');
const saveMessage = events.find((event) => event.type === 'message')?.message;
assert.equal(saveMessage.type, 'SAVE_SETTING');
assert.deepEqual(
saveMessage.payload.paypalAccounts.map((account) => account.id),
['pp-2']
);
assert.equal(saveMessage.payload.currentPayPalAccountId, 'pp-2');
assert.equal(latestState.currentPayPalAccountId, 'pp-2');
assert.equal(latestState.paypalEmail, 'next@example.com');
assert.match(events.at(-1)?.message || '', /已删除 PayPal 账号:old@example\.com/);
});