import $ from 'jquery'
// cf https://swisnl.github.io/jQuery-contextMenu/docs.html
import 'jquery-contextmenu'
// et sa css
import 'jquery-contextmenu/dist/jquery.contextMenu.css'
// on importe aussi le jquery.ui.position qu’il fournit et conseille, ça semble pas si utile… à remettre si le calcul des positions déconne sur certains navigateurs
// import 'jquery-contextmenu/dist/jquery.ui.position'
import { addElement } from 'sesajs/dom'
import { hasProp, isPlainObjectNotEmpty } from 'sesajs/object'
import { j3pShowError } from 'src/legacy/core/functions'
import { getGrapheBibli, resetGraphebibli, addGrapheBibli, addPropertyGrapheBibli, getIndexFromNumero, extraitGrapheBibli } from './grapheBibli'
import edgModale, { dialogueBranchement, dialogueNode, dialogueGraphe } from './boiteDialogue'
import { findPosDiv, gettag, extractNumberFromCss } from './fonctionsUtiles'
import { autoriseConnexions, connexionNodes, getConnexionDansGraphe, supprimeNode, ajouteNodeGraphe, connectTousLesNodes, connectTousLesBranchements } from './fonctionsNodes'
import { dispatch, getCompteurFutur, getCompteurHistorique, getConfigGraphTmp, getJsPlumbInstance, getObjetGraphe, getObjetGrapheNbBranchements, getObjetGrapheNode, getObjetGrapheProp, getPast, getPastLength, getPositions, getPosition, getTitre, setJsPlumbInstance } from './store'
import { ActionCreators } from 'redux-undo'
import { setConfigGraphTmp, delObjetGrapheBranchementProp, setObjetGrapheBranchementProp, setConfigGraphTmpSsElt, pushConfigGraphTmp, setObjetGrapheBranchement, setObjetGrapheTitle, resetObjetGrapheBranchements, addObjetGrapheBranchement, setPosition, enleveIndex, ajouteIndexFutur, enlevePremierFutur, ajouteDernierHistorique, enregistreIndex, videFutur, setConfigGraphTmpSectionOpts } from './actions'
// pour la css ça dépend des versions
// import 'jsplumb/css/jsPlumbToolkit-defaults.css' // 2.1.8
// import 'jsplumb/dist/css/jsPlumbToolkit-defaults.css' // 2.2.10
// import 'jsplumb/dist/css/jsplumbtoolkit-defaults.css' // 2.4.0
import 'jsplumb/css/jsplumbtoolkit-defaults.css' // 2.10.2
// http://jsplumb.github.io/jsplumb/changelog.html n’est pas bavard sur les breaking changes… faut tatonner
// avant la version 2.? pas la peine de lui affecter une variable car jsplumb exporte rien mais déclare des fcs en global
// en 2.1 il faut indiquer script-loader ici, `import 'jsplumb'` marche pas :-/ (et faut le mettre après l’import css)
// require('script-loader!jsplumb') // eslint-disable-line import/no-webpack-loader-syntax
// import 'jsplumb' // à partir de 2.2.0
import { jsPlumb } from 'jsplumb'
import { brProps } from 'src/editGraphe/config'
import { j3pBaseUrl } from 'src/lib/core/constantes'
/** @module editGraphe/scene */
/* eslint camelcase: 0 */ // on verra pour renommer plus tard
// on créé une variable pour l’élément jqueryfié (pour éviter de relancer la construction de $('#edgScene') à chaque fois qu’on en aura besoin
let $sceneElt
export function init (sceneElt, next) {
// on peut être appelé plusieurs fois, faut refaire l’init à chaque fois
// dom.addCss(contextMenuCss)
$sceneElt = $(sceneElt)
// on construit la scene
// vidange de la pîle d’actions à effectuer
// pileActions = []
// vidange de la scene
$sceneElt.empty()
// on ajoute un container pour que jsPlumb mette tout dedans,
// You can - and should - instruct jsPlumb to use some element as the parent of everything jsPlumb adds
// to the UI through usage of the setContainer method in jsPlumb, or by providing the Container in the
// parameters to a jsPlumb.getInstance call.
// dom.addElement(sceneElt, 'p', { className: 'drop', content: 'un peu de texte' })
$sceneElt.add('<p class="drop">un peu de texte</p>')
jsPlumb.ready(function () {
try {
// définition des valeurs par défaut, cf http://jsplumb.github.io/jsplumb/defaults.html
const instanceJsPlumb = jsPlumb.getInstance({
Endpoint: ['Dot', { radius: 2 }], // le point sur le bord de chaque node
HoverPaintStyle: { stroke: '#1e8151', strokeWidth: 10 },
Connector: ['Bezier', { curviness: 30 }],
ConnectionsDetachable: false,
ConnectionOverlays: [
[
'Arrow',
{
location: 1,
id: 'arrow',
length: 14,
foldback: 0.8
}
],
['Label', { label: '', id: 'label', cssClass: 'labelBranchement' }]
],
// Important, cf http://jsplumb.github.io/jsplumb/home.html#container
Container: 'edgScene'
})
setJsPlumbInstance(instanceJsPlumb)
instanceJsPlumb.batch(next)
} catch (error) {
next(error)
}
})
}
// insereNode(jqObj.event, items[0])
export function insereNode (event) {
const grapheTmp = getConfigGraphTmp()
if (grapheTmp && grapheTmp.length > 1) { // si l’on droppe une seule section l’ajout d’un noeud fin se fera manuellement
traitementNoeudFin()
}
const scrollTop = $sceneElt.scrollTop()
const scrollLeft = $sceneElt.scrollLeft()
// décalage dû à la position de la scene dans la page
const posDiv = findPosDiv(document.getElementById('edgScene'))
// calcul
const e = event || window.event
const diffx = e.clientX - posDiv.x + scrollLeft
const diffy = e.clientY - posDiv.y + scrollTop
// appel de la fonction de diagramme.js qui insère le noeud à la position demandée
if (grapheTmp !== '') {
integrerGraphe(diffx, diffy)
}
// appelé ici sans le false en param pour avoir les fenetres de param au prochain branchement
actualiseGestionnaireEvenements()
}
/**
* Nettoie le graphe d’éventuels trous et remplace les (s)nn=fin par des branchements vers des nœuds fin
* @private
*/
export function traitementNoeudFin () {
let maxNN = 0
let grapheTmp = getConfigGraphTmp()
// si le graphe a des 'trous' (passage du noeud 1 à 5 par exemple, les tableaux vont comporter des entrées à null, on les enlève donc
// edit : normalement ne devrait plus arriver puisque les tableaux ne devraient plus avoir de null
const trous = []
const newGraphTmp = grapheTmp.filter((elt, index) => {
if (!Array.isArray(elt)) {
trous.push(index)
return false
}
const num = Number(elt[0])
if (Number.isFinite(num)) maxNN = Math.max(maxNN, num)
else console.warn(`Nœud avec id non numérique : ${elt[0]}`)
return true
})
if (trous.length) {
console.error('Il y avait des trous dans le graphe', trous)
dispatch(setConfigGraphTmp(newGraphTmp))
grapheTmp = getConfigGraphTmp()
}
// @todo voir ça n’existe pas toujours
const maxPos = getPosition(maxNN - 1)
grapheTmp.forEach(function ([id, section, sectionOpts], index) {
const position = getPosition(index)
if (!Array.isArray(sectionOpts)) return console.warn(`Le nœud ${id}, section ${section} n’a pas d’options (ni branchements ni paramètres)`)
if (sectionOpts.some(elt => !elt)) {
// les sections fin on souvent du [null] comme options, on râle pas pour si peu
if (section !== 'fin') console.error(`Le nœud ${id} (section ${section}) a des options invalides`)
// on réaffecte notre var locale sans changer le state
sectionOpts = sectionOpts.filter(isPlainObjectNotEmpty)
// et on màj le state
dispatch(setConfigGraphTmpSectionOpts(index, sectionOpts))
// console.log('graphTmp après avoir viré l’option falsy', JSON.stringify(getConfigGraphTmp()))
}
sectionOpts.forEach(function (sectionOpt, optIndex) {
// faut pas modifier l’objet sur lequel on boucle mais toujours passer par les reducer du store
// on remplace les nn|snn = fin par des numéros vers de nouveaux nœud
;['nn', 'snn'].forEach(p => {
if (typeof sectionOpt[p] === 'string') {
if (sectionOpt[p].toLowerCase() === 'fin') {
// on remplace ce (s)nn=fin par (s)nn=nouveauNumero
maxNN += 1
const maxNnId = String(maxNN)
// console.log(`remplacement de ${p}=fin par ${p}=${maxNN} (nœud fin que l’on crée)`)
dispatch(pushConfigGraphTmp([maxNnId, 'fin', []]))
dispatch(setConfigGraphTmpSsElt(index, optIndex, p, maxNnId))
// les pptés de positionnement ont normalement été renseignées
if (!maxPos && position && position.length === 2) dispatch(setPosition(maxNN - 1, position))
}
} else if (typeof sectionOpt[p] === 'number') {
console.error(Error(`Branchement incorrect, le node ${id} a un branchement avec un ${p} de type number, on le passe en string`), sectionOpt)
dispatch(setConfigGraphTmpSsElt(index, optIndex, p, String(sectionOpt[p])))
}
})
})
})
}
// fonction appelée lorsqu’on droppe une section dans la scène, on a préalablement stocké le graphe au format JSON_bibli dans la variable grapheTmp
// TODO : gérer les nn:"fin", pour l’instant non représentés sur la scène, il faudrait à chaque itération augmenter le maxNumeroNode et remplacer nn:"fin" par nn:maxNumeroNode où maxNumeroNode est la section fin
function integrerGraphe (posx, posy) {
const grapheTmp = getConfigGraphTmp()
let positions = getPositions()
let noeud_depart, infos_branchements, numero_target
// les deux arguments sont les coordonnées de la souris, on maj les positions en les ajoutant à chacune
positions.forEach(function ([x, y], index) {
// on ajoute le décalage, avec min de 0
dispatch(setPosition(index, [Math.max(0, x + posx), Math.max(0, y + posy)]))
})
positions = getPositions()
// Attention : ne pas utiliser ici importerGraphe qui sert uniquement lors de la phase d’initialisation, car repart du noeud 1,
// ici on peut dropper des graphes + tard...
// on en garde la mémoire pour corriger les branchements car ce max est modifié lors des ajouts de nodes
const maxNumeroNodeTemp = getObjetGrapheProp('maxNumeroNode')
const decalage = 20
grapheTmp.forEach(function (node, index) {
node[0] = Number(node[0]) // pour éviter les pbs de graphes enregistrés avec des string...
// ajouteNodeGraphe(node,posx,posy)
const x = (positions && positions[index] && positions[index][0]) || decalage + index * 50
const y = (positions && positions[index] && positions[index][1]) || decalage + index * 50
ajouteNodeGraphe(node, x, y, getTitre(index)) // anciennement maxNumeroNodeTemp + index mais pb
})
actualiseGestionnaireEvenements(false)
// reste les branchements à faire, attention : réindexer correctement les nn...
// on parcourt à nouveau les noeuds du graphe
grapheTmp.forEach(function (node) {
noeud_depart = Number(node[0]) + maxNumeroNodeTemp
infos_branchements = getBranchements(node[2])
infos_branchements.forEach(function (info) {
// lorsqu’on droppe une section à un noeud, on n’a pas appelé traitementNoeudFin
// donc on a un seul embranchement avec un noeud fin (dans grapheTmp mais pas de branche dans objetGraphe ce qui est normal)
// Todo : vérifier l’export si un seul noeud... (en gros pas de node FIN sur la scène
if (info.nn !== 'fin' && info.nn !== 'Fin' && info.nn !== 'FIN') {
numero_target = parseInt(info.nn) + parseInt(maxNumeroNodeTemp)
connexionNodes('node' + noeud_depart, 'node' + numero_target, noeud_depart, info, maxNumeroNodeTemp)
// PB !! à faire en callback normalement...
// completeBranchement(noeud_depart, info, maxNumeroNodeTemp)
}
})
})
}
/*
function ajouteNodeGraphe (node, posx, posy, titre) {
let maxNumeroNode = getObjetGrapheProp('maxNumeroNode')
var nodeTmp = conversionNoeudBibliVersDiagramme(node)
// incrémente le numéro de node courant
maxNumeroNode++
// crée un node dans l’objet graphe, là il faudrait récupérer les infos courantes (section et paramètres)
const newNode = {
section: nodeTmp.section,
parametres: nodeTmp.parametres,
left: posx,
top: posy,
title: titre
}
setObjetGrapheNode(maxNumeroNode, newNode)
// affiche le node sur la scene à la position cliquée, il faudrait plutôt afficher le dernier node créé (ci dessus)
ajouteNodeSurScene(maxNumeroNode, newNode)
}
*/
/**
* Ajoute un node sur la scène
* @param {number} nodeNumero
* @param {object} node
*/
export function ajouteNodeSurScene (nodeNumero, node) {
let html
node.style = node.style || { back: '#cccccc' }
let nodeTitle
if (node.section !== 'fin') {
// EDIT : normalement le if est desormais tjs vrai car on initialise dans tous les cas store:nodeTitres
if (node.title !== null && node.title !== undefined) { // CA arrive quand on droppe une section qui n’a pas d’info titre en bdd
nodeTitle = node.title
dispatch(setObjetGrapheTitle(nodeNumero, nodeTitle))
// faudrait pas aussi lancer ça ?
// setTitre(nodeNumero, nodeTitle)
// non, surtout pas, si on le fait tous les nodes se retrouvent avec un titre Nœud 1 !
} else {
nodeTitle = 'Nœud ' + nodeNumero
console.warn(`node sans title, on impose ${nodeTitle}`)
}
html = '<div style="left:' + node.left + 'px;top:' + node.top + 'px;background-color:' + node.style.back + '" class="node nodeSection" id="node' + nodeNumero + '"><div id="titre_node' + nodeNumero + '">' + nodeTitle + '</div>'
// le node n°1 ne peut pas être supprimé car c’est le node d’entrée
// TODO : à revoir si nécessaire (prendre le min des noeuds
// if (nodeNumero > 1) {
// //html += '<div class="nodeDelete"><img src="img/cross.png" alt="Supprime"></div>'
// }
// else { html += '<div class="nodeEntree"><img src="img/door_in.png" alt="Entrée"></div>';}
// html += '<div class="nodeBranche" id="nodeBranche'+nodeNumero+'"><img src="img/arrow_out.png" alt="Branche"></div>'
html += '<div class="ep"></div>'
html += '<div class="numeronode">' + nodeNumero + '</div>'
// html += '<div class="nodeConfig"><img src="img/cog.png" alt="Paramétrage"></div>'
html += '</div>'
} else {
if (node.title !== null && node.title !== undefined) {
nodeTitle = node.title
dispatch(setObjetGrapheTitle(nodeNumero, nodeTitle))
// faudrait pas aussi lancer ça ?
// setTitre(nodeNumero, nodeTitle)
// non, surtout pas, si on le fait tous les nodes se retrouvent avec un titre Nœud 1 !
} else {
nodeTitle = 'Fin (nœud ' + nodeNumero + ')'
console.warn(`node sans title, on impose ${nodeTitle}`)
}
html = '<div style="left:' + node.left + 'px;top:' + node.top + 'px;background-color:' + node.style.back + '" class="node nodeFin" id="node' + nodeNumero + '"><div id="titre_node' + nodeNumero + '">' + nodeTitle + '</div>'
// html += '<div class='nodeDelete'><img src='img/cross.png' alt='Supprime'></div>'
// html += '<div class='nodeConfig'><img src='img/cog.png' alt='Paramétrage'></div>'
html += '<div class="numeronode">' + nodeNumero + '</div>'
html += '</div>'
}
$sceneElt.append(html)
autoriseConnexions('node' + nodeNumero)
}
// fonction qui convertit un noeud au format de la bibli en noeud au format du diagramme, en en gardant que les infos sur le noeud, pas les branchements
/*
function conversionNoeudBibliVersDiagramme (noeud) {
// le noeud est un tableau dont le premier élément est le numéro du noeud, info inutile dans ajouteNodeGraphe (puisque le nn est incémenté) mais utile dans la conversion globale
const node = {}
node.nn = noeud[0]
node.section = noeud[1]
const index = noeud[2].length - 1
node.parametres = noeud[2][index] // les premiers éléments sont juste les branchements de base, inutiles donc (nn:fin par exemple si pe=sans condition)
return node
}
*/
// fonction appelée par importerGraphe et integrerGraphe qui renseigne objetGraphe pour tous les branchements
export function completeBranchement (indexNodeDepart, infosBranche, decalage) {
// Rmq : les branchements sont déjà partiellement renseignés par actualiseGestionnaireEvenements, le nn est déjà correctement renseigné
let nodeDepart = getObjetGrapheNode(indexNodeDepart)
if (!nodeDepart) return console.error(Error(`Aucun node de départ d’index ${indexNodeDepart}`))
// initialisation de branchements si nécessaire, commenté pour debug
if (!nodeDepart.branchements) {
dispatch(resetObjetGrapheBranchements(indexNodeDepart))
nodeDepart = getObjetGrapheNode(indexNodeDepart)
}
let indexBranchement
if (hasProp(nodeDepart, 'branchements')) {
indexBranchement = nodeDepart.branchements.length - 1
} else {
indexBranchement = 0
}
const branchement = {}
for (const ppte in infosBranche) {
if (indexBranchement > -1 && hasProp(infosBranche, ppte) && ppte !== 'nn') {
const valeur = infosBranche[ppte]
// seule la valeur de la ppte snn est à corriger avec le décalage
if (ppte === 'snn') {
branchement.snn = String(Number(valeur) + Number(decalage)) // correctif au 26/08/17 : pb aux passages modes textes/graphes
} else {
if (ppte === 'conclusion' || ppte === 'sconclusion') {
branchement[ppte] = valeur.replace(/'/g, '’') // pour corriger les graphes existants qui ont des messages avec des apostrophes
} else {
branchement[ppte] = valeur
}
}
}
}
const labelTableau = definitLabel(infosBranche)
// var label = labelTableau[0]
branchement.label = labelTableau[0]
dispatch(addObjetGrapheBranchement(indexNodeDepart, indexBranchement, branchement))
if (infosBranche.snn) {
// cas particulier du snn : on créé un branchement sur la scène mais objetGraphe aura une ppte branchement sans info
const valeur = Number(infosBranche.snn) + decalage
// on va aussi créer une connexion entre le node
connexionNodes('node' + indexNodeDepart, 'node' + valeur)
// et on lui dégage sa ppte nn
// attention, on parle ici du branchement nouvellement créé donc pas l’objet branchement
const nbbranchements = getObjetGrapheNbBranchements(indexNodeDepart)
dispatch(delObjetGrapheBranchementProp(indexNodeDepart, nbbranchements - 1, 'nn'))
// delete branchementSnn.nn
// on donne l’info que c’est un branchement snn sinon on fait pas la différence avec un branchement nouvellement créé (sert pour l’interface de paramétrage du branchement)
dispatch(setObjetGrapheBranchementProp(indexNodeDepart, nbbranchements - 1, 'isSnn', true))
// branchementSnn.label = labelTableau[1]
dispatch(setObjetGrapheBranchementProp(indexNodeDepart, nbbranchements - 1, 'label', labelTableau[1]))
}
}
function definitLabel (infosBranche) {
let labelPrefix, labelSuffix, labelSnn
const label = infosBranche.pe || infosBranche.score
if (infosBranche.pe) {
labelPrefix = 'Phrase d’état :<br> '
} else {
labelPrefix = 'Score '
}
if (infosBranche.snn) {
labelSnn = infosBranche.max ? 'Si echec aux ' + infosBranche.max + ' tentatives ' : 'Après ' + infosBranche.maxParcours + ' passage(s) dans la boucle'
if (label === 'sans condition' || label === 'sanscondition' || label === 'sans_condition' || label === 'sans+condition') {
return ['Sans condition', labelSnn]
} else {
labelSuffix = infosBranche.max ? '<br>Maximum : ' + infosBranche.max : '<br>Maximum : ' + infosBranche.maxParcours
return [labelPrefix + label + labelSuffix, labelSnn]
}
} else {
if (label === 'sans condition' || label === 'sanscondition' || label === 'sans_condition' || label === 'sans+condition') {
return ['Sans condition', '']
} else {
return [labelPrefix + label, '']
}
}
}
function valideTitre (node) {
const nodeNumero = node.substring(4)
let nouveau_titre = gettag('input_titre').value
nouveau_titre = nouveau_titre.replace(/'/g, '’')
gettag('titre_' + node).innerHTML = nouveau_titre
$('#edgMasque, #edgModale').remove()
dispatch(setObjetGrapheTitle(nodeNumero, nouveau_titre))
}
/**
* Retourne un tableau de branchements (éventuellement vide)
* @param {Array} params le 3e élément du tableau d’un node
* @return {Object[]}
*/
export function getBranchements (params) {
if (!Array.isArray(params)) {
console.error(Error('paramètres invalides'), params)
return []
}
const retour = []
params.forEach(param => {
// param peut être null (y’a des nœud du genre ['4', 'fin', [null]])
if (param && param.nn) {
// c donc un branchement et non les paramètres de la section, on filtre sur les prop non undefined
const branchement = {}
brProps.forEach(p => {
if (['string', 'number'].includes(typeof param[p])) branchement[p] = param[p]
})
retour.push(branchement)
}
})
return retour
}
/**
* À passer en propriété position de $.contextMenu, pour rectifier le positionnement du menu contextuel
* (lié au positionnement css des div)
* @private
* @param elt
* @param x
* @param y
*/
function contextMenuPosition (elt, x, y) {
// Attention, header et menu n’existent pas forcément (en preview ou bien avec showParcours)
const $header = $('#edgHeader')
const $menu = $('#edgMenu')
let top = y + $sceneElt[0].scrollTop - 5
if ($header && $header[0]) top -= $header[0].offsetHeight
let left = x + $sceneElt[0].scrollLeft - 10
if ($menu && $menu[0]) left -= $menu[0].offsetWidth
elt.$menu.css({ top, left })
}
// gestionnaire d’événements de la scene, avec un booléen en paramètre utilisé lors du chargement d’un graphe (importerGraphe) ou de la fin d’un nouvel embranchement car on ne souhaite pas afficher la configuration des branchements
export function actualiseGestionnaireEvenements (affiche_dialogue_branchement, next) {
// En premier : on vide les événements précédemment créés
$('.nodeConfig').unbind()
$('.nodeBranche').unbind()
$('.nodeDelete').unbind()
$('.nodeEntree').unbind()
const instanceJsPlumb = getJsPlumbInstance()
instanceJsPlumb.unbind()
// Evénements sur les nodes
instanceJsPlumb.bind('connection', function (info) { // ce qui se passe lors d’une nouvelle connection
// créée une branchement dans le node de départ dans objetGraphe
const numeroNodeDepart = info.connection.sourceId.substr(4) // on retire le prefixe 'node'
const numeroNodeArrivee = info.connection.targetId.substr(4) // on retire le prefixe 'node'
let nodeDepart = getObjetGrapheNode(numeroNodeDepart)
// initialisation de branchements si nécessaire
if (!nodeDepart.branchements) {
dispatch(resetObjetGrapheBranchements(numeroNodeDepart))
nodeDepart = getObjetGrapheNode(numeroNodeDepart)
}
let rangCondition = 0
while (typeof nodeDepart.branchements[rangCondition] === 'object') {
rangCondition++
}
// ce comportement de base s’avère gênant (mettre un nn) car dans le cas d’un branchement contenant un snn,
// on veut le voir mais pas de nn, ceci sera donc modifié dans completeBranchement
// (qui s’occupe aussi de renseigner d’autres pptés dans objetGraphe comme la CCL)
const branchement = {
nn: numeroNodeArrivee,
branchementDomId: info.connection.id,
style: { stroke: '#aaaaaa', strokeWidth: 2, dashstyle: '0' }
// enonce:null,//seront renseignés ailleurs
// conclusion:null
}
dispatch(setObjetGrapheBranchement(numeroNodeDepart, rangCondition, branchement))
// const objetGraphe = getObjetGraphe()
info.connection.getOverlay('label').setLabel('Rang ' + (rangCondition + 1).toString())
// ouvre la boïte de dialogue avec le nouveau branchement
if (affiche_dialogue_branchement !== false) {
dialogueBranchement(info.connection.id)
}
})
// ce qui se passe lorsqu’on survole une connection : on détaille en mettant la PE ou le score
instanceJsPlumb.bind('mouseover', function (conn /*, originalEvent */) {
if (getConnexionDansGraphe(conn.id)) {
const branchement = getConnexionDansGraphe(conn.id)
if (branchement.branchement.label) {
const old_label = conn.getOverlay('label').getLabel()
conn.getOverlay('label').setLabel(branchement.branchement.label)
branchement.branchement.label = old_label
}
}
})
instanceJsPlumb.bind('mouseout', function (conn) {
if (getConnexionDansGraphe(conn.id)) {
const branchement = getConnexionDansGraphe(conn.id)
if (branchement.branchement.label) {
const old_label = conn.getOverlay('label').getLabel()
conn.getOverlay('label').setLabel(branchement.branchement.label)
branchement.branchement.label = old_label
}
}
})
// clic sur un branchement
instanceJsPlumb.bind('click', function (conn) {
const branchementDomId = conn.id
dialogueBranchement(branchementDomId)
})
// menu contextuel, sur les nodes
$.contextMenu({
selector: '.node',
position: contextMenuPosition,
zIndex: 25,
appendTo: '#edgScene',
items: {
rename: {
name: 'Renommer',
callback: function (key, options) {
const nodeConfigurerDomId = options.$trigger[0].id
const titre_actuel = document.getElementById('titre_' + nodeConfigurerDomId).innerHTML
const modaleElt = edgModale({ id: 'edgModale', contenu: '', width: '420', height: '100', booltitre: false, croix: false, autoClose: false })
const contenuElt = addElement(modaleElt, 'div', { id: 'contenudivD', content: 'Changement du titre du nœud : ' })
const input = addElement(contenuElt, 'input', { size: 50, id: 'input_titre', type: 'text', value: titre_actuel })
// cpasbien d’écouter keypress, mais pour la touche entrée c’est permis (sinon faudrait faire
// un vrai form avec un submit et choper le submit pour y faire un preventDefault et lancer notre validation)
input.addEventListener('keypress', (event) => {
if (event.keyCode === 13) valideTitre(nodeConfigurerDomId)
})
const button = addElement(contenuElt, 'button', { style: { display: 'block', width: '6em', margin: '1em auto' }, content: 'Valider' })
button.addEventListener('click', () => valideTitre(nodeConfigurerDomId))
input.focus()
// curseur à la fin
input.selectionStart = input.value.length
},
icon: 'font'
},
edit: {
name: 'Paramétrage',
callback: function (key, options) {
const nodeConfigurerDomId = options.$trigger[0].id
// empile le retour à la normale du branchement sur la scene
// var action = {evenement: 'initialiseDialogue', action: 'quitteNode', parametres: {nodeDomId: nodeConfigurerDomId}}
// pileActions.push(action)
// modifie le node sur la scene, todo : au clic sur la croix ou sur la scene remettre le css
// on réinitialise les css des autres nodes, idéalement au clic sur la croix ce serait mieux, mais on peut aussi cliquer sur la scène pour masquer la boite de dialogue, c’est donc plus simple comme ça
// $('.node').css('background-color', '#cccccc')
// $('#' + nodeConfigurerDomId).css('background-color', '#888888')
// écris les informations du node dans la boîte de dialogue
dialogueNode(nodeConfigurerDomId)
},
icon: 'edit'
},
test: {
name: 'Tester le nœud',
callback: function (key, options) {
// puis l’affichage (qui va mettre à jour GrapheBibli avant)
voirNoeud(options.$trigger[0].id)
},
icon: 'search'
},
// 'cut': {name: 'Cut', icon: 'cut'},
// 'copy': {name: 'Dupliquer le noeud', icon: 'copy'},//possibilité: dupliquer le noeud ? à voir
// 'paste': {name: 'Paste', icon: 'paste'},
delete: {
name: 'Suppression',
callback: function (key, options) {
const nodeSupprimerDomId = options.$trigger[0].id
const nodeSupprimerId = nodeSupprimerDomId.substr(4) // on retire le prefixe 'node'
// if (nodeSupprimerId==1){
// dialogueEntree()
// return
// }
// eslint-disable-next-line no-alert
if (typeof getObjetGrapheNode(nodeSupprimerId) !== 'undefined' && window.confirm('Vous voulez vraiment supprimer ce nœud ?')) {
// TODO : verifier l’utilité du 1er test ci-dessus
const index_jump = getPastLength()
dispatch(enregistreIndex(index_jump))// pour les undo/redo pour pouvoir jumper ici
dispatch(videFutur()) // si dans une suite d’undo une autre action qu’un redo est faite (par exemple un drop) il faut vider le futur
supprimeNode(nodeSupprimerDomId)
}
},
icon: 'delete'
},
sep1: '---------',
quit: {
name: 'Quitter',
callback: function () {
// une callback qui fait rien, juste ferme le menu
return true
},
icon: 'quit'
}
}
})
// menu contextuel sur la scène
$.contextMenu({
selector: '#edgScene',
zIndex: 25,
animation: { duration: 350, show: 'fadeIn', hide: 'fadeOut' },
appendTo: '#edgScene',
position: contextMenuPosition,
// cf https://swisnl.github.io/jQuery-contextMenu/docs/items.html
items: {
undo: {
name: 'Annuler la dernière action',
callback: function () {
const objetGraphe_old = getObjetGraphe()
const compteurHistorique = getCompteurHistorique()
if (compteurHistorique.length === 0) {
return j3pShowError('Il n’y a pas d’action à annuler.')
}
const index = compteurHistorique[compteurHistorique.length - 1]
// const index = compteurHistorique[-1]
const pastObjetGraphe = getPast()
const indexLast = pastObjetGraphe.length - 1
// ajout dans compteurFutur le nb d’actions à refaire
dispatch(ajouteIndexFutur(indexLast - index))
dispatch(ActionCreators.jumpToPast(index))
// dégager de compteurHistorique le dernier element
dispatch(enleveIndex())
// on récupère le premier(?) elt de futur
const objetGraphe_new = getObjetGraphe()
// il faut maintenant redessiner la scène à partir d’objetGraphe
dessineGraphe(objetGraphe_old, objetGraphe_new)
},
icon: 'undo'
},
redo: {
name: 'Refaire',
callback: function () {
const objetGraphe_old = getObjetGraphe()
const compteurFutur = getCompteurFutur()
if (compteurFutur.length === 0) {
return j3pShowError('Il n’y a pas d’action à rétablir.')
}
const index = compteurFutur[0]
const pastObjetGraphe = getPast()
let dernier_index = 0
if (pastObjetGraphe.length) dernier_index = pastObjetGraphe.length - 1
// ajouter dans compteurHistorique
dispatch(ajouteDernierHistorique(dernier_index + 1))
// back to the future...
// PB ! dégage le present de objetgraphe
dispatch(ActionCreators.jumpToFuture(index))
// dégager de compteurFutur le premier élément et diminuer les autres
dispatch(enlevePremierFutur())
const objetGraphe_new = getObjetGraphe()
// il faut maintenant redessiner la scène à partir d’objetGraphe
dessineGraphe(objetGraphe_old, objetGraphe_new)
},
icon: 'redo'
},
test: {
name: 'Tester le graphe',
callback: voirGraphe,
icon: 'search'
},
addFin: {
name: 'Ajouter un nœud FIN',
callback: function (action, opt) {
// contextMenuPosition vient de mettre ça à jour
const posx = extractNumberFromCss(opt.$menu.css('left'))
const posy = extractNumberFromCss(opt.$menu.css('top'))
const node = ['1', 'fin', []]
ajouteNodeGraphe(node, posx, posy, 'fin')
},
icon: 'add'
},
param: {
name: 'Paramétrer les noeuds',
callback: dialogueGraphe,
icon: 'edit'
},
// 'undo': {name: 'Annuler la dernière action', icon: 'undo'},
// 'redo': {name: 'Rétablir l’action annulée', icon: 'redo'},
sep1: '---------',
quit: {
name: 'Quitter cette boite de dialogue',
callback: function () {
return true // une callback qui fait rien, juste ferme le menu
},
icon: 'quit'
}
}
})
if (next) {
next()
}
}
/*
Fonction étudiant les diffs entre deux objetgraphes et mettant à jour la scène (via jsplumb)
TODO : drop graphe, annuler/refaire ok mais pas annuler ensuite
*/
function dessineGraphe (objetGraphe_old, objetGraphe_new) {
// ATTENTION : ne pas emettre d’actions ici sinon ça fait foirer le redo
// let currentNode
for (const nodeNumero in objetGraphe_old.nodes) {
if (!objetGraphe_new.nodes[nodeNumero]) { // un node supprimé
supprimeNode('node' + nodeNumero, false, objetGraphe_old)// le false est pour ne pas emettre d’action (pas de modif d’objetGraphe, c’est déjà fait...)
}// currentNode = objetGraphe_old.nodes[nodeNumero]
}
const instanceJsPlumb = getJsPlumbInstance()
instanceJsPlumb.unbind() // pour virer les écouteurs à la création de branchement
for (const nodeNumero in objetGraphe_new.nodes) {
if (!objetGraphe_old.nodes[nodeNumero]) { // un node ajouté
// const left = objetGraphe_new.nodes[nodeNumero].left
// const top = objetGraphe_new.nodes[nodeNumero].top
// const titre = objetGraphe_new.nodes[nodeNumero].title
// const num = getObjetGrapheMaxNumeroNode() // a voir
ajouteNodeSurScene(nodeNumero, objetGraphe_new.nodes[nodeNumero])
// pb : de nouvelles connexions sont créées, avec des nouveaux conn.id, il faut donc maj les branchementDomId de objetGraphe_new... (qui correspondent à ceux qui ont été effacés), ceci est fait dans connectTousLesNodes
// ajouteNodeGraphe('node' + nodeNumero, left, top, titre, false) // le false est pour ne pas emettre d’action (pas de modif d’objetGraphe, c’est déjà fait...)
}// currentNode = objetGraphe_old.nodes[nodeNumero]
}
for (const nodeNumero in objetGraphe_new.nodes) { // on parcourt à nouveau les nodes pour refaire les branchements maintenant
if (!objetGraphe_old.nodes[nodeNumero]) {
connectTousLesNodes(nodeNumero) // on recréé tous les branchements pointant vers ce noeud travaille uniquement avec objetGraphe_new car emet des actions (le correctif des branchementDomId, qui vont être recréés),
connectTousLesBranchements(nodeNumero) // on recréé tous les branchements partant de ce noeud
}
}
actualiseGestionnaireEvenements() // pour remettre les écouteurs
}
/**
* Affiche le graphe en iframe avec j3p (met à jour Graphebibli puis l’utilise pour l’ajouter dans l’url de j3p.html)
* @todo passer par la méthode display() de sesatheque
*/
function voirGraphe () {
const graphe = updateGrapheBibli()
// console.log('le graphe j3p que l’on va tester : ', graphe)
// @todo importer directement le loader et lui filer un div qu’on créerait ici (mais pour le moment faut une iframe sinon y’a rien qui fonctionne)
const url = j3pBaseUrl + '?graphe=' + encodeURIComponent(JSON.stringify(graphe))
const modaleElt = edgModale({ id: 'edgModale', contenu: '', width: '890', height: '710', booltitre: false })
// pourquoi ce div entre #edgModale et l’iframe ?
const divD = addElement(modaleElt, 'div', { id: 'contenudivD' })
const iframe = addElement(divD, 'iframe', { id: 'iframe_affiche_graphe', src: url, width: 890, height: 710 })
$(iframe).focus()
}
/**
* Retourne la propriété parametres pour une ressource j3p, avec le grapheBibli que l’on mettra à jour
* et la propriété editgraphes avec les positions et titres courants
* @returns {{g: *, editgraphes: {positionNodes: Array, titreNodes: Array}}}
*/
export function getCurrentRessourceParametres () {
// editgraphes.positionNodes
// on parcourt les div d’id nodeX pour récupérer les pptes css left et top
const positionNodes = []
try {
$('.node').each(function () {
positionNodes.push([$(this)[0].offsetLeft, $(this)[0].offsetTop])
})
} catch (error) {
console.error('jquery plante sur le each node')
console.error(error)
}
const titreNodes = []
const objetGraphe = getObjetGraphe()
for (const nodeNumero in objetGraphe.nodes) {
titreNodes.push(objetGraphe.nodes[nodeNumero].title)
}
// reste à mettre à jour le graphe au format tableau et renvoyer le tout
updateGrapheBibli(false)
return {
g: getGrapheBibli(),
editgraphes: { positionNodes, titreNodes }
}
}
/**
* Met à jour l’objet Graphebibli à partir du diagramme courant (objetGraphe)
* (on enlève les pptés de positionnement des noeuds inutiles)
* @param shouldAddMissingNodes
* @return {Array} Le grapheBibli
*/
export function updateGrapheBibli (shouldAddMissingNodes = true) {
// TODO : a terminer quand la ppté enonce fonctionnera, MAJ : ppte enonce a priori abandonnée, virer ce qui suit... (et l’usage de enonce en dessous)
/* function traitement_enonce(Obj){
var objet=new Object
objet.propriete='pe'
objet.valeur='>=0'
return objet
}
*/
// on réinitialise Graphebibli
// Graphebibli = []
resetGraphebibli()
// une première boucle pour corriger l’oubli de noeud FIN (pas fait dans la boucle ci dessous car objetGraphe.nodes évolue après)
let objetGraphe = getObjetGraphe()
let currentNode
if (shouldAddMissingNodes) {
for (const nodeNumero in objetGraphe.nodes) {
currentNode = objetGraphe.nodes[nodeNumero]
if ((!hasProp(currentNode, 'branchements') && currentNode.section.toUpperCase() !== 'FIN') || (hasProp(currentNode, 'branchements') && currentNode.branchements.length === 0)) {
actualiseGestionnaireEvenements(false) // pour éviter la boite de dialogue...
const posDiv = findPosDiv(gettag('node' + nodeNumero))
const x = posDiv.x + 180
const y = posDiv.y + 60
let node = []
node = ['1', 'fin', []]
ajouteNodeGraphe(node, x, y, 'fin')
// PB ici : objetgraphe modifié, on le gette à nouveau
const objetGraphe = getObjetGraphe()
connexionNodes('node' + nodeNumero, 'node' + objetGraphe.maxNumeroNode)
// const branchement = currentNode.branchements[0] || {}
const branchement = (hasProp(currentNode, 'branchements') && currentNode.branchements.length > 0) ? currentNode.branchements[0] : {}
branchement.score = 'sans+condition'
branchement.conclusion = 'Fin de l‘activité'
for (const ppte in branchement) {
dispatch(setObjetGrapheBranchementProp(nodeNumero, 0, ppte, branchement[ppte]))
}
// dispatch(setObjetGrapheBranchement(nodeNumero, 0, branchement)) // commenté car sinon on écrasait le branchement fait automatiquement par actualiseGestionnaireEvenements
// faut pas de alert ici, car on peut être appelé par getCurrentRessourceParametres qui veut juste les params
// TODO à remplacer éventuellement par un dialog, remis pour test
j3pShowError('Le nœud ' + nodeNumero + ' ne comportait pas de branchement, un branchement vers un nœud Fin a été ajouté automatiquement.')
}
}
// ne pas oublier de le getter à nouveau...
objetGraphe = getObjetGraphe()
}
// un graphe de la bibli a un format tableau de tableaux : [ [ 1, "squelettederivation", [ { "pe": ">=0", "nn": "fin", "conclusion": "" }, { "nbrepetitions": 2, "indication": "", "limite": "", "nbchances": 2, "f": "derivees_uv_exp_2" } ] ] ]
// on parcourt les nodes
Object.entries(objetGraphe.nodes).forEach(([nodeNumero, currentNode]) => {
// Edit : plutôt que de placer un element dans Graphebibli à l’index nodeNumero-1, on en ajoute un, pour éviter les creux dans ce tableau qui feront planter j3p
const index = addGrapheBibli([nodeNumero, currentNode.section, []])
// cas particulier : si pas de branchement on ajoute une branche fin
for (const indexbranche in currentNode.branchements) {
// là on pourrait faire des tests pour signaler à l’utilisateur des trucs qui vont pas (absence de branchement par exemple)
const objetBranchement = {}
for (const branche in currentNode.branchements[indexbranche]) {
// on ne garde pas l’info de la ppté style (à voir ce serait ptet pertinent) et branchementDomId, ni le label
if (branche !== 'style' && branche !== 'branchementDomId' && branche !== 'enonce' && branche !== 'label') {
// on ajoute donc au tableau l’objet branchement, là on a un pb sur la ppté enonce qui n’existe pas dans J3P, à remplacer par pe: ou score: (fait ensuite)
objetBranchement[branche] = currentNode.branchements[indexbranche][branche]
}
}
// certains branchements de objetGraphe ne doivent pas être repercutés dans GrapheBibli : ceux qui sont uniquement visuels et issus d’un snn, ils n’ont alors pas de ppté nn
if (hasProp(currentNode.branchements[indexbranche], 'nn')) {
addPropertyGrapheBibli(index, 2, objetBranchement)
}
}
// on termine par le paramétrage :
addPropertyGrapheBibli(index, 2, currentNode.parametres)
// A noter que l’on ne récupère pas les pptés currentNode.left et currentNode.top, peut-être utile pour les garder en mémoire pour un réaffichage ultérieur :
})
return getGrapheBibli()
// TODO : virer les NULL de Graphebibli, plutôt refaire ci dessus et remplacer le nodeNumero par l’ajout d’un element aux deux tableaux
}
/**
* Affiche un des noeud après mise à jour de Graphebibli
* @param numeroNode
*/
function voirNoeud (numeroNode) {
updateGrapheBibli()
let testable = true
const noeud_graphebibli = extraitGrapheBibli(getIndexFromNumero(parseInt(numeroNode.substr(4))))
if (noeud_graphebibli[1] === 'fin' || noeud_graphebibli[1] === 'Fin' || noeud_graphebibli[1] === 'FIN') {
j3pShowError("Un nœud 'Fin' n’est pas testable")
return
}
for (const ppte in noeud_graphebibli[2][1]) {
if (typeof noeud_graphebibli[2][1][ppte] === 'string') {
if (noeud_graphebibli[2][1][ppte].indexOf('j3p.parcours.donnees') !== -1) {
testable = false
}
}
}
if (!testable) {
j3pShowError('Ce nœud utilise des paramètres d’un nœud précédent, il n’est donc pas testable seul.')
return
}
// faut virer tous les branchements pour n'en garder qu'un sans condition vers un nœud fin
const branchementsParams = noeud_graphebibli[2]
if (!Array.isArray(branchementsParams)) {
console.error(Error('Nœud invalide'), noeud_graphebibli)
return j3pShowError('Nœud invalide')
}
const graphe = [[
'1',
noeud_graphebibli[1],
[
{ nn: 'fin', score: 'sans condition' },
branchementsParams[branchementsParams.length - 1]
]
], [
'2', 'fin'
]]
const url = j3pBaseUrl + 'prod1.html#' + encodeURIComponent(JSON.stringify(graphe))
const modaleElt = edgModale({ id: 'edgModale', contenu: '', width: '890', height: '710', booltitre: false })
const divD = addElement(modaleElt, 'div', { id: 'contenudivD' })
const iframe = addElement(divD, 'iframe', { id: 'iframe_affiche_noeud', src: url, width: 890, height: 710 })
$(iframe).focus()
}