mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-01-09 11:08:51 +01:00
feat: Added Partners, Backgrounds, and related variation selections to commander building.
This commit is contained in:
parent
641b305955
commit
d416c9b238
65 changed files with 11835 additions and 691 deletions
959
code/web/templates/build/_partner_controls.html
Normal file
959
code/web/templates/build/_partner_controls.html
Normal file
|
|
@ -0,0 +1,959 @@
|
|||
{% set prefix = partner_id_prefix if partner_id_prefix is defined else 'partner' %}
|
||||
{% set feature_available = partner_feature_available if partner_feature_available is defined else False %}
|
||||
{% set partner_capable = partner_capable if partner_capable is defined else False %}
|
||||
{% set partner_options = partner_options if partner_options is defined else [] %}
|
||||
{% set background_options = background_options if background_options is defined else [] %}
|
||||
{% set partner_select_label = partner_select_label if partner_select_label is defined else 'Partner commander' %}
|
||||
{% set partner_select_placeholder = partner_select_placeholder if partner_select_placeholder is defined else 'Select a partner' %}
|
||||
{% set partner_auto_assigned = partner_auto_assigned if partner_auto_assigned is defined else False %}
|
||||
{% set partner_auto_opt_out = partner_auto_opt_out if partner_auto_opt_out is defined else False %}
|
||||
{% set partner_auto_default = partner_auto_default if partner_auto_default is defined else None %}
|
||||
{% set partner_prefill_available = partner_prefill_available if partner_prefill_available is defined else False %}
|
||||
{% set partner_note_id = prefix ~ '-partner-autonote' %}
|
||||
{% set partner_warning_id = prefix ~ '-partner-warnings' %}
|
||||
{% set partner_suggestions_enabled = partner_suggestions_enabled if partner_suggestions_enabled is defined else False %}
|
||||
{% set partner_suggestions = partner_suggestions if partner_suggestions is defined else [] %}
|
||||
{% set partner_suggestions_hidden = partner_suggestions_hidden if partner_suggestions_hidden is defined else [] %}
|
||||
{% set partner_suggestions_total = partner_suggestions_total if partner_suggestions_total is defined else 0 %}
|
||||
{% set partner_suggestions_metadata = partner_suggestions_metadata if partner_suggestions_metadata is defined else {} %}
|
||||
{% set partner_suggestions_loaded = partner_suggestions_loaded if partner_suggestions_loaded is defined else False %}
|
||||
{% set partner_suggestions_error = partner_suggestions_error if partner_suggestions_error is defined else None %}
|
||||
{% set partner_suggestions_available = partner_suggestions_available if partner_suggestions_available is defined else False %}
|
||||
{% set partner_suggestions_has_hidden = partner_suggestions_has_hidden if partner_suggestions_has_hidden is defined else False %}
|
||||
{% if feature_available %}
|
||||
<fieldset>
|
||||
<legend>Partner Mechanics</legend>
|
||||
{% if not partner_capable %}
|
||||
<p class="muted" style="font-size:12px;">This commander doesn't support partner mechanics or backgrounds.</p>
|
||||
{% else %}
|
||||
<input type="hidden" name="partner_enabled" value="{{ partner_hidden_value or '1' }}" />
|
||||
<input type="hidden" name="partner_auto_opt_out" value="{{ '1' if partner_auto_opt_out else '0' }}" data-partner-auto-opt="{{ prefix }}" />
|
||||
<input type="hidden" name="partner_selection_source" value="" data-partner-selection-source="{{ prefix }}" />
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.5rem;">Choose either a partner commander or a background—never both.</div>
|
||||
{% if partner_role_hint %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">{{ partner_role_hint }}</div>
|
||||
{% endif %}
|
||||
{% if primary_partner_with %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">
|
||||
Pairs naturally with <strong>{{ primary_partner_with|join(', ') }}</strong>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_options and partner_options|length and (not background_options or not background_options|length) %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">No Backgrounds available for this commander.</div>
|
||||
{% elif background_options and background_options|length and (not partner_options or not partner_options|length) %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">This commander can't select a partner commander but can choose a Background.</div>
|
||||
{% endif %}
|
||||
{% if partner_error %}
|
||||
<div style="color:#a00; margin-bottom:.5rem; font-weight:600;">{{ partner_error }}</div>
|
||||
{% endif %}
|
||||
<div id="{{ partner_note_id }}" class="partner-autonote" data-partner-autonote="{{ prefix }}" data-autonote="{{ partner_auto_note or '' }}" style="color:#046d1f; margin-bottom:.5rem; font-size:12px;" role="status" aria-live="polite" aria-atomic="true" aria-hidden="{{ 'false' if partner_auto_note else 'true' }}" {% if not partner_auto_note %}hidden{% endif %}>
|
||||
<span class="sr-only">Partner pairing update:</span>
|
||||
<span data-partner-note-copy>{% if partner_auto_note %}{{ partner_auto_note }}{% endif %}</span>
|
||||
</div>
|
||||
{% if partner_prefill_available and partner_auto_default %}
|
||||
<div style="display:flex; align-items:center; gap:.5rem; margin-bottom:.5rem; flex-wrap:wrap;">
|
||||
<button type="button" class="chip{% if not partner_auto_opt_out %} active{% endif %}" data-partner-autotoggle="{{ prefix }}" data-partner-default="{{ partner_auto_default }}" aria-pressed="{% if not partner_auto_opt_out %}true{% else %}false{% endif %}" aria-describedby="{{ partner_note_id }}">
|
||||
{% if partner_auto_opt_out %}Enable default partner{% else %}Use default partner ({{ partner_auto_default }}){% endif %}
|
||||
</button>
|
||||
<span class="muted" style="font-size:12px;">Toggle to opt-out and choose a different partner.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_suggestions_enabled %}
|
||||
<div class="partner-suggestions" data-partner-suggestions="{{ prefix }}" data-partner-scope="{{ partner_scope if partner_scope is defined else prefix }}" data-api-endpoint="{{ partner_suggestions_endpoint if partner_suggestions_endpoint is defined else '/api/partner/suggestions' }}" data-primary-name="{{ primary_commander_display }}" data-suggestions-json='{{ partner_suggestions | tojson }}' data-hidden-json='{{ partner_suggestions_hidden | tojson }}' data-total="{{ partner_suggestions_total }}" data-loaded="{{ '1' if partner_suggestions_loaded else '0' }}" data-error="{{ partner_suggestions_error or '' }}" data-has-hidden="{{ '1' if partner_suggestions_has_hidden else '0' }}" data-available="{{ '1' if partner_suggestions_available else '0' }}" data-metadata-json='{{ partner_suggestions_metadata | tojson }}' style="display:grid; gap:.35rem; margin-bottom:.75rem;">
|
||||
<div class="partner-suggestions__header" style="display:flex; justify-content:space-between; align-items:center; gap:.5rem; flex-wrap:wrap;">
|
||||
<span style="font-weight:600;">Suggested partners</span>
|
||||
<div class="partner-suggestions__controls" style="display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
<button type="button" class="chip" data-partner-suggestions-refresh="{{ prefix }}" aria-label="Refresh partner suggestions">Refresh</button>
|
||||
<button type="button" class="chip" data-partner-suggestions-more="{{ prefix }}" hidden>Show more</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="partner-suggestions__list" data-partner-suggestions-list style="display:flex; flex-wrap:wrap; gap:.35rem;"></div>
|
||||
<div class="partner-suggestions__meta muted" data-partner-suggestions-meta style="font-size:12px;"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="partner-controls" data-partner-controls="{{ prefix }}" data-partner-scope="{{ partner_scope if partner_scope is defined else prefix }}" data-primary-name="{{ primary_commander_display }}" style="display:grid; gap:.5rem; margin-bottom:.5rem;">
|
||||
{% if partner_options and partner_options|length %}
|
||||
<label style="display:grid; gap:.35rem;">
|
||||
<span>{{ partner_select_label }}</span>
|
||||
<select name="secondary_commander" id="{{ prefix }}-partner-secondary" data-partner-select="secondary" aria-describedby="{{ partner_note_id }} {{ partner_warning_id }}">
|
||||
<option value="">{{ partner_select_placeholder }}</option>
|
||||
{% for opt in partner_options %}
|
||||
{% set is_selected = (selected_secondary_commander|lower == opt.name|lower) %}
|
||||
<option value="{{ opt.name }}" data-pairing-mode="{{ opt.pairing_mode }}" data-mode-label="{{ opt.mode_label }}" data-color-label="{{ opt.color_label }}" data-color-code="{{ opt.color_code }}" data-image-url="{{ opt.image_url or '' }}" data-scryfall-url="{{ opt.scryfall_url or '' }}" data-role-label="{{ opt.role_label or 'Partner commander' }}" {% if is_selected %}selected{% endif %}>
|
||||
{{ opt.name }} — {{ opt.color_label }}
|
||||
{% if opt.pairing_mode == 'partner_with' %}(Partner With){% elif opt.pairing_mode == 'partner_restricted' and opt.restriction_label %} (Partner - {{ opt.restriction_label }}){% elif opt.pairing_mode == 'doctor_companion' and opt.role_label %} ({{ opt.role_label }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if background_options and background_options|length %}
|
||||
<label style="display:grid; gap:.35rem;">
|
||||
<span>Background</span>
|
||||
<select name="background" id="{{ prefix }}-partner-background" data-partner-select="background" aria-describedby="{{ partner_note_id }} {{ partner_warning_id }}">
|
||||
<option value="">Select a background</option>
|
||||
{% for opt in background_options %}
|
||||
<option value="{{ opt.name }}" data-color-label="{{ opt.color_label }}" data-color-code="{{ opt.color_code if opt.color_code is defined else '' }}" data-image-url="{{ opt.image_url or '' }}" data-scryfall-url="{{ opt.scryfall_url or '' }}" data-role-label="{{ opt.role_label or 'Background' }}" {% if selected_background == opt.name %}selected{% endif %}>{{ opt.name }} — {{ opt.color_label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:flex; gap:.5rem; margin-bottom:.5rem; flex-wrap:wrap;">
|
||||
<button type="button" class="chip" data-partner-clear="{{ prefix }}">Clear selection</button>
|
||||
</div>
|
||||
<div class="partner-preview" data-partner-preview="{{ prefix }}" {% if partner_preview %}data-preview-json='{{ partner_preview | tojson }}'{% else %}hidden{% endif %}>
|
||||
{% if partner_preview %}
|
||||
{% set preview_image = partner_preview.secondary_image_url or partner_preview.image_url %}
|
||||
{% if not preview_image and partner_preview.secondary_name %}
|
||||
{% set preview_image = 'https://api.scryfall.com/cards/named?fuzzy=' ~ partner_preview.secondary_name|urlencode ~ '&format=image&version=normal' %}
|
||||
{% endif %}
|
||||
{% set preview_href = partner_preview.secondary_scryfall_url or partner_preview.scryfall_url %}
|
||||
{% if not preview_href and partner_preview.secondary_name %}
|
||||
{% set preview_href = 'https://scryfall.com/search?q=' ~ partner_preview.secondary_name|urlencode %}
|
||||
{% endif %}
|
||||
{% set preview_role = partner_preview.secondary_role_label or partner_preview.role_label %}
|
||||
{% set preview_primary = partner_preview.primary_name or primary_commander_display %}
|
||||
{% set preview_secondary = partner_preview.secondary_name %}
|
||||
{% set preview_themes = partner_preview.theme_tags %}
|
||||
{% set preview_mode_label = partner_preview.partner_mode_label %}
|
||||
{% set preview_color_label = partner_preview.color_label %}
|
||||
<div class="partner-preview__layout">
|
||||
{% if preview_image %}
|
||||
<div class="partner-preview__art">
|
||||
{% if preview_href %}<a href="{{ preview_href }}" target="_blank" rel="noopener">{% endif %}
|
||||
<img src="{{ preview_image }}" alt="{{ (preview_secondary or 'Selected card') ~ ' card image' }}" loading="lazy" decoding="async" />
|
||||
{% if preview_href %}</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="partner-preview__details">
|
||||
<div class="partner-preview__header">{{ preview_mode_label }}{% if preview_color_label %} • {{ preview_color_label }}{% endif %}</div>
|
||||
{% if preview_role %}
|
||||
<div class="partner-preview__role">{{ preview_role }}</div>
|
||||
{% endif %}
|
||||
{% if preview_secondary %}
|
||||
<div class="partner-preview__pairing">Pairing: {{ preview_primary }}{% if preview_secondary %} + {{ preview_secondary }}{% endif %}</div>
|
||||
{% endif %}
|
||||
{% if preview_themes %}
|
||||
<div class="partner-preview__themes muted">Theme emphasis: {{ preview_themes|join(', ') }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="{{ partner_warning_id }}" data-partner-warnings="{{ prefix }}" data-warnings-json='{{ (partner_warnings or []) | tojson }}' style="background:#fff7e5; border:1px solid #f0c36d; border-radius:8px; padding:.75rem; font-size:12px; color:#7a4b02;" role="alert" aria-live="polite" aria-hidden="{{ 'false' if partner_warnings and partner_warnings|length else 'true' }}" {% if not (partner_warnings and partner_warnings|length) %}hidden{% endif %}>
|
||||
{% if partner_warnings and partner_warnings|length %}
|
||||
<strong>Warnings</strong>
|
||||
<ul style="margin:.35rem 0 0 1.1rem;">
|
||||
{% for warn in partner_warnings %}
|
||||
<li>{{ warn }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<script>
|
||||
(function(){
|
||||
var prefix = '{{ prefix }}';
|
||||
var controls = document.querySelector('[data-partner-controls="' + prefix + '"]');
|
||||
if (!controls || controls.dataset.partnerInit === '1') return;
|
||||
controls.dataset.partnerInit = '1';
|
||||
var scope = controls.getAttribute('data-partner-scope') || prefix;
|
||||
var selects = Array.prototype.slice.call(controls.querySelectorAll('[data-partner-select]'));
|
||||
var clearBtn = document.querySelector('[data-partner-clear="' + prefix + '"]');
|
||||
var optInput = document.querySelector('input[name="partner_auto_opt_out"][data-partner-auto-opt="' + prefix + '"]');
|
||||
var autoToggle = document.querySelector('[data-partner-autotoggle="' + prefix + '"]');
|
||||
var defaultPartner = autoToggle ? autoToggle.getAttribute('data-partner-default') : null;
|
||||
var previewBox = document.querySelector('[data-partner-preview="' + prefix + '"]');
|
||||
var warningsBox = document.querySelector('[data-partner-warnings="' + prefix + '"]');
|
||||
var autoNoteBox = document.querySelector('[data-partner-autonote="' + prefix + '"]');
|
||||
var autoNoteCopy = autoNoteBox ? autoNoteBox.querySelector('[data-partner-note-copy]') : null;
|
||||
var primaryName = controls.getAttribute('data-primary-name') || '';
|
||||
var fieldset = controls.closest('fieldset');
|
||||
var partnerEnabledInput = fieldset ? fieldset.querySelector('input[name="partner_enabled"]') : null;
|
||||
var selectionSourceInput = fieldset ? fieldset.querySelector('input[name="partner_selection_source"][data-partner-selection-source="' + prefix + '"]') : null;
|
||||
var initialAutoNote = autoNoteBox ? (autoNoteBox.getAttribute('data-autonote') || '') : '';
|
||||
function setSelectionSource(value){
|
||||
if (!selectionSourceInput) return;
|
||||
if (value && typeof value === 'string'){
|
||||
selectionSourceInput.value = value;
|
||||
} else {
|
||||
selectionSourceInput.value = '';
|
||||
}
|
||||
}
|
||||
function updateSuggestionsMeta(){
|
||||
if (!suggestionsMeta || !suggestionsState){ return; }
|
||||
var message = '';
|
||||
var isError = false;
|
||||
if (suggestionsState.loading){
|
||||
message = 'Loading partner suggestions…';
|
||||
} else if (suggestionsState.error){
|
||||
message = suggestionsState.error;
|
||||
isError = true;
|
||||
} else if (suggestionsState.visible && suggestionsState.visible.length){
|
||||
var shown = suggestionsState.visible.length;
|
||||
if (suggestionsState.expanded && Array.isArray(suggestionsState.hidden)){
|
||||
shown += suggestionsState.hidden.length;
|
||||
}
|
||||
if (suggestionsState.total && suggestionsState.total > 0){
|
||||
message = 'Showing ' + shown + ' of ' + suggestionsState.total + ' suggestions.';
|
||||
} else {
|
||||
message = 'Suggestions generated from recent deck data.';
|
||||
}
|
||||
var meta = suggestionsState.metadata || {};
|
||||
if (meta.generated_at){
|
||||
message += ' Updated ' + meta.generated_at + '.';
|
||||
}
|
||||
} else if (suggestionsState.loaded){
|
||||
message = 'No partner suggestions available for this commander yet.';
|
||||
} else {
|
||||
message = '';
|
||||
}
|
||||
suggestionsMeta.textContent = message;
|
||||
suggestionsMeta.hidden = !message;
|
||||
if (isError){
|
||||
suggestionsMeta.style.color = '#a00';
|
||||
} else {
|
||||
suggestionsMeta.style.color = '';
|
||||
}
|
||||
}
|
||||
|
||||
function markSuggestionActive(){
|
||||
if (!suggestionsList || !suggestionsState){ return; }
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
var partnerValue = partnerSel && partnerSel.value ? partnerSel.value.toLowerCase() : '';
|
||||
var backgroundValue = bgSel && bgSel.value ? bgSel.value.toLowerCase() : '';
|
||||
var buttons = suggestionsList.querySelectorAll('[data-partner-suggestion]');
|
||||
buttons.forEach(function(btn){
|
||||
var mode = (btn.getAttribute('data-mode') || 'partner').toLowerCase();
|
||||
var name = (btn.getAttribute('data-name') || '').toLowerCase();
|
||||
var active = false;
|
||||
if (mode === 'background'){
|
||||
active = !!backgroundValue && backgroundValue === name;
|
||||
} else {
|
||||
active = !!partnerValue && partnerValue === name;
|
||||
}
|
||||
btn.classList.toggle('active', active);
|
||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function renderSuggestions(){
|
||||
if (!suggestionsBox || !suggestionsList || !suggestionsState){
|
||||
return;
|
||||
}
|
||||
suggestionsList.innerHTML = '';
|
||||
if (suggestionsState.error){
|
||||
updateSuggestionsMeta();
|
||||
if (suggestionsMoreButton){ suggestionsMoreButton.hidden = true; }
|
||||
return;
|
||||
}
|
||||
var items = Array.isArray(suggestionsState.visible) ? suggestionsState.visible.slice() : [];
|
||||
if (suggestionsState.expanded && Array.isArray(suggestionsState.hidden)){
|
||||
items = items.concat(suggestionsState.hidden);
|
||||
}
|
||||
if (!items.length){
|
||||
updateSuggestionsMeta();
|
||||
if (suggestionsMoreButton){ suggestionsMoreButton.hidden = true; }
|
||||
return;
|
||||
}
|
||||
items.forEach(function(item){
|
||||
if (!item || !item.name) return;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'chip partner-suggestion-chip';
|
||||
btn.style.display = 'flex';
|
||||
btn.style.flexDirection = 'column';
|
||||
btn.style.alignItems = 'flex-start';
|
||||
btn.style.gap = '2px';
|
||||
btn.setAttribute('data-partner-suggestion', '1');
|
||||
btn.setAttribute('data-mode', item.mode || 'partner');
|
||||
btn.setAttribute('data-name', item.name);
|
||||
if (item.mode_label){ btn.setAttribute('data-mode-label', item.mode_label); }
|
||||
if (item.summary){ btn.setAttribute('data-summary', item.summary); }
|
||||
if (typeof item.score_percent === 'number'){ btn.setAttribute('data-score', String(item.score_percent)); }
|
||||
var titleParts = [];
|
||||
if (item.summary){ titleParts.push(item.summary); }
|
||||
if (Array.isArray(item.reasons) && item.reasons.length){ titleParts = titleParts.concat(item.reasons); }
|
||||
if (titleParts.length){ btn.title = titleParts.join(' • '); }
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'partner-suggestion-chip__name';
|
||||
nameSpan.textContent = item.name;
|
||||
nameSpan.style.fontWeight = '600';
|
||||
btn.appendChild(nameSpan);
|
||||
var summaryText = '';
|
||||
if (item.summary){ summaryText = item.summary; }
|
||||
else if (typeof item.score_percent === 'number'){ summaryText = item.score_percent + '% match'; }
|
||||
else if (item.mode_label){ summaryText = item.mode_label; }
|
||||
if (summaryText){
|
||||
var summarySpan = document.createElement('span');
|
||||
summarySpan.className = 'partner-suggestion-chip__meta muted';
|
||||
summarySpan.textContent = summaryText;
|
||||
summarySpan.style.fontSize = '11px';
|
||||
summarySpan.style.opacity = '0.85';
|
||||
btn.appendChild(summarySpan);
|
||||
}
|
||||
suggestionsList.appendChild(btn);
|
||||
});
|
||||
if (suggestionsMoreButton){
|
||||
if (!suggestionsState.expanded && Array.isArray(suggestionsState.hidden) && suggestionsState.hidden.length){
|
||||
suggestionsMoreButton.hidden = false;
|
||||
suggestionsMoreButton.textContent = 'Show more (' + suggestionsState.hidden.length + ')';
|
||||
} else {
|
||||
suggestionsMoreButton.hidden = true;
|
||||
}
|
||||
}
|
||||
markSuggestionActive();
|
||||
updateSuggestionsMeta();
|
||||
}
|
||||
|
||||
function revealHiddenSuggestions(){
|
||||
if (!suggestionsState){ return; }
|
||||
if (Array.isArray(suggestionsState.hidden) && suggestionsState.hidden.length){
|
||||
suggestionsState.expanded = true;
|
||||
renderSuggestions();
|
||||
} else {
|
||||
fetchSuggestions({ includeHidden: true });
|
||||
}
|
||||
}
|
||||
|
||||
function collectSelectNames(kind){
|
||||
var selector = '[data-partner-select="' + kind + '"]';
|
||||
var sel = controls.querySelector(selector);
|
||||
if (!sel){ return []; }
|
||||
var values = [];
|
||||
Array.prototype.forEach.call(sel.options, function(opt){
|
||||
if (!opt || !opt.value){ return; }
|
||||
values.push(opt.value);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function setSuggestionsLoading(flag){
|
||||
if (!suggestionsState){ return; }
|
||||
suggestionsState.loading = !!flag;
|
||||
if (suggestionsRefreshButton){
|
||||
if (flag){
|
||||
suggestionsRefreshButton.classList.add('loading');
|
||||
suggestionsRefreshButton.setAttribute('aria-busy', 'true');
|
||||
} else {
|
||||
suggestionsRefreshButton.classList.remove('loading');
|
||||
suggestionsRefreshButton.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
updateSuggestionsMeta();
|
||||
}
|
||||
|
||||
function fetchSuggestions(options){
|
||||
if (!suggestionsBox || !suggestionsState){ return; }
|
||||
if (typeof window === 'undefined' || typeof window.fetch !== 'function'){ return; }
|
||||
if (!primaryName){ return; }
|
||||
var includeHidden = !!(options && options.includeHidden);
|
||||
try {
|
||||
var endpoint = suggestionsBox.getAttribute('data-api-endpoint') || '/api/partner/suggestions';
|
||||
var params = new URLSearchParams();
|
||||
params.set('commander', primaryName);
|
||||
var partnerNames = collectSelectNames('secondary');
|
||||
var backgroundNames = collectSelectNames('background');
|
||||
partnerNames.forEach(function(name){ params.append('partner', name); });
|
||||
backgroundNames.forEach(function(name){ params.append('background', name); });
|
||||
params.set('limit', '8');
|
||||
params.set('visible_limit', '3');
|
||||
var modeSet = {};
|
||||
var modes = (options && Array.isArray(options.modes)) ? options.modes : ['partner_with', 'partner', 'doctor_companion', 'background'];
|
||||
modes.forEach(function(mode){
|
||||
var normalized = String(mode || '').trim();
|
||||
if (!normalized){ return; }
|
||||
var lower = normalized.toLowerCase();
|
||||
if (modeSet[lower]){ return; }
|
||||
modeSet[lower] = true;
|
||||
params.append('mode', normalized);
|
||||
});
|
||||
if (includeHidden){ params.set('include_hidden', '1'); }
|
||||
if (options && options.forceRefresh){ params.set('refresh', '1'); }
|
||||
var fetchUrl = endpoint + (endpoint.indexOf('?') === -1 ? '?' : '&') + params.toString();
|
||||
if (suggestionsAbort){
|
||||
try { suggestionsAbort.abort(); } catch(_){ }
|
||||
}
|
||||
suggestionsAbort = new AbortController();
|
||||
setSuggestionsLoading(true);
|
||||
fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: suggestionsAbort.signal,
|
||||
}).then(function(resp){
|
||||
if (!resp.ok){
|
||||
throw new Error('suggestions request failed');
|
||||
}
|
||||
return resp.json();
|
||||
}).then(function(data){
|
||||
suggestionsAbort = null;
|
||||
suggestionsState.error = '';
|
||||
suggestionsState.loaded = true;
|
||||
suggestionsState.metadata = data && data.metadata ? data.metadata : {};
|
||||
suggestionsState.total = (data && typeof data.total === 'number') ? data.total : 0;
|
||||
suggestionsState.visible = Array.isArray(data && data.visible) ? data.visible : [];
|
||||
suggestionsState.hidden = Array.isArray(data && data.hidden) ? data.hidden : [];
|
||||
suggestionsState.expanded = includeHidden && suggestionsState.hidden.length ? true : false;
|
||||
suggestionsBox.setAttribute('data-loaded', '1');
|
||||
suggestionsBox.setAttribute('data-error', '');
|
||||
renderSuggestions();
|
||||
}).catch(function(err){
|
||||
if (err && err.name === 'AbortError'){ return; }
|
||||
suggestionsState.error = 'Unable to load partner suggestions.';
|
||||
suggestionsBox.setAttribute('data-error', suggestionsState.error);
|
||||
renderSuggestions();
|
||||
}).finally(function(){
|
||||
setSuggestionsLoading(false);
|
||||
});
|
||||
} catch(_err){
|
||||
suggestionsState.error = 'Unable to load partner suggestions.';
|
||||
renderSuggestions();
|
||||
}
|
||||
}
|
||||
var initialWarnings = [];
|
||||
if (warningsBox && warningsBox.dataset.warningsJson){
|
||||
try { initialWarnings = JSON.parse(warningsBox.dataset.warningsJson); }
|
||||
catch(_){ initialWarnings = []; }
|
||||
}
|
||||
var serverPayload = null;
|
||||
if (previewBox && previewBox.dataset.previewJson){
|
||||
try{ serverPayload = JSON.parse(previewBox.dataset.previewJson); }
|
||||
catch(_){ serverPayload = null; }
|
||||
}
|
||||
setServerPayload(serverPayload);
|
||||
var suggestionsBox = document.querySelector('[data-partner-suggestions="' + prefix + '"]');
|
||||
var suggestionsList = null;
|
||||
var suggestionsMeta = null;
|
||||
var suggestionsMoreButton = null;
|
||||
var suggestionsRefreshButton = null;
|
||||
var suggestionsAbort = null;
|
||||
var suggestionsState = null;
|
||||
function parseSuggestionsAttr(element, attr, fallback){
|
||||
if (!element){ return fallback; }
|
||||
var raw = element.getAttribute(attr);
|
||||
if (!raw){ return fallback; }
|
||||
try { return JSON.parse(raw); }
|
||||
catch(_){ return fallback; }
|
||||
}
|
||||
if (suggestionsBox){
|
||||
suggestionsList = suggestionsBox.querySelector('[data-partner-suggestions-list]');
|
||||
suggestionsMeta = suggestionsBox.querySelector('[data-partner-suggestions-meta]');
|
||||
suggestionsMoreButton = suggestionsBox.querySelector('[data-partner-suggestions-more="' + prefix + '"]');
|
||||
suggestionsRefreshButton = suggestionsBox.querySelector('[data-partner-suggestions-refresh="' + prefix + '"]');
|
||||
suggestionsState = {
|
||||
visible: parseSuggestionsAttr(suggestionsBox, 'data-suggestions-json', []),
|
||||
hidden: parseSuggestionsAttr(suggestionsBox, 'data-hidden-json', []),
|
||||
metadata: parseSuggestionsAttr(suggestionsBox, 'data-metadata-json', {}),
|
||||
total: parseInt(suggestionsBox.getAttribute('data-total') || '0', 10) || 0,
|
||||
error: suggestionsBox.getAttribute('data-error') || '',
|
||||
loaded: suggestionsBox.getAttribute('data-loaded') === '1',
|
||||
expanded: false,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
var modeLabels = {
|
||||
'partner': 'Partner',
|
||||
'partner_with': 'Partner With',
|
||||
'doctor_companion': "Doctor & Companion",
|
||||
'background': 'Choose a Background'
|
||||
};
|
||||
function buildCardImageUrl(name){
|
||||
if (!name) return '';
|
||||
return 'https://api.scryfall.com/cards/named?fuzzy=' + encodeURIComponent(name) + '&format=image&version=normal';
|
||||
}
|
||||
function buildScryfallUrl(name){
|
||||
if (!name) return '';
|
||||
return 'https://scryfall.com/search?q=' + encodeURIComponent(name);
|
||||
}
|
||||
function defaultRoleForMode(mode){
|
||||
if (!mode) return '';
|
||||
switch(String(mode).toLowerCase()){
|
||||
case 'background':
|
||||
return 'Background';
|
||||
case 'doctor_companion':
|
||||
return "Doctor pairing";
|
||||
default:
|
||||
return 'Partner commander';
|
||||
}
|
||||
}
|
||||
var previewAbort = null;
|
||||
if (typeof window !== 'undefined' && !window.partnerPreviewState){
|
||||
try { window.partnerPreviewState = {}; } catch(_){ }
|
||||
}
|
||||
|
||||
function setPreviewState(detail){
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!detail || typeof detail !== 'object') return;
|
||||
var scopeKey = detail.scope || scope || prefix;
|
||||
if (!scopeKey) return;
|
||||
var store = window.partnerPreviewState || {};
|
||||
store[scopeKey] = {
|
||||
theme_tags: Array.isArray(detail.theme_tags) ? detail.theme_tags.slice() : [],
|
||||
payload: detail.payload || null,
|
||||
warnings: Array.isArray(detail.warnings) ? detail.warnings.slice() : [],
|
||||
auto_note: detail.auto_note || null,
|
||||
partner_mode: detail.partner_mode || null,
|
||||
resolved_secondary: detail.resolved_secondary || null,
|
||||
resolved_background: detail.resolved_background || null,
|
||||
secondary_role_label: detail.secondary_role_label || detail.role_label || null,
|
||||
};
|
||||
try { window.partnerPreviewState = store; } catch(_){ }
|
||||
}
|
||||
|
||||
function escapeHtml(str){
|
||||
return String(str || "").replace(/[&<>"']/g, function(ch){
|
||||
return ({"&":"&","<":"<",">":">","\"":""","'":"'"}[ch]);
|
||||
});
|
||||
}
|
||||
|
||||
function clearPreview(){
|
||||
if (!previewBox) return;
|
||||
previewBox.hidden = true;
|
||||
previewBox.innerHTML = '';
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function renderPreview(payload){
|
||||
if (!previewBox) return;
|
||||
if (!payload){
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
var mode = payload.partner_mode || payload.mode || '';
|
||||
var modeLabel = payload.partner_mode_label || payload.mode_label || modeLabels[mode] || 'Partner Mechanics';
|
||||
var colorLabel = payload.color_label || '';
|
||||
var secondaryName = payload.secondary_name || payload.name || '';
|
||||
var primary = payload.primary_name || primaryName;
|
||||
var themes = Array.isArray(payload.theme_tags) ? payload.theme_tags : [];
|
||||
var imageUrl = payload.secondary_image_url || payload.image_url || '';
|
||||
if (!imageUrl && secondaryName){
|
||||
imageUrl = buildCardImageUrl(secondaryName);
|
||||
}
|
||||
var scryfallUrl = payload.secondary_scryfall_url || payload.scryfall_url || '';
|
||||
if (!scryfallUrl && secondaryName){
|
||||
scryfallUrl = buildScryfallUrl(secondaryName);
|
||||
}
|
||||
var roleLabel = payload.secondary_role_label || payload.role_label || defaultRoleForMode(mode);
|
||||
var html = '<div class="partner-preview__layout">';
|
||||
var normalizedTags = Array.isArray(themes) ? themes.filter(function(tag){ return tag && String(tag).trim(); }).map(function(tag){ return String(tag).trim(); }) : [];
|
||||
themes = normalizedTags;
|
||||
var tagString = normalizedTags.length ? normalizedTags.join(', ') : '';
|
||||
if (imageUrl){
|
||||
var attrParts = [];
|
||||
if (secondaryName){
|
||||
attrParts.push('data-card-name="' + escapeHtml(secondaryName) + '"');
|
||||
attrParts.push('data-original-name="' + escapeHtml(secondaryName) + '"');
|
||||
}
|
||||
if (roleLabel){
|
||||
attrParts.push('data-role="' + escapeHtml(roleLabel) + '"');
|
||||
}
|
||||
if (tagString){
|
||||
attrParts.push('data-tags="' + escapeHtml(tagString) + '"');
|
||||
attrParts.push('data-overlaps="' + escapeHtml(tagString) + '"');
|
||||
}
|
||||
html += '<div class="partner-preview__art card-preview"' + (attrParts.length ? ' ' + attrParts.join(' ') : '') + '>';
|
||||
if (scryfallUrl){
|
||||
html += '<a href="' + escapeHtml(scryfallUrl) + '" target="_blank" rel="noopener">';
|
||||
}
|
||||
html += '<img src="' + escapeHtml(imageUrl) + '" alt="' + escapeHtml((secondaryName || 'Selected card') + ' card image') + '" loading="lazy" decoding="async" data-card-name="' + escapeHtml(secondaryName || '') + '"';
|
||||
if (roleLabel){ html += ' data-role="' + escapeHtml(roleLabel) + '"'; }
|
||||
if (tagString){ html += ' data-tags="' + escapeHtml(tagString) + '" data-overlaps="' + escapeHtml(tagString) + '"'; }
|
||||
html += ' />';
|
||||
if (scryfallUrl){
|
||||
html += '</a>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="partner-preview__details">';
|
||||
html += '<div class="partner-preview__header">' + escapeHtml(modeLabel);
|
||||
if (colorLabel){ html += ' • ' + escapeHtml(colorLabel); }
|
||||
html += '</div>';
|
||||
if (roleLabel){
|
||||
html += '<div class="partner-preview__role">' + escapeHtml(roleLabel) + '</div>';
|
||||
}
|
||||
if (secondaryName){
|
||||
var pairing = escapeHtml(primary);
|
||||
if (pairing){ pairing += ' + '; }
|
||||
html += '<div class="partner-preview__pairing">Pairing: ' + pairing + escapeHtml(secondaryName) + '</div>';
|
||||
}
|
||||
if (themes && themes.length){
|
||||
html += '<div class="partner-preview__themes muted">Theme emphasis: ' + themes.map(escapeHtml).join(', ') + '</div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
previewBox.innerHTML = html;
|
||||
previewBox.hidden = false;
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function setServerPayload(payload){
|
||||
serverPayload = (payload && typeof payload === 'object') ? payload : null;
|
||||
if (!previewBox) return;
|
||||
if (serverPayload){
|
||||
try {
|
||||
previewBox.setAttribute('data-preview-json', JSON.stringify(serverPayload));
|
||||
} catch(_){
|
||||
previewBox.removeAttribute('data-preview-json');
|
||||
}
|
||||
} else {
|
||||
previewBox.removeAttribute('data-preview-json');
|
||||
}
|
||||
}
|
||||
|
||||
function updateAutoNote(note){
|
||||
if (!autoNoteBox) return;
|
||||
var text = (note && String(note).trim()) || '';
|
||||
autoNoteBox.setAttribute('aria-live', 'polite');
|
||||
if (autoNoteCopy){
|
||||
autoNoteCopy.textContent = text;
|
||||
} else {
|
||||
autoNoteBox.textContent = text;
|
||||
}
|
||||
autoNoteBox.hidden = !text;
|
||||
autoNoteBox.setAttribute('aria-hidden', (!text).toString());
|
||||
try { autoNoteBox.setAttribute('data-autonote', text); } catch(_){ }
|
||||
}
|
||||
|
||||
function updateWarnings(list){
|
||||
if (!warningsBox) return;
|
||||
var warnings = Array.isArray(list) ? list.filter(function(msg){ return msg && String(msg).trim(); }) : [];
|
||||
try { warningsBox.setAttribute('data-warnings-json', JSON.stringify(warnings)); } catch(_){ }
|
||||
if (!warnings.length){
|
||||
warningsBox.innerHTML = '';
|
||||
warningsBox.hidden = true;
|
||||
warningsBox.setAttribute('aria-hidden', 'true');
|
||||
return;
|
||||
}
|
||||
var html = '<strong>Warnings</strong><ul style="margin:.35rem 0 0 1.1rem;">';
|
||||
warnings.forEach(function(msg){
|
||||
html += '<li>' + escapeHtml(String(msg)) + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
warningsBox.innerHTML = html;
|
||||
warningsBox.hidden = false;
|
||||
warningsBox.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function dispatchPreview(detail){
|
||||
if (typeof document === 'undefined') return;
|
||||
setPreviewState(detail);
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('partner:preview', { detail: detail }));
|
||||
} catch(_){ }
|
||||
}
|
||||
|
||||
function requestPreviewUpdate(){
|
||||
if (typeof window === 'undefined' || typeof window.fetch !== 'function') return;
|
||||
if (!primaryName) return;
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
var secondaryVal = partnerSel ? (partnerSel.value || '') : '';
|
||||
var bgVal = bgSel ? (bgSel.value || '') : '';
|
||||
var enabledVal = partnerEnabledInput ? (partnerEnabledInput.value || '') : '1';
|
||||
if (previewAbort){
|
||||
try { previewAbort.abort(); } catch(_){ }
|
||||
}
|
||||
previewAbort = new AbortController();
|
||||
var formData = new FormData();
|
||||
formData.append('commander', primaryName);
|
||||
formData.append('partner_enabled', enabledVal || '1');
|
||||
formData.append('secondary_commander', secondaryVal);
|
||||
formData.append('background', bgVal);
|
||||
formData.append('partner_auto_opt_out', optInput ? (optInput.value || '0') : '0');
|
||||
formData.append('scope', scope || prefix);
|
||||
formData.append('selection_source', selectionSourceInput ? (selectionSourceInput.value || '') : '');
|
||||
fetch('/build/partner/preview', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: previewAbort.signal,
|
||||
}).then(function(resp){
|
||||
if (!resp.ok){ throw new Error('preview request failed'); }
|
||||
return resp.json();
|
||||
}).then(function(data){
|
||||
previewAbort = null;
|
||||
if (!data) return;
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'preview')){
|
||||
setServerPayload(data.preview);
|
||||
if (data.preview){ renderPreview(data.preview); }
|
||||
else { clearPreview(); }
|
||||
}
|
||||
updateAutoNote(data && data.auto_note);
|
||||
updateWarnings(data && data.warnings);
|
||||
var evtDetail = {
|
||||
scope: (data && data.scope) || scope || prefix,
|
||||
payload: (data && data.preview) || null,
|
||||
theme_tags: (data && data.theme_tags) || [],
|
||||
warnings: (data && data.warnings) || [],
|
||||
auto_note: (data && data.auto_note) || null,
|
||||
partner_mode: (data && data.partner_mode) || null,
|
||||
resolved_secondary: Object.prototype.hasOwnProperty.call(data || {}, 'resolved_secondary') ? (data && data.resolved_secondary) : undefined,
|
||||
resolved_background: Object.prototype.hasOwnProperty.call(data || {}, 'resolved_background') ? (data && data.resolved_background) : undefined,
|
||||
secondary_role_label: data && data.preview ? (data.preview.secondary_role_label || data.preview.role_label || null) : null,
|
||||
};
|
||||
dispatchPreview(evtDetail);
|
||||
if (partnerSel && Object.prototype.hasOwnProperty.call(data, 'resolved_secondary')){
|
||||
partnerSel.value = data.resolved_secondary || '';
|
||||
}
|
||||
if (bgSel && Object.prototype.hasOwnProperty.call(data, 'resolved_background')){
|
||||
bgSel.value = data.resolved_background || '';
|
||||
}
|
||||
}).catch(function(err){
|
||||
if (err && err.name === 'AbortError'){ return; }
|
||||
previewAbort = null;
|
||||
});
|
||||
}
|
||||
|
||||
updateAutoNote(initialAutoNote);
|
||||
updateWarnings(initialWarnings);
|
||||
var initialHasPreview = !!(serverPayload && Array.isArray(serverPayload.theme_tags) && serverPayload.theme_tags.length);
|
||||
if (initialHasPreview || initialWarnings.length || (initialAutoNote && initialAutoNote.trim())){
|
||||
setTimeout(function(){
|
||||
dispatchPreview({
|
||||
scope: scope || prefix,
|
||||
payload: serverPayload,
|
||||
theme_tags: (serverPayload && serverPayload.theme_tags) || [],
|
||||
warnings: initialWarnings,
|
||||
auto_note: initialAutoNote || null,
|
||||
partner_mode: serverPayload ? (serverPayload.partner_mode || serverPayload.mode || null) : null,
|
||||
secondary_role_label: serverPayload ? (serverPayload.secondary_role_label || serverPayload.role_label || null) : null,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function renderFromServer(){
|
||||
if (serverPayload){
|
||||
renderPreview(serverPayload);
|
||||
} else {
|
||||
clearPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function renderFromSelection(sel, modeOverride){
|
||||
if (!sel){
|
||||
if (serverPayload){ renderFromServer(); } else { clearPreview(); }
|
||||
return;
|
||||
}
|
||||
var option = sel.options[sel.selectedIndex];
|
||||
if (!option || !option.value){
|
||||
if (serverPayload){ renderFromServer(); } else { clearPreview(); }
|
||||
return;
|
||||
}
|
||||
var mode = modeOverride || option.getAttribute('data-pairing-mode') || 'partner';
|
||||
var image = option.getAttribute('data-image-url') || '';
|
||||
var link = option.getAttribute('data-scryfall-url') || '';
|
||||
var role = option.getAttribute('data-role-label') || '';
|
||||
if (!image){ image = buildCardImageUrl(option.value); }
|
||||
if (!link){ link = buildScryfallUrl(option.value); }
|
||||
if (!role){ role = defaultRoleForMode(mode); }
|
||||
var payload = {
|
||||
partner_mode: mode,
|
||||
partner_mode_label: option.getAttribute('data-mode-label') || modeLabels[mode] || 'Partner Mechanics',
|
||||
color_label: option.getAttribute('data-color-label') || '',
|
||||
secondary_name: option.value,
|
||||
primary_name: primaryName,
|
||||
secondary_image_url: image,
|
||||
secondary_scryfall_url: link,
|
||||
secondary_role_label: role,
|
||||
};
|
||||
payload.secondary_role_label = role;
|
||||
payload.theme_tags = payload.theme_tags || [];
|
||||
renderPreview(payload);
|
||||
}
|
||||
|
||||
function setOptOut(flag){
|
||||
if (optInput){ optInput.value = flag ? '1' : '0'; }
|
||||
if (autoToggle){
|
||||
autoToggle.classList.toggle('active', !flag);
|
||||
autoToggle.setAttribute('aria-pressed', (!flag).toString());
|
||||
var label = flag ? 'Enable default partner' : 'Use default partner';
|
||||
if (!flag && defaultPartner){ label += ' (' + defaultPartner + ')'; }
|
||||
autoToggle.textContent = label;
|
||||
}
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function applySuggestionSelection(mode, name){
|
||||
if (!name){ return; }
|
||||
setSelectionSource('suggestion');
|
||||
var normalizedMode = String(mode || '').toLowerCase();
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (normalizedMode === 'background'){
|
||||
if (bgSel){ bgSel.value = name; }
|
||||
if (partnerSel){ partnerSel.value = ''; }
|
||||
if (autoToggle){ setOptOut(true); }
|
||||
if (bgSel){
|
||||
renderFromSelection(bgSel, 'background');
|
||||
requestPreviewUpdate();
|
||||
} else {
|
||||
renderFromServer();
|
||||
}
|
||||
} else {
|
||||
if (partnerSel){ partnerSel.value = name; }
|
||||
if (bgSel){ bgSel.value = ''; }
|
||||
if (autoToggle){
|
||||
syncBySelection();
|
||||
} else if (partnerSel){
|
||||
renderFromSelection(partnerSel);
|
||||
requestPreviewUpdate();
|
||||
} else {
|
||||
renderFromServer();
|
||||
}
|
||||
}
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function syncBySelection(){
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
if (!partnerSel || !autoToggle || !defaultPartner) return;
|
||||
if (partnerSel.value && partnerSel.value.toLowerCase() === defaultPartner.toLowerCase()){
|
||||
setOptOut(false);
|
||||
renderFromSelection(partnerSel);
|
||||
requestPreviewUpdate();
|
||||
} else if (partnerSel.value) {
|
||||
setOptOut(true);
|
||||
renderFromSelection(partnerSel);
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
if (autoToggle){
|
||||
autoToggle.addEventListener('click', function(){
|
||||
var currentOptOut = optInput && optInput.value === '1';
|
||||
if (currentOptOut){
|
||||
setOptOut(false);
|
||||
setSelectionSource('auto');
|
||||
if (defaultPartner){
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
if (partnerSel){ partnerSel.value = defaultPartner; }
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (bgSel){ bgSel.value = ''; }
|
||||
renderFromSelection(partnerSel);
|
||||
} else {
|
||||
renderFromServer();
|
||||
}
|
||||
requestPreviewUpdate();
|
||||
} else {
|
||||
setOptOut(true);
|
||||
setSelectionSource('');
|
||||
selects.forEach(function(sel){
|
||||
if (sel && sel.getAttribute('data-partner-select') === 'secondary'){
|
||||
sel.value = '';
|
||||
}
|
||||
});
|
||||
renderFromServer();
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selects.forEach(function(sel){
|
||||
sel.addEventListener('change', function(){
|
||||
setSelectionSource('manual');
|
||||
var key = sel.getAttribute('data-partner-select');
|
||||
if (key === 'secondary' && sel.value){
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (bgSel){ bgSel.value = ''; }
|
||||
if (autoToggle){
|
||||
syncBySelection();
|
||||
} else {
|
||||
renderFromSelection(sel);
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
markSuggestionActive();
|
||||
return;
|
||||
}
|
||||
if (key === 'background' && sel.value){
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
if (partnerSel){ partnerSel.value = ''; }
|
||||
if (autoToggle){ setOptOut(true); }
|
||||
renderFromSelection(sel, 'background');
|
||||
requestPreviewUpdate();
|
||||
markSuggestionActive();
|
||||
return;
|
||||
}
|
||||
if (!sel.value){
|
||||
renderFromServer();
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
markSuggestionActive();
|
||||
});
|
||||
});
|
||||
|
||||
if (suggestionsState){
|
||||
if (suggestionsList){
|
||||
suggestionsList.addEventListener('click', function(evt){
|
||||
var target = evt.target.closest('[data-partner-suggestion]');
|
||||
if (!target){ return; }
|
||||
evt.preventDefault();
|
||||
var mode = target.getAttribute('data-mode') || 'partner';
|
||||
var name = target.getAttribute('data-name') || '';
|
||||
applySuggestionSelection(mode, name);
|
||||
});
|
||||
}
|
||||
if (suggestionsMoreButton){
|
||||
suggestionsMoreButton.addEventListener('click', function(){
|
||||
revealHiddenSuggestions();
|
||||
});
|
||||
}
|
||||
if (suggestionsRefreshButton){
|
||||
suggestionsRefreshButton.addEventListener('click', function(){
|
||||
fetchSuggestions({ includeHidden: suggestionsState.expanded, forceRefresh: true });
|
||||
});
|
||||
}
|
||||
if (suggestionsState.visible && suggestionsState.visible.length){
|
||||
renderSuggestions();
|
||||
} else if (suggestionsState.error){
|
||||
updateSuggestionsMeta();
|
||||
} else {
|
||||
fetchSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
if (clearBtn){
|
||||
clearBtn.addEventListener('click', function(){
|
||||
selects.forEach(function(sel){ if (sel){ sel.value = ''; } });
|
||||
if (autoToggle){ setOptOut(true); }
|
||||
setSelectionSource('');
|
||||
renderFromServer();
|
||||
requestPreviewUpdate();
|
||||
markSuggestionActive();
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.newDeckPartnerState){
|
||||
try {
|
||||
var restore = window.newDeckPartnerState;
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (partnerSel && restore.secondary){ partnerSel.value = restore.secondary; }
|
||||
if (bgSel && restore.background){ bgSel.value = restore.background; }
|
||||
if (restore.enabled === false){ selects.forEach(function(sel){ if (sel){ sel.value = ''; } }); }
|
||||
if (partnerSel && partnerSel.value){ renderFromSelection(partnerSel); }
|
||||
else if (bgSel && bgSel.value){ renderFromSelection(bgSel, 'background'); }
|
||||
delete window.newDeckPartnerState;
|
||||
} catch(_){ }
|
||||
}
|
||||
|
||||
if (optInput && optInput.value === '1'){
|
||||
setOptOut(true);
|
||||
renderFromServer();
|
||||
} else {
|
||||
setOptOut(!defaultPartner);
|
||||
if (defaultPartner){ syncBySelection(); }
|
||||
else if (serverPayload){ renderFromServer(); }
|
||||
}
|
||||
markSuggestionActive();
|
||||
|
||||
try {
|
||||
var slot = document.getElementById('newdeck-tags-slot');
|
||||
if (slot) slot.setAttribute('data-has-content', '1');
|
||||
} catch(_){ }
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue