fix: keep source selector stable

This commit is contained in:
TopherMayor
2026-04-30 12:03:35 -07:00
parent 106270117d
commit 071a18e4a4

View File

@@ -1481,6 +1481,7 @@
let priceRefreshTimer = null; let priceRefreshTimer = null;
let pendingVoteOptionId = null; let pendingVoteOptionId = null;
let pendingVoteRemove = false; let pendingVoteRemove = false;
let pendingStableOptionOrder = null;
// ── Init ─────────────────────────────────────────────────── // ── Init ───────────────────────────────────────────────────
function init() { function init() {
@@ -1760,6 +1761,10 @@
} }
function setOptionSource(optionId, sourceKey) { function setOptionSource(optionId, sourceKey) {
pendingStableOptionOrder = {
tabId: activeTab,
optionIds: getRenderedOptionIds(),
};
state.priceSourceSelections = { state.priceSourceSelections = {
...state.priceSourceSelections, ...state.priceSourceSelections,
[optionId]: normalizeSourceKey(sourceKey), [optionId]: normalizeSourceKey(sourceKey),
@@ -1769,12 +1774,19 @@
} }
function setSortMode(sortMode) { function setSortMode(sortMode) {
pendingStableOptionOrder = null;
const allowedModes = new Set(['vote-desc', 'vote-asc', 'price-asc', 'price-desc']); const allowedModes = new Set(['vote-desc', 'vote-asc', 'price-asc', 'price-desc']);
state.sortMode = allowedModes.has(sortMode) ? sortMode : 'vote-desc'; state.sortMode = allowedModes.has(sortMode) ? sortMode : 'vote-desc';
localStorage.setItem('cabo_sort_mode', state.sortMode); localStorage.setItem('cabo_sort_mode', state.sortMode);
render(); render();
} }
function getRenderedOptionIds() {
return [...document.querySelectorAll('#optionsList .option-card[data-option-id]')]
.map(card => card.dataset.optionId)
.filter(Boolean);
}
function getOptionOrderIndexMap() { function getOptionOrderIndexMap() {
return new Map(state.options.map((opt, index) => [opt.id, index])); return new Map(state.options.map((opt, index) => [opt.id, index]));
} }
@@ -1817,6 +1829,24 @@
}); });
} }
function applyPendingStableOrder(sortedOptions) {
if (!pendingStableOptionOrder || pendingStableOptionOrder.tabId !== activeTab) {
pendingStableOptionOrder = null;
return sortedOptions;
}
const order = new Map(pendingStableOptionOrder.optionIds.map((optionId, index) => [optionId, index]));
pendingStableOptionOrder = null;
if (!order.size) return sortedOptions;
return [...sortedOptions].sort((a, b) => {
const aIndex = order.has(a.id) ? order.get(a.id) : Number.MAX_SAFE_INTEGER;
const bIndex = order.has(b.id) ? order.get(b.id) : Number.MAX_SAFE_INTEGER;
if (aIndex !== bIndex) return aIndex - bIndex;
return sortedOptions.indexOf(a) - sortedOptions.indexOf(b);
});
}
function escapeHtml(value) { function escapeHtml(value) {
return String(value ?? '') return String(value ?? '')
.replace(/&/g, '&') .replace(/&/g, '&')
@@ -2308,7 +2338,7 @@
return; return;
} }
const sorted = sortOptionsByMode(opts, getOptionOrderIndexMap()); const sorted = applyPendingStableOrder(sortOptionsByMode(opts, getOptionOrderIndexMap()));
const maxVotes = sorted[0] ? getVoteEntries(sorted[0]).length : 1; const maxVotes = sorted[0] ? getVoteEntries(sorted[0]).length : 1;
const budgetBoard = activeTab === 'budget' ? renderBudgetBoard() : ''; const budgetBoard = activeTab === 'budget' ? renderBudgetBoard() : '';
@@ -2325,7 +2355,7 @@
: (opt.url ? `<a href="${opt.url}" target="_blank" rel="noopener noreferrer" class="option-link" onclick="event.stopPropagation()">🔗 ${opt.url.replace(/^https?:\/\//, '').split('/')[0]}</a>` : ''); : (opt.url ? `<a href="${opt.url}" target="_blank" rel="noopener noreferrer" class="option-link" onclick="event.stopPropagation()">🔗 ${opt.url.replace(/^https?:\/\//, '').split('/')[0]}</a>` : '');
return ` return `
<div class="option-card${hasVoted ? ' voted' : ''}"> <div class="option-card${hasVoted ? ' voted' : ''}" data-option-id="${escapeHtml(opt.id)}">
<div class="option-top"> <div class="option-top">
<div class="option-name">${opt.name}</div> <div class="option-name">${opt.name}</div>
<div class="option-votes">${voteEntries.length} vote${voteEntries.length !== 1 ? 's' : ''}</div> <div class="option-votes">${voteEntries.length} vote${voteEntries.length !== 1 ? 's' : ''}</div>