修改添加通用组件
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user