editGraphe/boiteDialogue.js

// FIXME il ne faudrait pas pouvoir échanger les rangs d’un branchement nn avec son propre ssn (supprimer les snn des rangs possibles)
import $ from 'jquery'

import { notify } from 'sesajs/error'

import { testIntervalleDecimaux } from 'sesajs/regexp'
import { addElement } from 'sesajs/dom'

import { hasProp, stringify } from 'sesajs/object'
import { j3pAddContent, j3pAddTxt, j3pElement, j3pSetProps, j3pShowError } from 'src/legacy/core/functions'

import { actualiseGestionnaireEvenements } from './scene'
import { clone, creebtnsradios, creediv, creeinput, creelisteDeroulante, creespan, findPosDiv } from './fonctionsUtiles'
import { actualiseRangsSurScene, ajouteNodeGraphe, connexionNodes, getConnexionDansGraphe, getJspConnexionFromBranchementDomId, getNodeDansGraphe, reindexeBranchementsNode } from './fonctionsNodes'
import { dispatch, getConfig, getJsPlumbInstance, getObjetGraphe, getRootElt } from './store'
import { delObjetGrapheBranchement, delObjetGrapheBranchementProp, setObjetGrapheBranchement, setObjetGrapheBranchementProp, setObjetGrapheParam } from './actions'
import getSectionParams from 'src/editGraphe/getSectionParams'
// import { addGlobalChecker, startDomDeletionTracking } from 'src/lib/utils/debug'

// pour le closeButton
import './boiteDialogue.scss'
import Modal from 'src/lib/entities/dom/Modal'

/**
 * Gère les boites de dialogue dans editgraphe
 * @module editGraphe/boiteDialogue
 */

/**
 * Appelé au clic sur le bouton radio PE / score
 * @param {boolean} isScore
 * @private
 */
function afficheScoreOuPe (isScore) {
  j3pElement('affichage_pe').style.display = isScore ? 'none' : 'block'
  j3pElement('affichage_score').style.display = isScore ? 'block' : 'none'
}

function supprimeBranchement (branchementDomId) {
  const objetGraphe = getObjetGraphe()
  const instanceJsPlumb = getJsPlumbInstance()

  // faut récupérer ça avant de le supprimer d’objetGraphe
  const branchementScene = getJspConnexionFromBranchementDomId(branchementDomId)
  // mais on le vire effectivement plus tard, pour le conserver si la suppression dans objetGraphe plante

  // supprime le branchement dans objetGraphe
  const branchement = getConnexionDansGraphe(branchementDomId)
  // S’il y a une ppte snn il faut aussi supprimer le branchement correspondant...
  if (hasProp(branchement.branchement, 'snn')) {
    for (const branchementIndex in objetGraphe.nodes[branchement.sourceNodeId].branchements) {
      if (!hasProp(objetGraphe.nodes[branchement.sourceNodeId].branchements[branchementIndex], 'nn')) { // c’est le branchement qui n’a pas de nn
        const branchementDomIdBis = objetGraphe.nodes[branchement.sourceNodeId].branchements[branchementIndex].branchementDomId
        const branchementSceneBis = getJspConnexionFromBranchementDomId(branchementDomIdBis)
        instanceJsPlumb.deleteConnection(branchementSceneBis)
        // delete objetGraphe.nodes[branchement.sourceNodeId].branchements[branchementIndex]
        dispatch(delObjetGrapheBranchement(branchement.sourceNodeId, branchementIndex))
      }
    }
  }
  dispatch(delObjetGrapheBranchement(branchement.sourceNodeId, branchement.branchementIndex))

  // ré-indexe les rangs des branchements du node duquel partait ce branchement supprimé
  reindexeBranchementsNode(branchement.sourceNodeId, false)

  // supprime le branchement de la scene
  instanceJsPlumb.deleteConnection(branchementScene)
  // ALEX : commenté car faisait buguer : au déplacement des nodes, on se retrouvait avec les flèches mal placées.
  // Les _jsPlumb_endpoint n’avais plus de parent (la scène normalement) d’où bug dans la fonction
  // adjustForParentOffsetAndScroll du fichier jsPlumb-1.4.0-RC1 $("._jsPlumb_endpoint").remove()

  // fermela boite de dialogue du branchement qu’on vient de supprimer
  $('#edgMasque, #modaleBranchement').remove()
} // supprimeBranchement

function afficheMaxParcours (isShown) {
  j3pElement('affichage_max_parcours').style.display = isShown ? 'block' : 'none'
}

/**
 * Affiche la boite modale
 * @param {object} options
 * @param {string} [options.id=edgModale]
 * @param {string} [options.contenu] Le contenu html à afficher dans la modale
 * @param {number} [options.width=200] Largeur
 * @param {number} [options.height=150] Hauteur
 * @param {boolean} [options.booltitre=false] Passer true pour ajouter un titre dans la modale
 * @param {boolean} [options.croix=true] Ajoute une croix pour fermer la modale
 * @param {string} [options.titre='il faudrait un titre'] Titre, devrait être fourni si booltitre, ignoré sinon
 * @param {boolean} [options.autoClose=true] Passer false pour que la modale ne se ferme pas quand on clique à l’extérieur
 * @return {HTMLDivElement} Le conteneur de la modale
 * @private
 */
function edgModale ({
  id = 'edgModale',
  contenu = '',
  width = 200,
  height = 150,
  booltitre = false,
  croix = true,
  titre = 'il faudrait un titre',
  autoClose = true
}) {
  const dropModale = () => $(`#${id}, #edgMasque`).remove()
  let boite = j3pElement(id, null)
  if (boite) {
    // on a oublié de virer la précédente avant, bizarre, on le fait ici
    console.warn(`Appel de edgModale pour l’id ${id} qui existait déjà`)
    dropModale()
  }
  const rootElt = getRootElt()
  if (!rootElt) throw new Error('Il manque l’élément html conteneur de l’application')
  const larg = $(window).width()
  const haut = $(window).height()
  const style = {
    top: `${(haut - height - 100) / 2}px`,
    left: `${(larg - width - 80) / 2}px`,
    minWidth: `${width}px`,
    minHeight: `${height}px`,
    display: 'none' // on la crée invisible et c’est le fadeIn plus bas qui l’affiche
  }
  boite = addElement(rootElt, 'div', { id, className: 'boiteDialogue', style })

  if (booltitre) {
    addElement(boite, 'div', { id: `${id}titre`, className: 'titre', content: titre })
  }
  // on met toujours le div ${id}contenu, même vide
  addElement(boite, 'div', { id: `${id}contenu`, className: 'contenu' }).innerHTML = contenu
  if (croix) {
    const croixElt = addElement(boite, 'div', { className: 'croix' })
    croixElt.addEventListener('click', dropModale, false)
  }

  const masqueElt = addElement(rootElt, 'div', { id: 'edgMasque', style: { display: 'none' } })
  const $masque = $(masqueElt)
  $masque.css({ filter: 'alpha(opacity=50)' }).fadeIn(500)
  // on autorise uniquement à fermer la boite modale par clic sur le masque lorsqu’on n’est pas en paramétrage de noeud
  if (autoClose) $masque.click(dropModale)
  $(boite).fadeIn()

  return boite
}

export default edgModale

/**
 * Affiche un message d’erreur
 * @param {string} errorMsg Le message d’erreur à afficher
 * @param {object} options Les attributs à passer à edgModale (titre et contenu seront imposés)
 */
export function j3pModaleError (errorMsg = 'Une erreur est survenue', options) {
  if (!options) options = {}
  options.titre = 'Erreur'
  options.booltitre = true
  options.contenu = errorMsg
  edgModale(options)
}

/**
 * Ajoute #contenu_branchement pour un branchement snn non paramétrable
 * @private
 */
function afficheModaleSnnNonParametrable () {
  const modaleElt = edgModale({ width: '320', height: '250' })
  addElement(modaleElt, 'h3', { content: 'Configuration du branchement' }) // wrapper div #branchement_entete viré
  addElement(modaleElt, 'div', { id: 'contenu_branchement' }).innerHTML = 'Ce branchement n’est pas paramétrable : il est le résultat d’un maximum de passages atteint pour un nœud (dans un branchement qui boucle vers ce même nœud ou un nœud précédent du graphe).<br/>On peut modifier le message qui sera affiché à l’élève en paramétrant ce branchement parent.'
}

/**
 * Ajoute #contenu_branchement pour un branchement qui existe déjà
 * @private
 */
function suiteParametrageExistant () {
  const modaleElt = edgModale({ width: '320', height: '250', booltitre: false })
  addElement(modaleElt, 'h3', { content: 'Configuration du branchement' })
  addElement(modaleElt, 'div', { id: 'contenu_branchement' }).innerHTML = 'Ce branchement existe déjà !<br>Vous pouvez en modifier les paramètres en cliquant dessus.'
}

/**
 * Retourne true s’il y a des nodes non snn avant branchementIndex (non compris)
 * @param {Branchement[]} branchements liste de branchements partant du node concerné
 * @param {number} branchementIndex
 * @return {boolean}
 * @private
 */
const hasPrevious = (branchements, branchementIndex) => branchements.slice(0, branchementIndex).some(br => !br.isSnn)
/**
 * Retourne true s’il y a des nodes non snn après branchementIndex (non compris)
 * @param {Branchement[]} branchements liste de branchements partant du node concerné
 * @param {number} branchementIndex
 * @return {boolean}
 * @private
 */
const hasNext = (branchements, branchementIndex) => branchements.slice(branchementIndex + 1).some(br => !br.isSnn)

/**
 * Affiche la modale d’édtion du branchement
 * @param {string} branchementDomId
 */
