function initBuilderPanel() { (function () { var xEl = document.getElementById('pos-x'), yEl = document.getElementById('pos-y'); var wEl = document.getElementById('pos-w'), 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() { var canvasEl = document.getElementById('el-' + CURRENT_SELECTED_ID); var x = parseFloat(xEl.value) || 0, y = parseFloat(yEl.value) || 0; var w = parseFloat(wEl.value) || 1, 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(function (el) { el.addEventListener('change', apply); }); })(); bindAspectButtons(); bindScreenBackgroundMusicField(); document.querySelectorAll('.uploadInput').forEach(function (input) { input.addEventListener('change', function () { if (!input.files.length) return; var status = input.parentElement.querySelector('.uploadStatus'); var targetField = document.getElementById(input.dataset.target); status.textContent = 'Envoi en cours...'; var data = new FormData(); data.append('file', input.files[0]); fetch("/game/" + GAME_SLUG + "/upload", { method: 'POST', body: data }) .then(function (r) { return r.json(); }) .then(function (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(function () { status.textContent = 'Fichier envoyé — Enregistré.'; }); } else { status.textContent = "Échec de l'envoi."; } }) .catch(function () { 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(function (input) { input.addEventListener('change', function () { if (!input.files.length) return; var status = input.parentElement.querySelector('.uploadStatus'); var targetField = document.getElementById(input.dataset.target); status.textContent = 'Envoi en cours...'; var data = new FormData(); data.append('file', input.files[0]); fetch("/game/" + GAME_SLUG + "/upload", { method: 'POST', body: data }) .then(function (r) { return r.json(); }) .then(function (json) { if (json.url) { targetField.value = json.url; status.textContent = 'Fichier envoyé.'; } else { status.textContent = "Échec de l'envoi."; } }) .catch(function () { 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(); var 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() { var defSel = document.getElementById('field-definition_id'); var rowSel = document.getElementById('field-row_id'); var champSel = document.getElementById('field-champ'); var champNomSel = document.getElementById('field-champ_nom'); if (!defSel || !rowSel || !champSel || !champNomSel) return; // pas la Jauge defSel.addEventListener('change', function () { var info = (DEFINITIONS_DATA || {})[defSel.value] || { rows: [], fields: [] }; rowSel.innerHTML = info.rows.length ? info.rows.map(function (r) { return ''; }).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). var simpleFields = info.fields.filter(function (f) { return f.type !== 'relation'; }); var fieldOptions = simpleFields.map(function (f) { return ''; }).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) { var mode = sel.value; document.querySelectorAll('.visCondRow').forEach(function (row) { var scope = row.dataset.visCondScope; var show = scope === 'any' ? !!mode : scope === mode; row.style.display = show ? '' : 'none'; }); } function bindDefinitionFieldSelects(defSelId, fieldSelIds) { var defSel = document.getElementById(defSelId); if (!defSel) return; var fieldSels = fieldSelIds.map(function (id) { return document.getElementById(id); }).filter(Boolean); if (!fieldSels.length) return; defSel.addEventListener('change', function () { var info = (DEFINITIONS_DATA || {})[defSel.value] || { fields: [] }; var opts = '' + info.fields.map(function (f) { return ''; }).join(''); fieldSels.forEach(function (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