function initBuilderPanel() { (function () { const xEl = document.getElementById('pos-x'); const yEl = document.getElementById('pos-y'); const wEl = document.getElementById('pos-w'); const hEl = document.getElementById('pos-h'); // pos-w/pos-h n'existent pas pour un élément posé dans un conteneur (sa // taille se règle via les champs "Largeur/Hauteur (%)"/"fixe (px)" du // formulaire général à la place, voir size_ctrls plus haut) — X/Y non // plus, mais ils restent dans le DOM (juste désactivés) pour cet élément. if (!xEl || !wEl) return; function apply() { const canvasEl = document.getElementById(`el-${CURRENT_SELECTED_ID}`); const x = parseFloat(xEl.value) || 0; const y = parseFloat(yEl.value) || 0; const w = parseFloat(wEl.value) || 1; const h = parseFloat(hEl.value) || 1; if (canvasEl) { canvasEl.style.left = `${x}%`; canvasEl.style.top = `${y}%`; canvasEl.style.width = `${w}%`; canvasEl.style.height = `${h}%`; } saveGeometry(CURRENT_SELECTED_ID, x, y, w, h); } [xEl, yEl, wEl, hEl].forEach((el) => { el.addEventListener('change', apply); }); }()); bindAspectButtons(); bindScreenBackgroundMusicField(); document.querySelectorAll('.uploadInput').forEach((input) => { input.addEventListener('change', () => { if (!input.files.length) return; const status = input.parentElement.querySelector('.uploadStatus'); const targetField = document.getElementById(input.dataset.target); status.textContent = 'Envoi en cours...'; const data = new FormData(); data.append('file', input.files[0]); fetch(`/game/${GAME_SLUG}/upload`, { method: 'POST', body: data }) .then((r) => r.json()) .then((json) => { if (json.url) { targetField.value = json.url; status.textContent = 'Fichier envoyé — Enregistrement...'; // Enregistre tout de suite, comme n'importe quel autre réglage — // un fichier envoyé ne doit jamais rester "en attente". submitPropsForm().then(() => { status.textContent = 'Fichier envoyé — Enregistré.'; }); } else { status.textContent = "Échec de l'envoi."; } }) .catch(() => { status.textContent = "Échec de l'envoi."; }); }); }); // Phase 6 — champ d'upload DANS le formulaire de nœud (action "Jouer un // son") : contrairement à .uploadInput (panneau de propriétés d'un // élément), il n'y a pas de formulaire à autosauvegarder ici — le champ // texte est simplement rempli, la valeur part avec le reste du nœud au // clic sur "Créer" (voir submitNodeForm()). document.querySelectorAll('.nodeFormUploadInput').forEach((input) => { input.addEventListener('change', () => { if (!input.files.length) return; const status = input.parentElement.querySelector('.uploadStatus'); const targetField = document.getElementById(input.dataset.target); status.textContent = 'Envoi en cours...'; const data = new FormData(); data.append('file', input.files[0]); fetch(`/game/${GAME_SLUG}/upload`, { method: 'POST', body: data }) .then((r) => r.json()) .then((json) => { if (json.url) { targetField.value = json.url; status.textContent = 'Fichier envoyé.'; } else { status.textContent = "Échec de l'envoi."; } }) .catch(() => { status.textContent = "Échec de l'envoi."; }); }); }); bindPropsAutosave(); bindJaugeDefinitionSelect(); bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']); bindDefinitionFieldSelects('field-visibilite_cond_definition_id', ['field-visibilite_cond_champ']); bindClauseListDefinitionSelect('field-data_definition_id'); bindClauseListRows(); initFilterValuePickers(); const visCondModeSel = document.getElementById('field-visibilite_cond_mode'); if (visCondModeSel) toggleVisCondFields(visCondModeSel); restoreTreeCollapsedState(); applySelectionHighlight(CURRENT_SELECTED_ID); restoreFloatPanelState('props', 'propsPanelFloat', 'propsPanelReopenBtn'); restoreFloatPanelState('left', 'panelLeftFloat', 'panelLeftReopenBtn'); } // Jauge : quand on change l'objet de données à suivre SANS quitter le // panneau (donc sans le rechargement serveur qui repeuplerait row_id/champ/ // champ_nom via controls_with_values.py), on reconstruit ces trois menus à // la volée à partir de DEFINITIONS_DATA (même source que les formulaires de // la Logique de la scène, voir onFlowDefinitionChange). DEFINITIONS_DATA // est déclaré plus bas dans ce script (donc encore "undefined" au moment où // initBuilderPanel() tourne en tout premier) — sans conséquence ici, // puisqu'on ne le lit que dans le gestionnaire, jamais au moment du bind. // Libellé d'un champ dans une liste déroulante — précise l'objet visé pour // un champ "relation" (ex. "parcour (→ parcours)"), même règle que // _field_label() côté Python (controls_with_values.py). function fieldOptionLabel(f) { return (f.type === 'relation' && f.relation_definition_name) ? (`${f.name} (→ ${f.relation_definition_name})`) : f.name; } function bindJaugeDefinitionSelect() { const defSel = document.getElementById('field-definition_id'); const rowSel = document.getElementById('field-row_id'); const champSel = document.getElementById('field-champ'); const champNomSel = document.getElementById('field-champ_nom'); if (!defSel || !rowSel || !champSel || !champNomSel) return; // pas la Jauge defSel.addEventListener('change', () => { const info = (DEFINITIONS_DATA || {})[defSel.value] || { rows: [], fields: [] }; rowSel.innerHTML = info.rows.length ? info.rows.map((r) => ``).join('') : ''; // La Jauge affiche une valeur/un nom tel quel — un champ "relation" (un // id, pas une valeur) n'a pas sa place ici, même exclusion que côté // serveur (voir controls_with_values.py). const simpleFields = info.fields.filter((f) => f.type !== 'relation'); const fieldOptions = simpleFields.map((f) => ``).join(''); champSel.innerHTML = fieldOptions || ''; champNomSel.innerHTML = `${fieldOptions}`; }); } // ---------- Réglages "champ" d'un filtre (Répéteur/Donnée liée) : listes // déroulantes peuplées avec les VRAIS champs de l'objet choisi, jamais // tapées à la main — même principe que bindJaugeDefinitionSelect() ci-dessus, // généralisé pour s'appliquer à un nombre quelconque de listes "champ" // rattachées à un même sélecteur d'objet (ici : filtre_champ + filtre2_champ, // ou data_filtre_champ + data_filtre2_champ). ---------- // ---------- Condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) : // n'affiche que les réglages pertinents pour le mode choisi (variable // globale, ou champ d'un objet) — les deux autres restent en base (peu // importe, seul le mode courant est lu par is_element_visible()) mais sont // masqués pour ne pas embrouiller. ---------- function toggleVisCondFields(sel) { const mode = sel.value; document.querySelectorAll('.visCondRow').forEach((row) => { const scope = row.dataset.visCondScope; const show = scope === 'any' ? !!mode : scope === mode; row.style.display = show ? '' : 'none'; }); } function bindDefinitionFieldSelects(defSelId, fieldSelIds) { const defSel = document.getElementById(defSelId); if (!defSel) return; const fieldSels = fieldSelIds.map((id) => document.getElementById(id)).filter(Boolean); if (!fieldSels.length) return; defSel.addEventListener('change', () => { const info = (DEFINITIONS_DATA || {})[defSel.value] || { fields: [] }; const opts = `${ info.fields.map((f) => ``).join('')}`; fieldSels.forEach((sel) => { sel.innerHTML = opts; }); }); } // ---------- "Donnée liée" : liste de conditions ILLIMITÉE (champ/opérateur/ // valeur), combinées entre elles par ET/OU (voir c_clause_list.py, // save_element_controls.py, resolve_bound_row.py). Chaque ligne existante // est rendue côté serveur (au chargement du panneau) ; "+ Ajouter une // condition" clone un