export function dialogueBranchement (branchementDomId) {
  /**
   * Complète le formulaire d’édition du branchement
   * - si .pe contient plus d’un élément c une section quantitative (ou plutôt si contient pe_1)
   *   => on laisse la possibilité de jouer sur le score/pe sinon que sur le score
   * Une fois terminé, à la validation, on récupère les infos, on renseigne objetGraphe.nodes[branchement.sourceNodeId].branchements[branchement.branchementIndex]
   * On ne change que la conclusion et pe et/ou score (vérifier si existant déjà)
   * DONE : récupérer la pe et ou le score actuel, en cas de section quantitative, on renseigne le score uniquement (même si c pe inf 0)
   * DONE : si cas particulier du snn, cad si branchement.branchement a une ppte isSnn alors pas de configuration du noeud possible (msg pour renvoyer à l’autre branchement)
   * DONE : en cas de branchement d’un noeud sur lui même (sourceid=targetid par ex en ligne 26, à voir si pas plus simple de faire une recherche sur branchement.branchement.nn ?) alors interface spéciale ou l’on demande de compléter aussi les snn et node Fin supplémentaire à la validation
   * DONE : récupérer toutes les infos à la validation
   * DONE : gérer le sans condition
   * DONE : message undefined si nouvel auto branchement
   * TODO : vérifier que avoir nn de type string et snn de type int ne pose pas de pb à l’execution du graphe (en testant par ex ["4","parallelesremed1",[{"nn":"7","pe":">=0.8","conclusion":"Fin"},{"nn":"4","pe":"<=0.79","conclusion":"Réessayons l\exercice précédent","sconclusion":"Parcours terminé","snn":6,"max":3},{"inclinaison":"[15;40]","nbrepetitions":3}]];)
   * DONE : si branche deja existante, message pour pas possible ?
   * TODO : Bouton Annuler les modifications
   * @private
   * @param {Connexion} connexion
   * @param {string} sectionName
   * @param {boolean} autobranchement
   */
  function suiteParametrageBranchement (connexion, sectionName, autobranchement) {
    // fonction qui permet de constuire la liste déroulante pour les embranchements de noeuds possibles
    function construitListeSnn (container) {
      const select = addElement(container, 'select', { id: 'deroulante_snn' })
      addElement(select, 'option', { value: 'rien' })
      const noeudActuel = jspConnexion.sourceId.substr(4)
      for (const nodeId of Object.keys(objetGraphe.nodes)) {
        if (nodeId !== noeudActuel) {
          addElement(select, 'option', { content: nodeId, value: nodeId })
        }
      }
      addElement(select, 'option', { content: 'Ajouter un nœud fin', value: 'ajouter_fin' })
    }

    const objetGraphe = getObjetGraphe()
    const { sectionsParams } = getConfig()
    const sectionParams = sectionsParams[sectionName]
    const isSectionQuali = Array.isArray(sectionParams.pe) && sectionParams.pe.length
    const brIndex = Number(connexion.branchementIndex)
    const branchements = objetGraphe.nodes[connexion.sourceNodeId].branchements

    const modaleElt = edgModale({ id: 'modaleBranchement', width: '620', height: '450', croix: false })
    addElement(modaleElt, 'h3', { content: 'Configuration du branchement' })
    const contenu = addElement(modaleElt, 'div', { id: 'contenu_branchement', content: 'Rang de la condition : ' })
    const rang = brIndex + 1
    addElement(contenu, 'span', { id: 'dialogueBranchementRang', content: String(rang) })
    // console.debug('branchement', brIndex, 'parmi', branchements, 'hasNext ?', hasNext(branchements, brIndex), 'hasPrev ?', hasPrevious(branchements, brIndex))
    let style = hasPrevious(branchements, brIndex) ? {} : { visibility: 'hidden' }
    addElement(contenu, 'div', { className: 'flecheBas', title: 'Descendre', style })
      .addEventListener('click', modifierRangCondition.bind(null, branchementDomId, false))
    style = hasNext(branchements, brIndex) ? {} : { visibility: 'hidden' }
    addElement(contenu, 'div', { className: 'flecheHaut', title: 'Monter', style })
      .addEventListener('click', modifierRangCondition.bind(null, branchementDomId, true))
    // console.debug('connexion', connexion, 'avec graphe', objetGraphe)
    j3pAddTxt(contenu, '(utiliser les flèches pour modifier le rang de la condition)')
    addElement(contenu, 'br')
    addElement(contenu, 'br')

    addElement(modaleElt, 'div', { id: 'contenu_score_ou_pe' })
    const scorePeElt = addElement(modaleElt, 'div')
    const affScoreElt = addElement(scorePeElt, 'div', { id: 'affichage_score' })
    const affPeElt = addElement(scorePeElt, 'div', { id: 'affichage_pe' })

    if (isSectionQuali) {
      // on ajoute les boutons radio pour choisir la phrase d’état (pe_1, pe_2…)
      const container = j3pElement('contenu_score_ou_pe')
      container.style.textAlign = 'center'
      let form = addElement(container, 'form')
      let label = addElement(form, 'label', { content: 'Phrase d’état' })
      let input = addElement(label, 'input', { type: 'radio', name: 'choix_score_pe', value: 'PE', id: 'radio_pe' })
      input.addEventListener('click', () => afficheScoreOuPe(false))
      label = addElement(form, 'label', { content: 'Score' })
      input = addElement(label, 'input', { type: 'radio', name: 'choix_score_pe', value: 'score', id: 'radio_score' })
      input.addEventListener('click', () => afficheScoreOuPe(true))
      j3pAddTxt(contenu, 'Pour ce nœud, on peut choisir d’orienter suivant la Phrase d’État ou le score (réel compris entre 0 et 1) :')
      addElement(contenu, 'br')
      // on passe à la gestion de la pe
      affPeElt.style.display = 'none'
      j3pAddTxt(affPeElt, 'Choix de la Phrase d’État : (on peut en choisir plusieurs pour un même embranchement)')
      addElement(affPeElt, 'br')

      // form des pe + sans condition
      form = addElement(affPeElt, 'form', { id: 'deroulante_pe' })
      for (const peObj of sectionParams.pe) {
        // à priori y’a qu’une seule clé dans chaque objet élément de params.pe
        for (const [key, value] of Object.entries(peObj)) {
          const label = addElement(form, 'label', { content: value })
          addElement(label, 'input', { type: 'checkbox', id: `checkbox_${key}`, value: key })
        }
      }

      label = addElement(form, 'label', { content: 'Sans condition' })
      input = addElement(label, 'input', {
        type: 'checkbox',
        name: 'choix_sans_condition',
        id: 'radio_sans_condition_pe'
      })
      input.addEventListener('click', () => sansCondition(sectionName))
      // et on masque ça
      affScoreElt.style.display = 'none'
    } // isSectionQuali

    // dans tous les cas on a l’affichage du score (masqué si besoin)
    j3pAddTxt(affScoreElt, 'Score de l’élève (réel compris entre 0 et 1) : ')
    const selectScoreElt = addElement(affScoreElt, 'select', { id: 'deroulante_score' })
    addElement(selectScoreElt, 'option', { value: 'rien' })
    addElement(selectScoreElt, 'option', { content: '≤', value: 'inf' })
    addElement(selectScoreElt, 'option', { content: '≥', value: 'sup' })
    const scoreInput = creeinput({ id: 'input_score', papa: affScoreElt, value: '' })
    scoreInput.addEventListener('blur', () => {
      if (scoreInput.value.includes(',')) scoreInput.value = scoreInput.value.replace(/,/, '.')
      const score = Number(scoreInput.value)
      if (score >= 0 && score <= 1) return
      j3pShowError(`Score invalide (doit être compris entre 0 et 1) : ${scoreInput.value}`)
    })
    scoreInput.style.width = '25px'
    const label = addElement(affScoreElt, 'label', { content: 'Sans condition', style: { marginLeft: '1em' } })
    const cbSsCond = addElement(label, 'input', {
      type: 'checkbox',
      name: 'choix_sans_condition',
      id: 'sans_condition_score'
    })
    cbSsCond.addEventListener('click', () => sansCondition())
    // startDomDeletionTracking(affScoreElt.parentNode, '#sans_condition_score')
    // avec la ligne suivante on a une fct globale c que l’on peu appeler depuis la console du navigateur ou dans notre code
    // avec par ex c('avant truc')
    // addGlobalChecker(cbSsCond, 'sans_condition_score', 'c', { withLog: true })
    const conclusionDialogue = connexion.branchement.conclusion || ''
    addElement(scorePeElt, 'br')
    const labelMsg = addElement(scorePeElt, 'label', { content: 'Message affiché en fin de nœud (laisser vide pour passer directement au nœud suivant) : ' })
    addElement(labelMsg, 'input', { type: 'text', size: '70', id: 'input_conclusion', value: conclusionDialogue })
    addElement(scorePeElt, 'br')
    addElement(scorePeElt, 'br')

    // cas où les noeuds de départ et d’arrivée sont identiques, il faut les champs supplémentaires
    if (autobranchement) {
      j3pAddTxt(scorePeElt, 'A l’issu de ce branchement, l’élève sera orienté sur le même nœud, avec comme paramètres complémentaires :')
      // une liste pour ces champs
      const ul = addElement(scorePeElt, 'ul')
      let li = addElement(ul, 'li')
      let label = addElement(li, 'label', { content: 'Un nombre maximal de passages dans ce nœud :' })
      // #input max
      addElement(label, 'input', { id: 'input_max', type: 'number', min: 2, step: 1, style: { width: '35px' } })
      // #input_sconclusion
      li = addElement(ul, 'li')
      label = addElement(li, 'label', { content: 'Un message en cas d’echec après toutes les tentatives :' })
      addElement(label, 'input', { type: 'text', id: 'input_sconclusion', size: 65 })
      // target
      li = addElement(ul, 'li')
      label = addElement(li, 'label', { content: 'Le nœud vers lequel l’élève sera alors orienté :' })
      construitListeSnn(label)
    } else {
      // Nouveauté J3P Novembre 2016 : possibilité de boucler sur une partie du graphe, et non sur un noeud lui même
      j3pAddTxt(scorePeElt, 'Ce branchement renvoie-t-il vers un nœud précédent du graphe ?')
      const form = addElement(scorePeElt, 'form')
      let label = addElement(form, 'label', { content: 'Oui' })
      addElement(label, 'input', { type: 'radio', name: 'choix_max_parcours', value: 'max_parcours_oui', id: 'radio_max_parcours_oui' })
      label.addEventListener('click', afficheMaxParcours.bind(null, true))
      label = addElement(form, 'label', { content: 'Non' })
      addElement(label, 'input', { type: 'radio', name: 'choix_max_parcours', value: 'max_parcours_non', id: 'radio_max_parcours_non', checked: true })
      label.addEventListener('click', afficheMaxParcours.bind(null, false))

      const divMaxParcours = addElement(scorePeElt, 'div', { id: 'affichage_max_parcours' })
      // console.debug('#affichage_max_parcours créé')
      j3pAddTxt(divMaxParcours, 'Cette portion du graphe est une boucle, il faut donc préciser :')
      const ul = addElement(divMaxParcours, 'ul')
      let li = addElement(ul, 'li')
      label = addElement(li, 'label', { content: 'Un nombre maximal de passages dans ce nœud' })
      addElement(label, 'input', { type: 'number', id: 'input_max', min: 2, step: 1, style: { width: '35px' } })
      li = addElement(ul, 'li')
      label = addElement(li, 'label', { content: 'Un message à l’issue de tous les passages dans ce nœud' })
      addElement(label, 'input', { type: 'text', id: 'input_sconclusion', size: 65 })
      li = addElement(ul, 'li')
      label = addElement(li, 'label', { content: 'Le nœud vers lequel l’élève sera alors orienté :' })
      construitListeSnn(label)
      // et on cache par défaut
      divMaxParcours.style.display = 'none'
    }

    // on a soit autobranchement soit un #affichage_max_parcours dans le dom

    /* avec ce code
    html = '<input type="button" onclick="supprimeBranchement(\'' + branchementDomId + '\',\'' + true + '\');" value="Supprimer ce branchement">'
    scorePeElt.innerHTML += html
    le listener sur la checkbox "sans condition" se faisait dégager (les nodes étaient retirés / ajoutés du DOM
    mais perdaient leur listener au passage, pb tracé avec MutationObserver)
     */
    addElement(scorePeElt, 'input', { type: 'button', value: 'Supprimer ce branchement' })
      .addEventListener('click', supprimeBranchement.bind(null, branchementDomId))

    const divValider = addElement(modaleElt, 'div', { id: 'btn_valider' })
    const btnValider = addElement(divValider, 'input', { type: 'button', value: 'Valider ' })
    btnValider.addEventListener('click', valideBranchement.bind(null, branchementDomId, sectionName, autobranchement))
    $('#contenu_branchement').focus()
    // si c’est un nouveau branchement, il n’a pas encore de ppte pe ou score, sinon on la récupère pour la renseigner
    if (hasProp(connexion.branchement, 'pe') || hasProp(connexion.branchement, 'score')) {
      if (isSectionQuali && hasProp(connexion.branchement, 'pe')) {
        j3pElement('radio_pe').checked = true
        affPeElt.style.display = 'block'
        const tab = connexion.branchement.pe.split(',')
        if (tab[0].indexOf('condition') !== -1) {
          j3pElement('radio_sans_condition_pe').checked = true
          sansCondition(sectionName)
        } else {
          for (let k = 0; k < tab.length; k++) {
            const numeroPe = tab[k].split('pe_')[1] // PB : il peut y avoir "pe":"pe_1,pe_2" d’où le split
            j3pElement('checkbox_pe_' + numeroPe).checked = true
          }
        }
      } else {
        // une section qualitative où on a orienté via le score ou une section quantitative (utilisant pe ou score)
        let score, inegalite
        if (isSectionQuali) {
          j3pElement('radio_score').checked = true
          affScoreElt.style.display = 'block'
        }
        const chaineScore = (hasProp(connexion.branchement, 'score')) ? connexion.branchement.score : connexion.branchement.pe
        if (chaineScore.indexOf('<') !== -1) {
          score = chaineScore.split('<')[1]
          inegalite = 1
        }
        if (chaineScore.indexOf('>') !== -1) {
          score = chaineScore.split('>')[1]
          inegalite = 2
        }
        if (chaineScore.indexOf('<=') !== -1) {
          score = chaineScore.split('<=')[1]
          inegalite = 1
        }
        if (chaineScore.indexOf('>=') !== -1) {
          score = chaineScore.split('>=')[1]
          inegalite = 2
        }
        if (chaineScore.indexOf('condition') !== -1) {
          j3pElement('sans_condition_score').checked = true
          sansCondition() // inutile avec un listener change, mais important si listener click
        } else {
          selectScoreElt.selectedIndex = inegalite
          const inputScore = j3pElement('input_score')
          inputScore.value = score
        }
      }
    }
    if (autobranchement) {
      // dans le cas d’un auto-branchement (node depart=node arrivee)
      // il faut aussi renseigner le max/snn/sconclusion (sauf si c’est un nouveau branchement)
      j3pElement('deroulante_snn').value = connexion.branchement.snn ?? ''
      j3pElement('input_max').value = connexion.branchement.max ?? 2
      if (connexion.branchement.sconclusion) {
        j3pElement('input_sconclusion').value = connexion.branchement.sconclusion
      }
    }
    // et si maxParcours on répercute aussi les infos
    if (connexion.branchement.maxParcours) {
      j3pElement('affichage_max_parcours').style.display = 'block'
      j3pElement('radio_max_parcours_oui').checked = true
      j3pElement('deroulante_snn').value = connexion.branchement.snn
      j3pElement('input_max').value = connexion.branchement.maxParcours
      if (connexion.branchement.sconclusion) {
        j3pElement('input_sconclusion').value = connexion.branchement.sconclusion
      }
    }
  } // suiteParametrageBranchement

  const objetGraphe = getObjetGraphe()
  let isDepartArriveeIdentique = false
  const connexion = getConnexionDansGraphe(branchementDomId)
  if (connexion === false) {
    return console.error(Error('dialogueBranchement sans connexion correspondante dans le graphe'))
  }

  const jspConnexion = getJspConnexionFromBranchementDomId(branchementDomId)
  if (jspConnexion.sourceId === jspConnexion.targetId) {
    isDepartArriveeIdentique = true
  }
  // on détecte ici si le branchement est déjà existant (pour savoir s’il faut le créer)
  let nbBranchement = 0
  objetGraphe.nodes[connexion.sourceNodeId].branchements.forEach(branchement => {
    if (
      (
        branchement &&
        hasProp(branchement, 'nn') &&
        branchement.nn === connexion.branchement.nn
      ) || (
        hasProp(branchement, 'snn') &&
        branchement.snn === connexion.branchement.nn
      )
    ) {
      nbBranchement++
    }
  })
  if (nbBranchement === 2) {
    supprimeBranchement(jspConnexion.id)
    suiteParametrageExistant()
    return
  }

  const sectionName = objetGraphe.nodes[connexion.sourceNodeId].section

  // Cas particulier d’un branchement non paramétrable : le snn
  if (connexion.branchementisSnn) {
    afficheModaleSnnNonParametrable()
    return
  }
  getSectionParams(sectionName).then(() => {
    suiteParametrageBranchement(connexion, sectionName, isDepartArriveeIdentique)
  }).catch(j3pShowError)
}

