screens/labels/data_operations.py : 6 nouvelles opérations pour "Modifier une donnée"/"Modifier une variable" — multiplier, diviser, modulo (garde-fou division par zéro : valeur inchangée plutôt qu'une ZeroDivisionError qui interromprait le graphe), minimum/maximum (borne la valeur ACTUELLE — utile pour une variable globale, qui n'a pas de min_value/max_value comme un champ d'objet), et alea (tire un nombre aléatoire entre deux bornes). screens/data_actions/compute_operation.py (déjà factorisé en Phase 0, donc une seule implémentation pour apply_data_action.py/ apply_variable_action.py) : implémente les 6. "alea" est la seule à deux opérandes — réutilise data_value au format "min,max" plutôt qu'une nouvelle colonne de nœud (bornes remises dans l'ordre si inversées). random.uniform pour un résultat décimal, random.randint pour un entier. static/js/screen_edit/flow-editor.js + templates/screen_edit.html : petit indice visuel — le champ "Valeur / montant" du formulaire de nœud affiche "min,max (ex. 1,6)" quand "alea" est choisi, pour ne pas laisser deviner ce format à deux nombres, différent de toutes les autres opérations. Sinon aucun nouveau champ/changement de schéma nécessaire, le <select> était déjà généré depuis DATA_OPERATIONS. Vérifié : 250 tests passent (19 dans test_compute_operation.py, dont un qui a dû être corrigé — il utilisait "multiplier" comme exemple d'opération INCONNUE, devenu un mauvais exemple maintenant qu'elle existe), 13 tests node:test toujours au vert, syntaxe JS validée. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
630 lines
34 KiB
JavaScript
630 lines
34 KiB
JavaScript
// ---------- Logique de la scène : éditeur de flow à nœuds ----------
|
|
//
|
|
// FLOW_NODES_INITIAL/FLOW_EDGES_INITIAL/FLOW_BLOCKS/ELEMENTS_LABELS/
|
|
// SCREENS_MAP/*_LABELS_MAP/CUSTOM_EVENTS_MAP/GAME_SLUG/SCREEN_ID sont
|
|
// posés par le petit <script> inline en tête de screen_edit.html (données
|
|
// Jinja) — voir static/js/play/ pour le même patron déjà en place côté
|
|
// moteur de jeu.
|
|
var FLOW_SCREEN_ID = SCREEN_ID;
|
|
// Blocs de logique (voir screens/flow/blocks/) : ALL_FLOW porte TOUS les
|
|
// nœuds/arêtes de cet écran, tous blocs confondus (chargement inchangé) ;
|
|
// un seul bloc est ouvert à la fois (modale unique, à la Unity) — pas
|
|
// besoin d'une seconde copie filtrée à tenir synchronisée, currentBlockNodes()/
|
|
// currentBlockEdges() recalculent la vue du bloc ouvert à chaque rendu.
|
|
var ALL_FLOW = {
|
|
nodes: FLOW_NODES_INITIAL,
|
|
edges: FLOW_EDGES_INITIAL,
|
|
};
|
|
var CURRENT_BLOCK_ID = null;
|
|
|
|
function currentBlockNodes() {
|
|
return ALL_FLOW.nodes.filter(function(n) { return n.block_id === CURRENT_BLOCK_ID; });
|
|
}
|
|
function currentBlockEdges() {
|
|
var ids = {};
|
|
currentBlockNodes().forEach(function(n) { ids[n.id] = true; });
|
|
return ALL_FLOW.edges.filter(function(e) { return ids[e.from_node_id] && ids[e.to_node_id]; });
|
|
}
|
|
|
|
var flowPendingPort = null; // {nodeId, port} en attente de connexion
|
|
|
|
function flowUrl(path) { return "/game/" + GAME_SLUG + path; }
|
|
|
|
function nodeLabel(node) {
|
|
if (node.node_type === 'trigger') {
|
|
if (node.trigger_event === 'affichage') return '🖥️ ' + TRIGGER_LABELS_MAP['affichage'];
|
|
if (node.trigger_event === 'evenement') {
|
|
var listenedEvent = CUSTOM_EVENTS_MAP[node.trigger_custom_event_id];
|
|
return '📣 Sur « ' + (listenedEvent ? listenedEvent.name : '?') + ' »';
|
|
}
|
|
return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' +
|
|
(ELEMENTS_LABELS[node.trigger_element_id] || '?');
|
|
}
|
|
if (node.node_type === 'condition') {
|
|
var clauseSubject = function(c, field, variable) {
|
|
return (c.source ?? c.cond_source) === 'variable' ? ('🔧 ' + (variable || c.cond_variable || '?')) : (field || c.cond_field || '?');
|
|
};
|
|
var mainSubject = clauseSubject(node, node.cond_field, node.cond_variable);
|
|
var mainClause = '❓ SI ' + mainSubject + ' ' + (CONDITION_OPERATOR_LABELS_MAP[node.cond_operator] || node.cond_operator) + ' ' + (node.cond_value || '');
|
|
if (node.cond_clauses) {
|
|
// 2.4 — plusieurs clauses combinées : on affiche chaque champ/variable
|
|
// testé, relié par le connecteur ET/OU choisi, plutôt qu'une seule clause.
|
|
try {
|
|
var clauses = JSON.parse(node.cond_clauses);
|
|
var combLabel = node.cond_combinator === 'ou' ? ' OU ' : ' ET ';
|
|
return '❓ SI ' + clauses.map(function(c) {
|
|
return clauseSubject(c, c.field, c.variable) + ' ' + (CONDITION_OPERATOR_LABELS_MAP[c.operator] || c.operator) + ' ' + (c.value || '');
|
|
}).join(combLabel);
|
|
} catch (e) { /* JSON invalide : on retombe sur la clause historique */ }
|
|
}
|
|
return mainClause;
|
|
}
|
|
// action
|
|
var base = '⚡ ' + (ACTION_TYPE_LABELS_MAP[node.action_type] || node.action_type);
|
|
if (node.action_type === 'aller_a') {
|
|
base += ' → ' + (SCREENS_MAP[node.target_screen_id] || '?');
|
|
} else if (node.action_type === 'ouvrir_ligne') {
|
|
base += ' → ' + (SCREENS_MAP[node.target_screen_id] || '?') + ' (ligne cliquée)';
|
|
} else if (node.action_type === 'activer_onglet') {
|
|
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' (masque ses frères)';
|
|
} else if (node.action_type === 'modifier_element') {
|
|
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property);
|
|
if (node.element_property === 'visibilite') {
|
|
base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value);
|
|
} else if (node.element_property === 'surbrillance') {
|
|
base += ' = ' + (ELEMENT_SURBRILLANCE_LABELS_MAP[node.element_value] || node.element_value);
|
|
} else if (node.element_property === 'desactive') {
|
|
base += ' = ' + (ELEMENT_DESACTIVE_LABELS_MAP[node.element_value] || node.element_value);
|
|
} else if (node.element_value && node.element_value.indexOf('||') !== -1) {
|
|
var vparts = node.element_value.split('||');
|
|
base += ' = ' + vparts[0] + ' ↔ ' + vparts[1] + ' (va-et-vient)';
|
|
} else {
|
|
base += ' = ' + node.element_value;
|
|
}
|
|
} else if (node.action_type === 'modifier_donnee') {
|
|
base += ' → ' + (node.target_field || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
|
if (node.data_value) base += ' ' + node.data_value;
|
|
} else if (node.action_type === 'modifier_variable') {
|
|
base += ' → ' + (node.target_variable || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
|
if (node.data_value) base += ' ' + node.data_value;
|
|
} else if (node.action_type === 'attendre') {
|
|
base += ' (' + (node.data_value || '0') + ' s)';
|
|
} else if (node.action_type === 'declencher_evenement') {
|
|
// Notification pure : rien d'autre à afficher que le nom de
|
|
// l'événement (voir screens/custom_events/) — aucun élément/ligne
|
|
// n'est jamais transmis au déclenchement.
|
|
var dispatchedEvent = CUSTOM_EVENTS_MAP[node.target_custom_event_id];
|
|
base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »';
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function renderFlow() {
|
|
var canvas = document.getElementById('flowCanvas');
|
|
var svg = document.getElementById('flowEdges');
|
|
canvas.innerHTML = '';
|
|
svg.innerHTML = '';
|
|
|
|
currentBlockNodes().forEach(function(node) {
|
|
var div = document.createElement('div');
|
|
div.className = 'flowNode flowNode-' + node.node_type;
|
|
div.style.left = node.x + 'px';
|
|
div.style.top = node.y + 'px';
|
|
div.dataset.nodeId = node.id;
|
|
|
|
var del = document.createElement('button');
|
|
del.className = 'flowNodeDelete';
|
|
del.type = 'button';
|
|
del.textContent = '✕';
|
|
del.onclick = function(ev) { ev.stopPropagation(); deleteFlowNode(node.id); };
|
|
div.appendChild(del);
|
|
|
|
var body = document.createElement('div');
|
|
body.className = 'flowNodeBody';
|
|
body.textContent = nodeLabel(node);
|
|
div.appendChild(body);
|
|
|
|
if (node.node_type !== 'trigger') {
|
|
var inPort = document.createElement('div');
|
|
inPort.className = 'flowPort flowPortIn';
|
|
inPort.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'in', null); };
|
|
div.appendChild(inPort);
|
|
}
|
|
if (node.node_type === 'condition') {
|
|
var outTrue = document.createElement('div');
|
|
outTrue.className = 'flowPort flowPortOut flowPortTrue';
|
|
outTrue.title = 'Vrai';
|
|
outTrue.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'vrai'); };
|
|
div.appendChild(outTrue);
|
|
var outFalse = document.createElement('div');
|
|
outFalse.className = 'flowPort flowPortOut flowPortFalse';
|
|
outFalse.title = 'Faux';
|
|
outFalse.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'faux'); };
|
|
div.appendChild(outFalse);
|
|
} else {
|
|
var outPort = document.createElement('div');
|
|
outPort.className = 'flowPort flowPortOut';
|
|
outPort.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'out'); };
|
|
div.appendChild(outPort);
|
|
}
|
|
|
|
div.addEventListener('mousedown', function(e) { onFlowNodeMouseDown(e, node.id); });
|
|
canvas.appendChild(div);
|
|
});
|
|
|
|
// Redessine les fils après que les nœuds soient dans le DOM (pour lire leur position réelle).
|
|
requestAnimationFrame(drawFlowEdges);
|
|
}
|
|
|
|
function portPosition(nodeId, side, sub) {
|
|
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
|
|
if (!div) return null;
|
|
var wrap = document.querySelector('.flowCanvasWrap');
|
|
var wrapRect = wrap.getBoundingClientRect();
|
|
var selector = side === 'in' ? '.flowPortIn' : (sub === 'vrai' ? '.flowPortTrue' : (sub === 'faux' ? '.flowPortFalse' : '.flowPortOut'));
|
|
var port = div.querySelector(selector);
|
|
if (!port) return null;
|
|
var r = port.getBoundingClientRect();
|
|
return { x: r.left + r.width / 2 - wrapRect.left + wrap.scrollLeft, y: r.top + r.height / 2 - wrapRect.top + wrap.scrollTop };
|
|
}
|
|
|
|
function drawFlowEdges() {
|
|
var svg = document.getElementById('flowEdges');
|
|
svg.innerHTML = '';
|
|
currentBlockEdges().forEach(function(edge) {
|
|
var from = portPosition(edge.from_node_id, 'out', edge.from_port);
|
|
var to = portPosition(edge.to_node_id, 'in', null);
|
|
if (!from || !to) return;
|
|
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', from.x); line.setAttribute('y1', from.y);
|
|
line.setAttribute('x2', to.x); line.setAttribute('y2', to.y);
|
|
line.setAttribute('class', 'flowEdgeLine' + (edge.from_port === 'faux' ? ' flowEdgeFalse' : (edge.from_port === 'vrai' ? ' flowEdgeTrue' : '')));
|
|
line.style.pointerEvents = 'stroke';
|
|
line.addEventListener('click', function() { deleteFlowEdge(edge.id); });
|
|
svg.appendChild(line);
|
|
});
|
|
}
|
|
|
|
function onPortClick(nodeId, side, port) {
|
|
if (side === 'out') {
|
|
flowPendingPort = { nodeId: nodeId, port: port };
|
|
return;
|
|
}
|
|
// side === 'in'
|
|
if (!flowPendingPort) return;
|
|
if (flowPendingPort.nodeId === nodeId) { flowPendingPort = null; return; }
|
|
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/edges/add'), {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId })
|
|
}).then(function(r){ return r.json(); }).then(function(edge){
|
|
ALL_FLOW.edges.push({ id: edge.id, screen_id: FLOW_SCREEN_ID, from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId });
|
|
flowPendingPort = null;
|
|
drawFlowEdges();
|
|
});
|
|
}
|
|
|
|
function deleteFlowEdge(edgeId) {
|
|
fetch(flowUrl('/flow/edges/' + edgeId + '/delete'), { method: 'POST' }).then(function(){
|
|
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.id !== edgeId; });
|
|
drawFlowEdges();
|
|
});
|
|
}
|
|
|
|
function deleteFlowNode(nodeId) {
|
|
fetch(flowUrl('/flow/nodes/' + nodeId + '/delete'), { method: 'POST' }).then(function(){
|
|
ALL_FLOW.nodes = ALL_FLOW.nodes.filter(function(n){ return n.id !== nodeId; });
|
|
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.from_node_id !== nodeId && e.to_node_id !== nodeId; });
|
|
renderFlow();
|
|
});
|
|
}
|
|
|
|
function onFlowNodeMouseDown(e, nodeId) {
|
|
if (e.target.classList.contains('flowPort') || e.target.classList.contains('flowNodeDelete')) return;
|
|
e.preventDefault();
|
|
var node = ALL_FLOW.nodes.find(function(n){ return n.id === nodeId; });
|
|
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
|
|
var startX = e.clientX, startY = e.clientY;
|
|
var startLeft = node.x, startTop = node.y;
|
|
function onMove(ev) {
|
|
var dx = ev.clientX - startX, dy = ev.clientY - startY;
|
|
node.x = Math.max(0, startLeft + dx);
|
|
node.y = Math.max(0, startTop + dy);
|
|
div.style.left = node.x + 'px';
|
|
div.style.top = node.y + 'px';
|
|
drawFlowEdges();
|
|
}
|
|
function onUp() {
|
|
document.removeEventListener('mousemove', onMove);
|
|
document.removeEventListener('mouseup', onUp);
|
|
fetch(flowUrl('/flow/nodes/' + nodeId + '/move'), {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({ x: node.x, y: node.y })
|
|
});
|
|
}
|
|
document.addEventListener('mousemove', onMove);
|
|
document.addEventListener('mouseup', onUp);
|
|
}
|
|
|
|
// ---------- Formulaire d'ajout de nœud ----------
|
|
|
|
var currentNodeFormType = null;
|
|
|
|
function openNodeForm(type) {
|
|
currentNodeFormType = type;
|
|
document.getElementById('nodeForm').style.display = 'block';
|
|
document.getElementById('nodeFormTitle').textContent =
|
|
type === 'trigger' ? 'Nouveau déclencheur' : (type === 'condition' ? 'Nouvelle condition' : 'Nouvelle action');
|
|
document.getElementById('nodeFormTrigger').style.display = type === 'trigger' ? 'block' : 'none';
|
|
document.getElementById('nodeFormCondition').style.display = type === 'condition' ? 'block' : 'none';
|
|
document.getElementById('nodeFormAction').style.display = type === 'action' ? 'block' : 'none';
|
|
if (type === 'trigger') toggleFlowTriggerFields();
|
|
if (type === 'condition') {
|
|
document.getElementById('nf-cond-source').value = 'objet';
|
|
onFlowConditionSourceChange();
|
|
onFlowDefinitionChange('cond');
|
|
flowConditionExtraClauses = [];
|
|
renderFlowConditionClauses();
|
|
}
|
|
if (type === 'action') {
|
|
document.getElementById('nf-action-element-toggle').checked = false;
|
|
toggleFlowActionFields();
|
|
}
|
|
}
|
|
|
|
function toggleFlowTriggerFields() {
|
|
var eventVal = document.getElementById('nf-trigger-event').value;
|
|
var isAffichage = eventVal === 'affichage';
|
|
// "Sur un événement personnalisé" ne concerne ni un élément précis ni
|
|
// l'écran entier : c'est un scan global (voir dispatchGameEvent() dans
|
|
// play.html), le seul réglage propre à ce nœud est QUEL événement.
|
|
var isCustomEvent = eventVal === 'evenement';
|
|
document.getElementById('nf-trigger-element-row').style.display = (isAffichage || isCustomEvent) ? 'none' : 'block';
|
|
document.getElementById('nf-trigger-repeater-hint').style.display = (isAffichage || isCustomEvent) ? 'none' : 'block';
|
|
document.getElementById('nf-trigger-affichage-hint').style.display = isAffichage ? 'block' : 'none';
|
|
document.getElementById('nf-trigger-event-row').style.display = isCustomEvent ? 'block' : 'none';
|
|
}
|
|
|
|
function closeNodeForm() {
|
|
document.getElementById('nodeForm').style.display = 'none';
|
|
currentNodeFormType = null;
|
|
}
|
|
|
|
// CLICKED_ROW_ID (-1, voir screens/flow/constants.py) : "la ligne de
|
|
// Répéteur sur laquelle on vient de cliquer" — pour un déclencheur "Au
|
|
// clic" posé sur un Répéteur, chaque ligne exécute le MÊME graphe et la
|
|
// ligne cliquée n'est jamais connue à l'avance ; choisir une ligne fixe
|
|
// dans l'éditeur n'a donc pas de sens pour ce cas. Résolue au moment de
|
|
// l'exécution (window.lastClickedRowId, voir play.html), pas à la
|
|
// création du nœud — proposée en tête de TOUTE liste "Ligne concernée",
|
|
// avant les lignes réelles de l'objet.
|
|
var FLOW_CLICKED_ROW_ID = -1;
|
|
|
|
function _flowRowOptionsHtml(info) {
|
|
var options = '<option value="' + FLOW_CLICKED_ROW_ID + '">🖱️ Ligne cliquée (Répéteur)</option>';
|
|
options += info.rows.length
|
|
? info.rows.map(function(r){ return '<option value="' + r.id + '">' + r.label + '</option>'; }).join('')
|
|
: '<option value="" disabled>Aucune donnée dans cet objet</option>';
|
|
return options;
|
|
}
|
|
|
|
function onFlowDefinitionChange(prefix) {
|
|
var defSel = document.getElementById(prefix === 'cond' ? 'nf-cond-definition' : 'nf-action-data-definition');
|
|
if (!defSel) return;
|
|
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
|
|
var rowSel = document.getElementById(prefix === 'cond' ? 'nf-cond-row' : 'nf-action-data-row');
|
|
var fieldSel = document.getElementById(prefix === 'cond' ? 'nf-cond-field' : 'nf-action-data-field');
|
|
rowSel.innerHTML = _flowRowOptionsHtml(info);
|
|
fieldSel.innerHTML = info.fields.length
|
|
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
|
|
: '<option value="">Aucun champ simple</option>';
|
|
}
|
|
|
|
function onFlowFieldChange() {
|
|
// Pas d'action nécessaire ici pour l'instant : le type du champ est relu
|
|
// directement sur l'option sélectionnée au moment de la création du nœud.
|
|
}
|
|
|
|
// Bascule la clause PRINCIPALE d'un nœud Condition entre "Champ d'un
|
|
// objet" (historique) et "Variable globale" — voir ensure_flow_schema.py
|
|
// (cond_source) et evaluateConditionClause() dans play.html. Même bascule
|
|
// pour une clause SUPPLÉMENTAIRE : voir onFlowClauseSourceChange() plus
|
|
// bas, sur le même principe mais scopée par index.
|
|
function onFlowConditionSourceChange() {
|
|
var isVariable = document.getElementById('nf-cond-source').value === 'variable';
|
|
document.getElementById('nf-cond-objet-fields').style.display = isVariable ? 'none' : 'block';
|
|
document.getElementById('nf-cond-variable-fields').style.display = isVariable ? 'block' : 'none';
|
|
var hint = document.getElementById('nf-cond-no-objet-hint');
|
|
if (hint) hint.style.display = isVariable ? 'none' : 'block';
|
|
}
|
|
|
|
// ---------- 2.4 — clauses supplémentaires (ET/OU) d'un nœud Condition ----------
|
|
// Chaque clause supplémentaire réutilise EXACTEMENT le même modèle que la
|
|
// clause principale (nf-cond-definition/row/field/operator/value) mais dans
|
|
// une ligne indépendante, numérotée par index dans flowConditionExtraClauses.
|
|
var flowConditionExtraClauses = [];
|
|
|
|
function addFlowConditionClause() {
|
|
var firstDefSel = document.getElementById('nf-cond-definition');
|
|
var defaultDefId = firstDefSel && firstDefSel.value ? firstDefSel.value : '';
|
|
flowConditionExtraClauses.push({
|
|
source: 'objet', definition_id: defaultDefId, row_id: '', field: '', field_type: '',
|
|
variable: '', variable_chemin: '', operator: 'egal', value: '',
|
|
});
|
|
renderFlowConditionClauses();
|
|
}
|
|
|
|
function removeFlowConditionClause(idx) {
|
|
flowConditionExtraClauses.splice(idx, 1);
|
|
renderFlowConditionClauses();
|
|
}
|
|
|
|
// Bascule une clause SUPPLÉMENTAIRE (indexée) entre "Champ d'un objet" et
|
|
// "Variable globale" — même principe que onFlowConditionSourceChange() pour
|
|
// la clause principale, scopé par index plutôt que par id fixe.
|
|
function onFlowClauseSourceChange(idx) {
|
|
var sel = document.getElementById('nf-cond-extra-source-' + idx);
|
|
if (!sel) return;
|
|
flowConditionExtraClauses[idx].source = sel.value;
|
|
var isVariable = sel.value === 'variable';
|
|
var objetFields = document.getElementById('nf-cond-extra-objet-fields-' + idx);
|
|
var varFields = document.getElementById('nf-cond-extra-variable-fields-' + idx);
|
|
if (objetFields) objetFields.style.display = isVariable ? 'none' : 'block';
|
|
if (varFields) varFields.style.display = isVariable ? 'block' : 'none';
|
|
}
|
|
|
|
function renderFlowConditionClauses() {
|
|
var wrap = document.getElementById('nf-cond-extra-clauses');
|
|
var combinatorRow = document.getElementById('nf-cond-combinator-row');
|
|
if (!wrap || !combinatorRow) return;
|
|
combinatorRow.style.display = flowConditionExtraClauses.length ? 'block' : 'none';
|
|
var defOptionsHtml = document.getElementById('nf-cond-definition') ? document.getElementById('nf-cond-definition').innerHTML : '';
|
|
wrap.innerHTML = flowConditionExtraClauses.map(function(clause, idx) {
|
|
var isVariable = clause.source === 'variable';
|
|
return '' +
|
|
'<div class="flowConditionClauseRow" style="border-top:1px dashed var(--border); margin-top:8px; padding-top:8px;">' +
|
|
'<label>Source<select id="nf-cond-extra-source-' + idx + '" onchange="onFlowClauseSourceChange(' + idx + ')">' +
|
|
'<option value="objet"' + (isVariable ? '' : ' selected') + '>Champ d\'un objet</option>' +
|
|
'<option value="variable"' + (isVariable ? ' selected' : '') + '>Variable globale</option>' +
|
|
'</select></label>' +
|
|
'<div id="nf-cond-extra-objet-fields-' + idx + '" style="display:' + (isVariable ? 'none' : 'block') + ';">' +
|
|
'<label>Objet<select id="nf-cond-extra-definition-' + idx + '" onchange="onFlowClauseDefinitionChange(' + idx + ')">' + defOptionsHtml + '</select></label>' +
|
|
'<label>Ligne concernée<select id="nf-cond-extra-row-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].row_id = this.value"></select></label>' +
|
|
'<label>Champ<select id="nf-cond-extra-field-' + idx + '" onchange="onFlowClauseFieldChange(' + idx + ')"></select></label>' +
|
|
'</div>' +
|
|
'<div id="nf-cond-extra-variable-fields-' + idx + '" style="display:' + (isVariable ? 'block' : 'none') + ';">' +
|
|
'<label>Variable<select id="nf-cond-extra-variable-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].variable = this.value">' +
|
|
'<option value="">(sélectionner une variable)</option>' + GLOBAL_VARIABLES_OPTIONS_HTML +
|
|
'</select></label>' +
|
|
'<label>Chemin (optionnel)<input type="text" id="nf-cond-extra-variable-chemin-' + idx + '" placeholder="ex. .nom ou [0].valeur" value="' + (clause.variable_chemin || '').replace(/"/g, '"') + '" onchange="flowConditionExtraClauses[' + idx + '].variable_chemin = this.value"></label>' +
|
|
'</div>' +
|
|
'<label>Condition<select id="nf-cond-extra-operator-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].operator = this.value">' +
|
|
CONDITION_OPERATORS_HTML +
|
|
'</select></label>' +
|
|
'<label>Valeur de comparaison<input type="text" id="nf-cond-extra-value-' + idx + '" placeholder="ex. 1, vrai, 10..." value="' + (clause.value || '').replace(/"/g, '"') + '" onchange="flowConditionExtraClauses[' + idx + '].value = this.value"></label>' +
|
|
'<button type="button" class="smallBtn button is-small" onclick="removeFlowConditionClause(' + idx + ')">Retirer cette condition</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
flowConditionExtraClauses.forEach(function(clause, idx) {
|
|
var defSel = document.getElementById('nf-cond-extra-definition-' + idx);
|
|
if (defSel && clause.definition_id) defSel.value = clause.definition_id;
|
|
onFlowClauseDefinitionChange(idx);
|
|
var opSel = document.getElementById('nf-cond-extra-operator-' + idx);
|
|
if (opSel) opSel.value = clause.operator || 'egal';
|
|
var varSel = document.getElementById('nf-cond-extra-variable-' + idx);
|
|
if (varSel && clause.variable) varSel.value = clause.variable;
|
|
});
|
|
}
|
|
|
|
function onFlowClauseDefinitionChange(idx) {
|
|
var defSel = document.getElementById('nf-cond-extra-definition-' + idx);
|
|
var rowSel = document.getElementById('nf-cond-extra-row-' + idx);
|
|
var fieldSel = document.getElementById('nf-cond-extra-field-' + idx);
|
|
if (!defSel || !rowSel || !fieldSel) return;
|
|
flowConditionExtraClauses[idx].definition_id = defSel.value;
|
|
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
|
|
rowSel.innerHTML = _flowRowOptionsHtml(info);
|
|
fieldSel.innerHTML = info.fields.length
|
|
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
|
|
: '<option value="">Aucun champ simple</option>';
|
|
flowConditionExtraClauses[idx].row_id = rowSel.value;
|
|
onFlowClauseFieldChange(idx);
|
|
}
|
|
|
|
function onFlowClauseFieldChange(idx) {
|
|
var fieldSel = document.getElementById('nf-cond-extra-field-' + idx);
|
|
if (!fieldSel) return;
|
|
var opt = fieldSel.options[fieldSel.selectedIndex];
|
|
flowConditionExtraClauses[idx].field = fieldSel.value;
|
|
flowConditionExtraClauses[idx].field_type = opt ? opt.dataset.type : '';
|
|
}
|
|
|
|
function toggleFlowActionFields() {
|
|
var typeSel = document.getElementById('nf-action-type');
|
|
var screenSel = document.getElementById('nf-action-target-screen');
|
|
var elementFields = document.getElementById('nf-action-element-fields');
|
|
var dataFields = document.getElementById('nf-action-data-fields');
|
|
var variableFields = document.getElementById('nf-action-variable-fields');
|
|
var waitFields = document.getElementById('nf-action-wait-fields');
|
|
var eventFields = document.getElementById('nf-action-event-fields');
|
|
var isOnglet = typeSel.value === 'activer_onglet';
|
|
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
|
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
|
dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
|
variableFields.style.display = typeSel.value === 'modifier_variable' ? 'block' : 'none';
|
|
waitFields.style.display = typeSel.value === 'attendre' ? 'block' : 'none';
|
|
if (eventFields) eventFields.style.display = typeSel.value === 'declencher_evenement' ? 'block' : 'none';
|
|
document.getElementById('nf-action-onglet-hint').style.display = isOnglet ? 'block' : 'none';
|
|
document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block';
|
|
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
|
|
if (typeSel.value === 'modifier_donnee') onFlowDefinitionChange('action');
|
|
updateDataValuePlaceholder('nf-action-data-operation', 'nf-action-data-value');
|
|
updateDataValuePlaceholder('nf-action-variable-operation', 'nf-action-variable-value');
|
|
}
|
|
|
|
// "alea" (Phase 2 — hasard) attend "min,max" dans data_value (voir
|
|
// screens/data_actions/compute_operation.py) plutôt qu'un seul nombre
|
|
// comme les autres opérations — indice visuel dans le champ plutôt que
|
|
// d'ajouter un champ dédié (aucun changement de schéma nécessaire).
|
|
function updateDataValuePlaceholder(operationSelectId, valueInputId) {
|
|
var opSel = document.getElementById(operationSelectId);
|
|
var valueInp = document.getElementById(valueInputId);
|
|
if (!opSel || !valueInp) return;
|
|
valueInp.placeholder = opSel.value === 'alea' ? 'min,max (ex. 1,6)' : 'Valeur / montant';
|
|
}
|
|
|
|
function toggleFlowElementPropertyValue() {
|
|
var propSel = document.getElementById('nf-action-element-property');
|
|
var visSel = document.getElementById('nf-action-element-value-visibility');
|
|
var surbrillanceSel = document.getElementById('nf-action-element-value-surbrillance');
|
|
var desactiveSel = document.getElementById('nf-action-element-value-desactive');
|
|
var colorInp = document.getElementById('nf-action-element-value-color');
|
|
var numInp = document.getElementById('nf-action-element-value-number');
|
|
var textInp = document.getElementById('nf-action-element-value-text');
|
|
var toggleRow = document.getElementById('nf-action-element-toggle-row');
|
|
[visSel, surbrillanceSel, desactiveSel, colorInp, numInp, textInp].forEach(function(el){ el.style.display = 'none'; });
|
|
if (propSel.value === 'visibilite') {
|
|
visSel.style.display = 'block';
|
|
toggleRow.style.display = 'none';
|
|
document.getElementById('nf-action-element-toggle').checked = false;
|
|
} else if (propSel.value === 'surbrillance') {
|
|
surbrillanceSel.style.display = 'block';
|
|
toggleRow.style.display = 'none';
|
|
document.getElementById('nf-action-element-toggle').checked = false;
|
|
} else if (propSel.value === 'desactive') {
|
|
desactiveSel.style.display = 'block';
|
|
toggleRow.style.display = 'none';
|
|
document.getElementById('nf-action-element-toggle').checked = false;
|
|
} else if (propSel.value === 'contenu') {
|
|
textInp.style.display = 'block';
|
|
toggleRow.style.display = 'none';
|
|
document.getElementById('nf-action-element-toggle').checked = false;
|
|
} else if (propSel.value === 'bg_color' || propSel.value === 'border_color') {
|
|
colorInp.style.display = 'block';
|
|
toggleRow.style.display = 'flex';
|
|
} else {
|
|
numInp.style.display = 'block';
|
|
toggleRow.style.display = 'flex';
|
|
}
|
|
toggleFlowElementToggleMode();
|
|
}
|
|
|
|
function toggleFlowElementToggleMode() {
|
|
var propSel = document.getElementById('nf-action-element-property');
|
|
var checked = document.getElementById('nf-action-element-toggle').checked;
|
|
var wrap = document.getElementById('nf-action-element-value2-wrap');
|
|
var colorInp2 = document.getElementById('nf-action-element-value2-color');
|
|
var numInp2 = document.getElementById('nf-action-element-value2-number');
|
|
var isColor = propSel.value === 'bg_color' || propSel.value === 'border_color';
|
|
wrap.style.display = checked ? 'block' : 'none';
|
|
colorInp2.style.display = (checked && isColor) ? 'block' : 'none';
|
|
numInp2.style.display = (checked && !isColor) ? 'block' : 'none';
|
|
}
|
|
|
|
function submitNodeForm() {
|
|
var payload = { node_type: currentNodeFormType, x: 20, y: 20 + currentBlockNodes().length * 10, block_id: CURRENT_BLOCK_ID };
|
|
|
|
if (currentNodeFormType === 'trigger') {
|
|
payload.trigger_event = document.getElementById('nf-trigger-event').value;
|
|
if (payload.trigger_event === 'evenement') {
|
|
payload.trigger_custom_event_id = parseInt(document.getElementById('nf-trigger-custom-event').value, 10);
|
|
} else if (payload.trigger_event !== 'affichage') {
|
|
payload.trigger_element_id = parseInt(document.getElementById('nf-trigger-element').value, 10);
|
|
}
|
|
} else if (currentNodeFormType === 'condition') {
|
|
var condSource = document.getElementById('nf-cond-source').value;
|
|
payload.cond_source = condSource;
|
|
payload.cond_operator = document.getElementById('nf-cond-operator').value;
|
|
payload.cond_value = document.getElementById('nf-cond-value').value;
|
|
if (condSource === 'variable') {
|
|
payload.cond_variable = document.getElementById('nf-cond-variable').value;
|
|
payload.cond_variable_chemin = document.getElementById('nf-cond-variable-chemin').value;
|
|
} else {
|
|
var fieldSel = document.getElementById('nf-cond-field');
|
|
var fieldOpt = fieldSel.options[fieldSel.selectedIndex];
|
|
payload.cond_definition_id = parseInt(document.getElementById('nf-cond-definition').value, 10);
|
|
payload.cond_row_id = parseInt(document.getElementById('nf-cond-row').value, 10) || null;
|
|
payload.cond_field = fieldSel.value;
|
|
payload.cond_field_type = fieldOpt ? fieldOpt.dataset.type : '';
|
|
}
|
|
if (flowConditionExtraClauses.length) {
|
|
// 2.4 — clauses combinées : la clause "historique" ci-dessus devient
|
|
// la première clause de la liste, suivie des clauses supplémentaires.
|
|
var mainClause = condSource === 'variable'
|
|
? { source: 'variable', variable: payload.cond_variable, variable_chemin: payload.cond_variable_chemin,
|
|
operator: payload.cond_operator, value: payload.cond_value }
|
|
: { source: 'objet', definition_id: payload.cond_definition_id, row_id: payload.cond_row_id,
|
|
field: payload.cond_field, field_type: payload.cond_field_type,
|
|
operator: payload.cond_operator, value: payload.cond_value };
|
|
var allClauses = [mainClause].concat(flowConditionExtraClauses.map(function(c) {
|
|
return c.source === 'variable'
|
|
? { source: 'variable', variable: c.variable, variable_chemin: c.variable_chemin, operator: c.operator, value: c.value }
|
|
: { source: 'objet', definition_id: parseInt(c.definition_id, 10), row_id: parseInt(c.row_id, 10) || null,
|
|
field: c.field, field_type: c.field_type, operator: c.operator, value: c.value };
|
|
}));
|
|
payload.cond_clauses = JSON.stringify(allClauses);
|
|
payload.cond_combinator = document.getElementById('nf-cond-combinator').value;
|
|
}
|
|
} else if (currentNodeFormType === 'action') {
|
|
var actionType = document.getElementById('nf-action-type').value;
|
|
payload.action_type = actionType;
|
|
if (actionType === 'aller_a' || actionType === 'ouvrir_ligne') {
|
|
payload.target_screen_id = parseInt(document.getElementById('nf-action-target-screen').value, 10);
|
|
} else if (actionType === 'activer_onglet') {
|
|
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
|
|
} else if (actionType === 'modifier_element') {
|
|
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
|
|
var prop = document.getElementById('nf-action-element-property').value;
|
|
payload.element_property = prop;
|
|
var isColorProp = prop === 'bg_color' || prop === 'border_color';
|
|
var toggleOn = prop !== 'visibilite' && prop !== 'surbrillance' && prop !== 'desactive' && document.getElementById('nf-action-element-toggle').checked;
|
|
if (prop === 'visibilite') {
|
|
payload.element_value = document.getElementById('nf-action-element-value-visibility').value;
|
|
} else if (prop === 'surbrillance') {
|
|
payload.element_value = document.getElementById('nf-action-element-value-surbrillance').value;
|
|
} else if (prop === 'desactive') {
|
|
payload.element_value = document.getElementById('nf-action-element-value-desactive').value;
|
|
} else if (prop === 'contenu') {
|
|
payload.element_value = document.getElementById('nf-action-element-value-text').value;
|
|
} else {
|
|
var v1 = isColorProp
|
|
? document.getElementById('nf-action-element-value-color').value
|
|
: document.getElementById('nf-action-element-value-number').value;
|
|
if (toggleOn) {
|
|
var v2 = isColorProp
|
|
? document.getElementById('nf-action-element-value2-color').value
|
|
: document.getElementById('nf-action-element-value2-number').value;
|
|
payload.element_value = v1 + '||' + v2;
|
|
} else {
|
|
payload.element_value = v1;
|
|
}
|
|
}
|
|
} else if (actionType === 'modifier_donnee') {
|
|
payload.target_definition_id = parseInt(document.getElementById('nf-action-data-definition').value, 10);
|
|
payload.target_row_id = parseInt(document.getElementById('nf-action-data-row').value, 10) || null;
|
|
payload.target_field = document.getElementById('nf-action-data-field').value;
|
|
payload.data_operation = document.getElementById('nf-action-data-operation').value;
|
|
payload.data_value = document.getElementById('nf-action-data-value').value;
|
|
} else if (actionType === 'modifier_variable') {
|
|
payload.target_variable = document.getElementById('nf-action-variable-name').value;
|
|
payload.data_operation = document.getElementById('nf-action-variable-operation').value;
|
|
payload.data_value = document.getElementById('nf-action-variable-value').value;
|
|
} else if (actionType === 'attendre') {
|
|
payload.data_value = document.getElementById('nf-action-wait-seconds').value;
|
|
} else if (actionType === 'declencher_evenement') {
|
|
// Notification pure : aucun élément/ligne à transmettre (voir
|
|
// screens/custom_events/) — c'est à l'écouteur de décider quoi
|
|
// faire, avec ses propres réglages habituels.
|
|
payload.target_custom_event_id = parseInt(document.getElementById('nf-action-custom-event').value, 10);
|
|
}
|
|
}
|
|
|
|
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/nodes/add'), {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
|
|
}).then(function(r){ return r.json(); }).then(function(node){
|
|
ALL_FLOW.nodes.push(node);
|
|
closeNodeForm();
|
|
renderFlow();
|
|
});
|
|
}
|
|
|