/**
 * Appelée au clic sur le btn sans condition qui gèle donc le choix de la pe ou du score
 * @private
 * @param {string} [sectionName] si fourni on agit sur la pe et sinon sur le score
 */
function sansCondition (sectionName) {
  if (sectionName) {
    // on gèle/degele les cases à cocher des pe
    const config = getConfig()
    const section = config.sectionsParams[sectionName]
    if (j3pElement('radio_sans_condition_pe').checked) {
      for (let i = 0; i < section.pe.length; i++) {
        j3pElement('checkbox_pe_' + (i + 1)).disabled = true
      }
    } else {
      for (let i = 0; i < section.pe.length; i++) {
        j3pElement('checkbox_pe_' + (i + 1)).disabled = false
      }
    }
  } else {
    // on gèle/degele la liste_deroulante et l’input du score
    const disabled = j3pElement('sans_condition_score').checked
    j3pElement('deroulante_score').disabled = disabled
    j3pElement('input_score').disabled = disabled
  }
}

/**
 * Change le rang d’un branchement
 * @param {string} branchementDomId
 * @param {boolean} isUp
 * @private
 */
function modifierRangCondition (branchementDomId, isUp) {
  const objetGraphe = getObjetGraphe()
  let echangeEffectif = false
  const branchementEdite = getConnexionDansGraphe(branchementDomId)
  const nodeId = branchementEdite.sourceNodeId
  const branchementsNode = objetGraphe.nodes[nodeId].branchements
  // console.debug('modifierRangCondition voit les branchements', branchementsNode)
  let currentIndex = branchementEdite.branchementIndex
  if (typeof currentIndex === 'string') {
    console.warn(`branchementIndex string (${currentIndex})`, branchementEdite)
    currentIndex = parseInt(currentIndex)
  }
  if (!Number.isFinite(currentIndex) || currentIndex < 0) throw Error(`index de branchement invalide (${branchementEdite.branchementIndex}) pour le nœud ${nodeId}`)
  // si modification possible
  if (!isUp && hasPrevious(branchementsNode, currentIndex)) {
    // y’a un précédent non snn (normalement on devrait jamais avoir de snn en 0, mais plus simple de garder ce test générique sur le slice)
    const previousIndex = currentIndex - 1
    let previous = clone(branchementsNode[previousIndex])
    // modification dans objetGraphe
    const current = clone(branchementsNode[currentIndex])
    dispatch(setObjetGrapheBranchement(nodeId, previousIndex, current))
    dispatch(setObjetGrapheBranchement(nodeId, currentIndex, previous))
    currentIndex--
    while (previous.isSnn) {
      // faut descendre encore d’un cran
      previous = clone(branchementsNode[currentIndex - 1])
      dispatch(setObjetGrapheBranchement(nodeId, currentIndex, previous))
      dispatch(setObjetGrapheBranchement(nodeId, currentIndex - 1, current))
      currentIndex--
    }
    echangeEffectif = true
  } else if (isUp && hasNext(branchementsNode, currentIndex)) {
    // le slice nous donne les nœud suivants, on en veut au moins un sans isSnn pour monter d’un cran
    // modification dans objetGraphe
    let next = clone(branchementsNode[currentIndex + 1])
    const current = clone(branchementsNode[currentIndex])
    dispatch(setObjetGrapheBranchement(nodeId, currentIndex + 1, current))
    dispatch(setObjetGrapheBranchement(nodeId, currentIndex, next))
    currentIndex++
    while (next.isSnn) {
      // faut monter d’un cran supplémentaire
      next = clone(branchementsNode[currentIndex + 1])
      dispatch(setObjetGrapheBranchement(nodeId, currentIndex, next))
      dispatch(setObjetGrapheBranchement(nodeId, currentIndex + 1, current))
      currentIndex++
    }
    echangeEffectif = true
  }
  if (echangeEffectif) {
    // modification dans la boîte de dialogue
    j3pAddContent('dialogueBranchementRang', currentIndex + 1, { replace: true })
    // refresh des flèches
    try {
      // on passe par visibility et pas display car on veut garder la place occupée à l’écran (et la css fixe du inline-block mais pourrait changer d’avis)
      document.querySelector('#contenu_branchement .flecheBas').style.visibility = hasPrevious(branchementsNode, currentIndex) ? 'visible' : 'hidden'
      document.querySelector('#contenu_branchement .flecheHaut').style.visibility = hasNext(branchementsNode, currentIndex) ? 'visible' : 'hidden'
    } catch (error) {
      console.error(error)
    }
    // modification sur la scene
    actualiseRangsSurScene(nodeId)
  }
}

function estNoeudFin (nn, objetGraphe) {
  return hasProp(objetGraphe, 'nodes') &&
    objetGraphe.nodes[nn] &&
    hasProp(objetGraphe.nodes[nn], 'section') &&
    objetGraphe.nodes[nn].section.toLowerCase() === 'fin'
}

/**
 * @param {string} branchementDomId
 * @param {string} sectionName
 * @param {boolean} autobranchement
 */
function valideBranchement (branchementDomId, sectionName, autobranchement) {
  let objetGraphe = getObjetGraphe()
  const config = getConfig()
  const section = config.sectionsParams[sectionName]
  let pe, testPeAFaire, testScoreAFaire, nodeId, posDiv, posX, posY, labelConnection
  testPeAFaire = false
  testScoreAFaire = false
  labelConnection = ''
  const noeudTemp = {}
  const connexion = getConnexionDansGraphe(branchementDomId)
  const branche = objetGraphe.nodes[connexion.sourceNodeId].branchements[connexion.branchementIndex]
  if (j3pElement('affichage_score').style.display === 'none') {
    testScoreAFaire = true
    // on travaille avec LA PE
    if (j3pElement('radio_sans_condition_pe').checked) {
      noeudTemp.pe = 'sans+condition'
      labelConnection = 'Pas de condition'
    } else {
      // on récupère la liste des PE
      pe = ''
      for (let i = 0; i < section.pe.length; i++) {
        if (j3pElement('checkbox_pe_' + (i + 1)).checked) {
          pe += 'pe_' + (i + 1) + ','
          // TODO : pour le label, récupérer section.pe[i] pour le mettre dans une ppte de objetGraphe (ou tout simplement dans labelConnection ci dessous)
          // dans definit_label, il faudra aussi récupérer les pe de config.sectionsParams pour afficher la bonne PE et non son nom
        }
      }
      if (pe === '') {
        return j3pShowError('Il faut choisir au moins une phrase d’état !')
      }
      pe = pe.substring(0, pe.length - 1)
      noeudTemp.pe = pe
      labelConnection = 'Si Phrase d`état=' + pe
    }
  } else {
    // ici c’est le score
    // cas particulier : si le noeud avait auparavant une ppte pe qui travaillait en fait sur le score (section quantitative uniquement donc) on a travaillé sur le score donc il faut maintenant dégager cette ancienne ppte pe
    testPeAFaire = true
    if (j3pElement('sans_condition_score').checked) {
      noeudTemp.score = 'sans+condition'
      labelConnection = 'Pas de condition'
    } else {
      // on reconstruit le score
      const value = Number(j3pElement('input_score').value.replace(/,/, '.'))
      if (isNaN(value) || Number(value) < 0 || Number(value) > 1) {
        return j3pShowError('Le score doit être un nombre compris entre 0 et 1.')
      }
      switch (j3pElement('deroulante_score').selectedIndex) {
        case 0:
          return j3pShowError('Il faut choisir le symbole inférieur ou supérieur dans la liste déroulante !')

        case 1:
          noeudTemp.score = '<=' + j3pElement('input_score').value
          labelConnection = 'Si Score' + noeudTemp.score
          break

        default :
          noeudTemp.score = '>=' + j3pElement('input_score').value
          labelConnection = 'Si Score' + noeudTemp.score
          break
      }
    }
  }
  noeudTemp.conclusion = j3pElement('input_conclusion').value.replace(/'/g, '’')
  const divMaxParcours = j3pElement('affichage_max_parcours', null)
  if (!autobranchement && !divMaxParcours) {
    notify(Error('Dans valideBranchement on a ni autobranchement ni #affichage_max_parcours, c’est pas normal'))
  }
  if (autobranchement || (divMaxParcours && divMaxParcours.style.display === 'block')) {
    // recup de input_max, input_sconclusion, deroulante_snn. Différents cas, si nouveau branchement (cad quand ce branchement n’a pas encore de ppte snn) : créer un branchement supplémentaire (voir completeBranchement), si branchement existant il faut regarder si le snn a été modifié (si oui il faut supprimer l’ancien branchement et en refaire un...)
    if (j3pElement('input_max').value === '') {
      return j3pShowError('Il faut préciser le nombre de retour maximal dans ce nœud (pour éviter de le faire recommencer indéfiniment)')
    }
    const snnValue = j3pElement('deroulante_snn')?.value ?? ''
    if (snnValue === 'rien' || snnValue === '') {
      return j3pShowError('Il faut choisir vers quel nœud l`élève sera orienté en cas d`échec.')
    }
    // si on est arrivé là c’est qu’il n’y a pas de msg d’erreur
    noeudTemp.sconclusion = j3pElement('input_sconclusion').value
    if (autobranchement) {
      noeudTemp.max = j3pElement('input_max').value
    } else {
      noeudTemp.maxParcours = j3pElement('input_max').value
    }
    nodeId = connexion.sourceNodeId
    actualiseGestionnaireEvenements(false) // ceci, grace au false, permet d’éviter de reafficher la boite de dialogue de branchement a la connection de deux nodes en dessous :
    if (hasProp(connexion.branchement, 'snn') && connexion.branchement.snn !== snnValue) {
      // on commence par supprimer le branchement maintenant inutile de objetGraphe (car va contenir le mauvais id de branchementDomId),
      // on supprime le branchement de la scène
      // on récupère le bon branchementDomId
      for (let i = 0; i < objetGraphe.nodes[nodeId].branchements.length; i++) {
        if (hasProp(objetGraphe.nodes[nodeId].branchements[i], 'isSnn')) {
          supprimeBranchement(objetGraphe.nodes[nodeId].branchements[i].branchementDomId)
        }
      }
    }
    if (!hasProp(connexion.branchement, 'snn') || (hasProp(connexion.branchement, 'snn') && connexion.branchement.snn !== snnValue)) {
      // c’est donc un nouvel autobranchement, ou alors on a modifié un existant (on a déjà dégagé les anciens branchements du coup) faut créer un lien sur la scène et un autre branchement dans objetGraphe
      if (snnValue === 'ajouter_fin') {
        // vers un nouveau noeud qu’il faut ajouter sur ma scène
        posDiv = findPosDiv(j3pElement('node' + nodeId))
        posX = posDiv.x + 30
        posY = posDiv.y + 30
        const node = ['1', 'fin', []]
        ajouteNodeGraphe(node, posX, posY, 'fin')
        objetGraphe = getObjetGraphe()// obligé de le getter à nouveau car on a changé le maxNumeroNode...
        connexionNodes('node' + nodeId, 'node' + objetGraphe.maxNumeroNode)
        noeudTemp.snn = objetGraphe.maxNumeroNode
        objetGraphe = getObjetGraphe()// obligé de le getter à nouveau après le connexionNodes sinon l’objetgraphe qui suit est pas bon...
        // il reste à corriger le dernier branchement du graphe comme dans completeBranchement
        for (let i = 0; i < objetGraphe.nodes[nodeId].branchements.length; i++) {
          if (hasProp(objetGraphe.nodes[nodeId].branchements[i], 'nn') && Number(objetGraphe.nodes[nodeId].branchements[i].nn) === Number(objetGraphe.maxNumeroNode)) {
            // objetGraphe.nodes[nodeId].branchements[i].isSnn = true
            const labelSnn = noeudTemp.max ? 'Si echec aux ' + noeudTemp.max + ' tentatives ' : 'Après ' + noeudTemp.maxParcours + ' passage(s) dans la boucle '
            dispatch(setObjetGrapheBranchementProp(nodeId, i, 'isSnn', true))
            dispatch(setObjetGrapheBranchementProp(nodeId, i, 'label', labelSnn))
            dispatch(delObjetGrapheBranchementProp(nodeId, i, 'nn'))
            // delete objetGraphe.nodes[nodeId].branchements[i].nn
          }
        }
      } else {
        connexionNodes('node' + nodeId, 'node' + snnValue)
        noeudTemp.snn = Number(snnValue) // je préfère convertir en Number, j’ai eu des surprises avec des string...
        objetGraphe = getObjetGraphe()// obligé de le getter à nouveau après le connexionNodes sinon l’objetgraphe qui suit est pas bon...
        // il reste à corriger le dernier branchement du graphe comme dans completeBranchement
        // un pb s’il existait déjà un autre branchement vers ce noeud (même depart et même fin), il faut pas le modifier donc on teste l’absence de ppté conclusion
        for (let i = 0; i < objetGraphe.nodes[nodeId].branchements.length; i++) {
          if (hasProp(objetGraphe.nodes[nodeId].branchements[i], 'nn') && Number(objetGraphe.nodes[nodeId].branchements[i].nn) === Number(snnValue) && !hasProp(objetGraphe.nodes[nodeId].branchements[i], 'conclusion')) {
            const labelSnn = noeudTemp.max ? 'Si echec aux ' + noeudTemp.max + ' tentatives ' : 'Après ' + noeudTemp.maxParcours + ' passage(s) dans la boucle '
            dispatch(setObjetGrapheBranchementProp(nodeId, i, 'isSnn', true))
            dispatch(setObjetGrapheBranchementProp(nodeId, i, 'label', labelSnn))
            dispatch(delObjetGrapheBranchementProp(nodeId, i, 'nn'))
            // objetGraphe.nodes[nodeId].branchements[i].isSnn = true
            // delete objetGraphe.nodes[nodeId].branchements[i].nn
          }
        }
      }
    }
    actualiseGestionnaireEvenements()// a voir si utile
  }
  for (const prop in noeudTemp) {
    dispatch(setObjetGrapheBranchementProp(connexion.sourceNodeId, Number(connexion.branchementIndex), prop, noeudTemp[prop]))
  }
  // const branche = objetGraphe.nodes[connexion.sourceNodeId].branchements[connexion.branchementIndex]
  if (testPeAFaire && hasProp(branche, 'pe')) {
    dispatch(delObjetGrapheBranchementProp(connexion.sourceNodeId, Number(connexion.branchementIndex), 'pe'))
    // delete noeud['pe']
  }
  if (testScoreAFaire && hasProp(branche, 'score')) {
    dispatch(delObjetGrapheBranchementProp(connexion.sourceNodeId, Number(connexion.branchementIndex), 'score'))
  }
  dispatch(setObjetGrapheBranchementProp(connexion.sourceNodeId, Number(connexion.branchementIndex), 'label', labelConnection))
  // fermer la fenetre
  $('#edgMasque, #modaleBranchement').remove()
}

/**
 * Valide un node
 * Appelée par suite_parametrage et valideParametresGraphe
 * @param numeroNode
 * @param {string} sectionName
 * @param {string} [prefixe='']
 * @private
 */
function valideParametres (numeroNode, sectionName, prefixe = '') {
  let valeurParam
  // changer le css du node (changeCouleurNode et transmettre nodeDomId) et récupérer les params de la boite
  // Todo : vérification syntaxique...
  // console.clear()
  const config = getConfig()
  const { parametres } = config.sectionsParams[sectionName]
  // input_parNOMPARAM et renseigner objetGraphe.nodes[numeroNode].parametres
  for (const [name, defaultValue, type] of parametres) {
    if (!isHiddenParam(sectionName, name)) {
      if (type === 'boolean') {
        // on cherche quel bouton a été checké :
        if (j3pElement(prefixe + name + 'true')?.checked) {
          valeurParam = true
        } else if (j3pElement(prefixe + name + 'false')?.checked) {
          valeurParam = false
        } else {
          valeurParam = defaultValue
        }
      } else if (type === 'liste') {
        valeurParam = j3pElement(prefixe + 'select' + name)?.value ?? ''
      } else {
        // on récupère la value en string pour tous les autres
        const strValue = j3pElement(prefixe + 'input_par' + name)?.value
        if (['number', 'entier', 'reel'].includes(type)) {
          valeurParam = Number(strValue.replace(',', '.'))
          if (!Number.isFinite(valeurParam)) {
            j3pShowError(`Valeur incorrecte pour le paramètre ${name} (${strValue}) => ${defaultValue}`)
            valeurParam = defaultValue
          } else if (type === 'entier' && !Number.isInteger(valeurParam)) {
            j3pShowError(`Valeur incorrecte pour le paramètre ${name} (${strValue} n’est pas un entier) => ${defaultValue}`)
            valeurParam = defaultValue
          }
        } else if (type === 'array') {
          try {
            valeurParam = JSON.parse(strValue)
            if (!Array.isArray(valeurParam)) {
              j3pShowError(`paramètre ${name} invalide, pas un array `)
              valeurParam = []
            }
          } catch (error) {
            j3pShowError(error, { message: `Le paramètre ${name} contient du json invalide` })
            valeurParam = []
          }
        } else if (type === 'intervalle') {
          if (testIntervalleDecimaux.test(strValue)) {
            valeurParam = strValue
          } else {
            j3pShowError(`paramètre ${name} invalide, pas un intervalle => ${defaultValue}`)
            valeurParam = defaultValue
          }
        } else {
          valeurParam = strValue
        }
      }
    }
    try { // PB : ne respecte pas le typage (pour les tableau, voire pour les booléens ?)
      dispatch(setObjetGrapheParam(numeroNode, name, valeurParam))
    } catch (e) {
      console.error('pb pour renseigner le param dans objetGraphe et erreur=', e)
    }
  }
  if (j3pElement(prefixe + 'param_donnees_parcours3').style.display === 'block') {
    switch (j3pElement(prefixe + 'deroulante_choix_noeud').value) {
      case 'rien':
        return j3pShowError('Il faut choisir la référence du nœud dont on veut récupérer les paramètres !')

      case 'noeud_precedent':
        valeurParam = 'j3p.parcours.donnees'
        break

      default :
        valeurParam = 'j3p.parcours.donnees[' + j3pElement(prefixe + 'deroulante_choix_noeud').value + ']'
        break
    }
    try {
      const param = config.sectionsParams[sectionName].donnees_parcours_nom_variable
      dispatch(setObjetGrapheParam(numeroNode, param, valeurParam))
    } catch (error) {
      console.error(error)
    }
  }
  $('#node' + numeroNode).css('background-color', '#cccccc')
  if (prefixe === '') {
    // faut virer la modale
    $('#edgMasque, #edgModale').remove()
  }
}

// Fonctions utilisées dans dialogueNode
// fonction appelée lors du paramétrage des sections avec donnes_parcours
function afficheParamDP (bool, nomsection) {
  j3pElement('param_donnees_parcours3').style.display = bool ? 'block' : 'none'
  if (!bool) {
    // on remet aussi le param à vide car si on vient d’un oui on avait j3p.parcours.donnees,à supprimer donc
    const param = getConfig().sectionsParams[nomsection].donnees_parcours_nom_variable
    j3pElement('input_par' + param).value = ''
  }
}

// fonction qui permet de savoir si l’on a le droit de surcharger un param
// (utilise actuellement config.parametres_non_affiches renseigné dans reducerConfig)
function isHiddenParam (nomSection, param) {
  const { parametresMasques } = getConfig()
  if (!parametresMasques?.[nomSection]) return false
  return parametresMasques[nomSection].includes(param)
}

// vidange de la boîte de dialogue
// et dépilement des actions à réaliser
function initialiseDialogue () {
  // // dépile les actions à effectuer lorsque le contenu de la boîte de dialogue change
  // for (var cleAction in plusieursActions) {
  //   if (pileActions[cleAction].evenement === 'initialiseDialogue') { // à dépiler ici donc !
  //     // effectue l’action
  //     window[pileActions[cleAction].action](pileActions[cleAction].parametres)
  //     // depile
  //     delete pileActions[cleAction]
  //   }
  // }
  // // vide la boîte de dialogue
  $('#contenudivD').empty() // TODO : a checker
}

function isPremierNoeud (nodeDomId) {
  const nodeNumero = Number(nodeDomId.substr(4))
  // every renvoie true si la callback renvoie true pour tous les éléments
  // (et renvoie false dès que la callback passée en param renvoie un false, sans boucler plus loin)
  // return getObjetGraphe().nodes.every(function (node, index) { return (index < nodeNumero) })
  return Object.keys(getObjetGraphe().nodes).every(numero => Number(numero) >= nodeNumero)
  // @todo vérifier que ce serait pas mieux avec ça
  // return getObjetGraphe().nodes.every((node, index) => index < nodeNumero)
}

// fonction qui permet de constuire la liste déroulante pour les embranchements de noeuds possibles
function construitListeNoeudDP (container, nodeDomId) {
  const objetGraphe = getObjetGraphe()
  const select = addElement(container, 'select', { id: 'deroulante_choix_noeud' })
  addElement(select, 'option', { value: 'rien' })
  const noeudActuel = nodeDomId.substr(4)
  Object.keys(objetGraphe.nodes).forEach(nodeId => {
    if (nodeId !== noeudActuel) {
      addElement(select, 'option', { content: `Du nœud ${nodeId}`, value: nodeId })
    }
  })
  addElement(select, 'option', { content: 'Du nœud appelant', value: 'noeud_precedent' })
  addElement(container, 'br')
}

function estDansGraphe (param, node) {
  const obj = {}
  obj.bool = false
  obj.valeur = ''
  const o = node.nodeParametres || ''
  for (const prop in o.parametres) {
    if (prop === (param)) {
      obj.bool = true
      obj.valeur = o.parametres[prop]
    }
  }
  return obj
}

function suiteParametrage (sectionName, node, prefixe = '') {
  const nodeDomId = 'node' + node.nodeNumero
  const { sectionsParams } = getConfig()
  const section = sectionsParams[sectionName]
  const divparam2 = j3pElement(prefixe + 'divparam2')
  if (!divparam2) return j3pShowError(Error('Erreur à la construction de la page, impossible d’afficher les paramètres'))// bugsnag dit que ça arrive… (l’erreur est déjà signalée en console par j3pElement
  if (!section) return j3pShowError(Error(`Impossible de paramétrer la section ${sectionName} car elle n’a pas été correctement chargée`), getConfig())
  if (hasProp(section, 'donnees_parcours_nom_variable') && !isPremierNoeud(nodeDomId)) {
    let html = 'Ce nœud  est particulier, la section programmée est capable de récupérer des informations du nœud précédent'
    // param_donnees_parcours
    if (typeof section.donnees_parcours_description === 'string' && section.donnees_parcours_description) {
      html += ', plus précisément :<br>' + section.donnees_parcours_description + '<br>'
    } else {
      html += '.<br>'
    }
    html += 'Souhaitez-vous réutiliser le paramétrage d’un nœud précédent ? (auquel cas certains paramètres suivants ne seront pas pris en compte)'
    j3pElement(prefixe + 'param_donnees_parcours').innerHTML = html

    const dp2Elt = j3pElement(prefixe + 'param_donnees_parcours2')
    dp2Elt.style.textAlign = 'center'
    const form = addElement(dp2Elt, 'form')
    let label = addElement(form, 'label', { content: 'Oui' })
    addElement(label, 'input', { type: 'radio', name: 'choix_param_DP', value: 'OUI', id: `${prefixe}radio_dp_true` })
      .addEventListener('click', afficheParamDP.bind(null, true))
    label = addElement(form, 'label', { content: 'Non (le nœud générera des données en fonction des paramètres suivants)' })
    addElement(label, 'input', { type: 'radio', name: 'choix_param_DP', value: 'NON', id: `${prefixe}radio_dp_false` })
      .addEventListener('click', afficheParamDP.bind(null, false))

    const params3 = j3pElement(prefixe + 'param_donnees_parcours3')
    j3pAddTxt(params3, 'Il est possible de choisir de quel nœud on récupèrera les paramètres, le nœud appelant ou un autre :')
    construitListeNoeudDP(params3, nodeDomId)
    addElement(params3, 'br')
  }
  // un objet dont chaque propriété sera le getter de sa valeur
  const getters = {}
  // idem pour le setter
  const setters = {}
  // tous les parametres
  for (const [prop, defaultValue, type, description, option] of section.parametres) {
    if (isHiddenParam(sectionName, prop)) break // si on veut pas afficher le param, typiquement le fichier chargé d’une section outil
    const id = prefixe + 'input_par' + prop
    const div = creediv({ papa: divparam2 })
    div.style.marginTop = '10px'
    const span = creespan({
      id: prefixe + 'titre_param_' + prop,
      innerHTML: prop + ' : ',
      papa: div,
      fontSize: '16px',
      color: '#663500'
    })
    span.style.color = '#FFF'
    span.style.display = 'inline-block'
    span.style.width = '200px'
    span.style.textAlign = 'right'
    const { bool, valeur: valeurSurchargee } = estDansGraphe(prop, node)
    const valeur = bool
      ? valeurSurchargee
      : defaultValue // sinon on reprend le paramétrage de base
    let input
    if (typeof valeur === 'object') {
      input = creeinput({
        fontSize: '20px',
        id,
        papa: div,
        value: stringify(valeur)
      })
      // son getter
      getters[prop] = () => {
        try {
          return JSON.parse(input.value)
        } catch (error) {
          console.error(error)
          return valeur
        }
      }
      setters[prop] = (value) => { input.value = value }
    } else if (type === 'boolean') {
      // c’est pas un input mais un span
      input = creebtnsradios({
        fontSize: '16px',
        color: '#FFFFFF',
        id,
        papa: div,
        value: ['true', 'false'],
        id_radios_btn: [prefixe + prop + 'true', prefixe + prop + 'false'],
        check: String(valeur)
      })
      // aj du getter / setter
      getters[prop] = () => {
        const inputs = document.getElementsByName(id)
        for (const input of inputs) {
          if (input.checked) return input.value === 'true'
        }
        return valeur
      }
      setters[prop] = (value) => {
        const inputs = document.getElementsByName(id)
        if (typeof value !== 'boolean') {
          console.error(Error(`Valeur non booléenne (${typeof value}) sur un type boolean`))
          value = Boolean(value)
        }
        const strValue = value ? 'true' : 'false'
        for (const input of inputs) {
          if (input.value === strValue) {
            input.checked = true
            break
          }
        }
      }
    } else if (type === 'editor' || type === 'multiEditor') {
      input = creeinput({ fontSize: '16px', id, papa: div, value: valeur })
      getters[prop] = () => input.value
      setters[prop] = (value) => { input.value = value }
      if (typeof option === 'function') {
        const editorFct = option // pour rendre le code plus compréhensible
        input.hidden = true
        // les deux boutons, dans un span pour que ce soit plus joli (reste calé dans la colonne de droite)
        const spanBtns = addElement(div, 'span', { style: { display: 'inline-block' } })
        const btnRunEditor = addElement(spanBtns, 'button', { style: { margin: '0 0.2rem' }, content: 'Éditer' })
        const btnRawEditor = addElement(spanBtns, 'button', { content: 'Saisie brute' })
        btnRawEditor.addEventListener('click', () => {
          input.hidden = false
        })

        // on remet l’input après nos boutons
        div.appendChild(input)

        btnRunEditor.addEventListener('click', () => {
          // au cas où l’input était affiché on le masque
          input.hidden = true
          let initalValue = getters[prop]()
          if (type === 'multiEditor') {
            // il faut récupérer tous les paramètres, éventuellement surchargés, pour les passer à l’éditeur (il peut avoir besoin des valeurs des autres params que celui qu’on édite
            initalValue = {}
            for (const [prop, getter] of Object.entries(getters)) {
              initalValue[prop] = getter()
            }
          }
          // on lance l’éditeur dans une modale (s’il est dans une iframe il sera limité à cette iframe mais avec du fullscreen ça marche plus, les input blockly ou mtg sont plus atteignables, cf http://localhost:8081/testIframe.html?edit&graphe=[1,%22blokmtg01%22,[{pe:%22sans%20condition%22,nn:%22fin%22,conclusion:%22Fin%22}]])
          const title = `Édition du paramètre « ${prop} »`
          const editorModal = new Modal({ title, zIndex: 100, fullScreenIframe: true, withoutClose: true }) // faut passer au-dessus des 90 de #edgModale
          editorModal.show()
          // addElement(iframeDoc.body, 'div', { style: { position: 'absolute', width: '100%', height: '100%', top: 0, left: 0, backgroundColor: '#eee', zIndex: 100, overflow: 'auto' } })

          const divContent = editorModal.addElement('div')
          editorFct(divContent, initalValue).then(result => {
            // console.debug(`l’éditeur du paramètre ${prop} nous retourne`, result)
            if (result != null) {
              if (typeof result === 'string') {
                input.value = result
              } else if (typeof result === 'object') {
                if (type === 'editor') {
                  console.error(Error(`L’éditeur du paramètre ${prop} nous retourne un objet (il devrait nous retourner une string) => JSON.stringify`), result)
                  input.value = stringify(result)
                } else {
                  // faut mettre tout ça dans les différents inputs…
                  for (let [p, v] of Object.entries(result)) {
                    // console.debug('on a récupéré', v, 'pour le param', p)
                    if (typeof v === 'object') v = stringify(v)
                    if (setters[p]) setters[p](v) // màj de l’input (ou bouton radio ou select)
                    else console.error(Error(`Il n’y a pas de paramètre ${p}`))
                  }
                }
              } else {
                throw Error(`L’éditeur de ${prop} ne retourne pas les valeurs attendues`)
              }
            }
            // et on peut fermer notre modale d’édition de paramètre
            editorModal.destroy()
          }).catch(j3pShowError)
        })
      } else {
        console.error(Error(`La section déclare le paramètre ${prop} de type ${type} mais elle ne fourni pas la fonction en 5e élément du paramètre`), option)
      }
    } else if (type === 'liste') {
      // c’est le span qui va contenir le select
      input = creelisteDeroulante({
        fontSize: '16px',
        nom_param: prop,
        id,
        papa: div,
        selected: valeur,
        value: option,
        prefixe
      })
      getters[prop] = () => {
        const id = prefixe + 'select' + prop
        const select = j3pElement(id)
        if (!select) {
          console.error(Error(`aucun élément #${id}`))
          return valeur
        }
        return select.options[select.selectedIndex].value
      }
      setters[prop] = (value) => {
        const id = prefixe + 'select' + prop
        const select = j3pElement(id)
        if (!select) return console.error(Error(`aucun élément #${id}`))
        for (const option of select.options) {
          if (option.value === value) {
            option.selected = true
            return
          }
        }
      }
    } else if (['entier', 'number', 'reel'].includes(type)) {
      input = creeinput({ fontSize: '16px', id, papa: div, value: String(valeur), type: 'text' })
      getters[prop] = () => Number(input.value)
      setters[prop] = (value) => { input.value = String(value) }
    } else {
      // tout le reste, chaine, intervalle, array, tableau…
      input = creeinput({ fontSize: '16px', id, papa: div, value: valeur })
      getters[prop] = () => input.value
      setters[prop] = (value) => { input.value = value }
    }
    // on ajoute un peu de style sur les input texte
    if (['string', 'entier', 'number', 'reel', 'intervalle', 'array', 'editor', 'multiEditor'].includes(type)) {
      input.style.color = '#2D5A7D'
      input.style.width = '350px'
      input.style.textAlign = 'right'
      input.style.marginLeft = '10px'
      input.style.background = '#DDEEFB'
      input.style.borderStyle = 'inset'
      input.style.borderWidth = '2px'
    }
    // On affiche le commentaire du paramètre…
    const showHelp = () => { j3pElement(prefixe + 'divinfos2').innerHTML = description.replace(/\n/g, '<br>') }
    // …au clic sur le nom du paramètre :
    j3pElement(prefixe + 'titre_param_' + prop).addEventListener('click', showHelp)
    // …et sur l’input
    input.addEventListener('focus', showHelp)
    // et pour le select faut aussi l’ajouter
    if (type === 'liste') {
      input.querySelector('select').addEventListener('focus', showHelp)
    }

    // donnees_parcours_nom_variable est un paramètre d’une section qui lui permet de récupérer le paramètre d’une autre section du graphe
    // c’est utilisé par ex avec les probas ou les dérivées (chercher donnees_parcours_nom_variable dans les sections)
    if (hasProp(section, 'donnees_parcours_nom_variable') && !isPremierNoeud(nodeDomId) && section.donnees_parcours_nom_variable === prop) {
      const chunks = /j3p.parcours.donnees\[([^\]]+)]/.exec(valeur)
      let noeudInitial
      if (chunks) {
        noeudInitial = chunks[1]
      } else if (valeur.includes('j3p.parcours.donnees')) {
        noeudInitial = 'noeud_precedent'
      }
      if (noeudInitial) {
        // console.debug('On réutilise un noeud précis', noeudInitial)
        j3pElement(prefixe + 'radio_dp_true').checked = true
        j3pElement(prefixe + 'deroulante_choix_noeud').value = noeudInitial
        afficheParamDP('true')
      }
    }
  }
  if (prefixe === '') {
    const papa = creediv({ id: 'btn_valider', papa: j3pElement('edgModale') })
    const btnAnnuler = creeinput({ type: 'button', value: 'Annuler', papa })
    btnAnnuler.style.margin = '1rem 3rem'
    btnAnnuler.addEventListener('click', () => $('#edgModale, #edgMasque').remove())
    const btnValider = creeinput({ type: 'button', value: 'Valider', papa })
    btnValider.style.margin = '1rem 3rem'
    btnValider.addEventListener('click', () => valideParametres(node.nodeNumero, sectionName))
  }
} // suite_parametrage

function construitDivs () {
  edgModale({ width: '620', height: '450', croix: false, autoClose: false })
}

// Fonction appelée pour compléter la boite de dialogue pour un node
async function completeDialogue (node, parent = 'edgModale', prefixe = '') {
  try {
    if (typeof parent === 'string') parent = j3pElement(parent)
    const paramEntete = addElement(parent, 'div', { id: prefixe + 'param_entete' })
    addElement(parent, 'div', { id: prefixe + 'param_donnees_parcours' })
    addElement(parent, 'div', { id: prefixe + 'param_donnees_parcours2' })
    addElement(parent, 'div', { id: prefixe + 'param_donnees_parcours3', style: { display: 'none' } })
    const conteneur = addElement(parent, 'div', { id: prefixe + 'contenudivD' })
    addElement(conteneur, 'div', {
      id: prefixe + 'divparam2',
      style: {
        display: 'inline-block',
        height: '360px',
        width: '400px',
        left: '10px',
        border: 'solid 2px #EEEEEE',
        overflow: 'auto',
        background: '#2D5A7D'
      }
    })
    addElement(conteneur, 'div', {
      id: prefixe + 'divinfos2',
      style: {
        display: 'inline-block',
        fontSize: '16px',
        height: '360px',
        width: '200px',
        float: 'right',
        border: 'solid 2px #EEEEEE',
        overflow: 'auto',
        background: '#2D5A7D',
        paddingTop: '0px',
        paddingLeft: '5px',
        paddingRight: '5px',
        color: '#FFF',
        borderStyle: 'inset'
      }
    })
    addElement(paramEntete, 'h3', { content: `Configuration du nœud n°${node.nodeNumero}` })
    node.nodeParametres.style = node.nodeParametres.style || { back: '#aa0000' }
    // faut charger les params de la section
    const sectionName = node.nodeParametres.section
    if (!sectionName) {
      console.error(Error('Pas de propriété section dans nodeParametres'), node)
      return j3pShowError(Error('Erreur interne, impossible de trouver le nom de la section'))
    }
    await getSectionParams(sectionName, node)
    await suiteParametrage(sectionName, node, prefixe)
  } catch (error) {
    j3pShowError(error)
  }
} // completeDialogue

export function dialogueNode (nodeDomId) {
  // nouvelle boîte de dialogue
  initialiseDialogue()
  // Attention, pb de syntaxe, ici node va être un objet avec deux propriétés, nodeNumero et nodeParametres, ce dernier
  const node = getNodeDansGraphe(nodeDomId)
  // cas d’un node section 'classique'
  if (node.nodeParametres.section !== 'fin') {
    construitDivs()
    completeDialogue(node)
  } else {
    // cas d’un node section fin
    const modaleElt = edgModale({ width: '420', height: '300' })
    const contenudivD = addElement(modaleElt, 'div', { id: 'contenudivD' })
    addElement(contenudivD, 'h3', { content: 'Fin de l`activité' })
    actualiseGestionnaireEvenements()
    $('#' + nodeDomId).css('background-color', '#cccccc')
  }
}

/**
 * Valide le graphe complet
 * @private
 */
function valideParametresGraphe () {
  const objetGraphe = getObjetGraphe()
  for (const index in objetGraphe.nodes) {
    if (!estNoeudFin(index, objetGraphe)) {
      const node = {
        nodeNumero: index,
        nodeParametres: objetGraphe.nodes[index]
      }
      const sectionName = node.nodeParametres.section || ''
      valideParametres(index, sectionName, 'noeud' + index)
    }
  }
  $('#edgMasque, #edgModale').remove()
}

export function dialogueGraphe () {
  const objetGraphe = getObjetGraphe()
  construitDivs()
  const edgModale = j3pElement('edgModale')
  const divParent = addElement(edgModale, 'div', { style: { display: 'inline-block', height: '400px', overflow: 'auto' } })
  const contenuElt = j3pElement('edgModalecontenu')
  if (!contenuElt) {
    console.error(Error('Pas trouvé le contenu à modifier de la boite de dialogue'))
    return
  }
  j3pAddTxt(contenuElt, 'Configuration des nœuds du graphe')
  j3pSetProps(contenuElt, { style: { fontWeight: 'bold', textAlign: 'center' } })
  // TODO : tester parametre liste déroulante d’un node qcq
  for (const index in objetGraphe.nodes) {
    if (!estNoeudFin(index, objetGraphe)) {
      // On recréé un node au sens de dialogueNode (avec le nodeDomId sur lequel on 'aurait' cliqué)
      const node = {
        nodeNumero: index,
        nodeParametres: objetGraphe.nodes[index]
      }
      addElement(divParent, 'div', { id: 'config_noeud' + index })
      completeDialogue(node, 'config_noeud' + index, 'noeud' + index)
      // TODO :
      // 2. Pb : adapter valideParametres
    }
  }
  const div = creediv({ id: 'btn_valider', papa: j3pElement('edgModale') })
  const input = creeinput({ type: 'button', value: 'Valider', papa: div })
  input.addEventListener('click', () => valideParametresGraphe())
}