/**
* Les fonctions liées à jsplumb pour relier les nodes
* @module editGraphe/fonctionsNode
*/
/* eslint camelcase: 0 */ // on verra pour renommer plus tard
import $ from 'jquery'
import { hasProp } from 'sesajs/object'
import { dispatch, getJsPlumbInstance, getObjetGraphe, getObjetGrapheMaxNumeroNode, getObjetGrapheNode, getObjetGrapheProp, loopObjetGrapheBranchements, loopObjetGrapheNodes } from './store'
import { ajouteNodeSurScene, completeBranchement } from './scene'
import { incrementeMaxNN, setObjetGrapheProp, setObjetGrapheNode, delObjetGrapheNode, delObjetGrapheBranchement, resetObjetGrapheBranchements, reindexObjetGrapheBranchements, setObjetGrapheBranchement } from './actions'
/**
* Crée la connection entre deux noeuds
* @param source par ex nodeX
* @param target
* @param indexDepart
* @param infosBranche
* @param decalage
* @returns {*|{server}}
*/
export function connexionNodes (source, target, indexDepart, infosBranche, decalage) {
const connexion = getJsPlumbInstance().connect({ source, target })
if (indexDepart) {
completeBranchement(indexDepart, infosBranche, decalage)
}
return connexion
}
/**
* Supprime une connexion sur la scene (jsPlumb only, pas de modif du store)
* @param branchement
*/
export function detacheBranchement (branchement) {
getJsPlumbInstance().deleteConnection(branchement)
}
function est_connexion (source, target) {
let bool = false
const instanceJsPlumb = getJsPlumbInstance()
const connexions = instanceJsPlumb.getConnections()
for (const i in connexions) {
const sourceId = connexions[i].sourceId.substr(4)
const targetId = connexions[i].targetId.substr(4)
if (String(source) === sourceId && String(target) === targetId) {
bool = true
}
}
return bool
}
/**
* Permet à un nœud d'être source et destination de connexions
* @param node
*/
export function autoriseConnexions (node) {
// const identifiant = 'node' + numero
// var identifiant_branche='nodeBranche'+numero
// var id_branche=document.getElementById(identifiant).lastChild
const nodeElt = document.getElementById(node)
if (nodeElt) {
const instanceJsPlumb = getJsPlumbInstance()
instanceJsPlumb.draggable(nodeElt, {})
instanceJsPlumb.makeSource(nodeElt, {
filter: '.ep',
connector: 'StateMachine',
endpoint: 'Blank',
anchor: 'Continuous',
connectorStyle: { stroke: '#aaaaaa', strokeWidth: 4, dashstyle: '0' },
connectorHoverStyle: { stroke: '#aa0000', strokeWidth: 4, dashstyle: '0' }
})
// autorisation de devenir une cible
instanceJsPlumb.makeTarget(nodeElt, {
dropOptions: { hoverClass: 'dragHover' },
anchor: 'Continuous',
// endpoint:'Blank',
allowLoopback: true
})
} else {
console.error('aucun élément', node)
}
}
export function actualiseGestionnaireEvenementsColoration (options) {
const instanceJsPlumb = getJsPlumbInstance()
// En premier : on vide les événements précédemment créés
// $('.nodeConfig').unbind()
// $('.nodeBranche').unbind()
// $('.nodeDelete').unbind()
// $('.nodeEntree').unbind()
// 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))
info.connection.getOverlay('label').setLabel('Rang ' + (rangCondition + 1).toString())
// ouvre la boïte de dialogue avec le nouveau branchement
})
// ce qui se passe lorsqu’on surole une connection : on détaille en mettant la PE ou le score
// dans ce cas de parcours coloré, on met le score de l’élève (ou la PE...) si c’est dispo (sinon il est pas arrivé là...)
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()
let new_label = ''
// on teste si l’on a une info du score ou pe de l’élève
for (let k = 0; k < options.noeuds.length - 1; k++) {
if (String(options.noeuds[k]) === String(branchement.sourceNodeId) && String(options.noeuds[k + 1]) === String(branchement.branchement.nn)) {
if (hasProp(branchement.branchement, 'pe')) {
new_label = 'Phrase d’état de l’élève : ' + options.pe[k]
} else {
new_label = 'Score de l’élève : ' + options.scores[k]
}
}
}
if (new_label !== '') {
conn.getOverlay('label').setLabel(new_label)
} else {
conn.getOverlay('label').setLabel(branchement.branchement.label)
}
branchement.branchement.label = old_label
}
}
})
instanceJsPlumb.bind('mouseout', 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
}
}
})
}
/* *******************
* Fonctions connexes
*/
// fonction appelée par integrerGraphe à chaque noeud ajouté
// - en theorie si on ajoute une ressource graphe, plusieurs noeuds à afficher sur la scène
// - on maj objetGraphe en plus de l’ajout sur la scène
export function ajouteNodeGraphe (node, posx, posy, titre, action_a_emettre = true) {
// console.log('on ajoute un noeud', node, 'avec maxNumeroNode', getObjetGrapheProp('maxNumeroNode'))
const nodeTmp = conversionNoeudBibliVersDiagramme(node)
// incrémente le numéro de node courant
// const num = setObjetGrapheMaxNumIncrement()
if (action_a_emettre) {
dispatch(incrementeMaxNN())
}
const num = getObjetGrapheMaxNumeroNode()
// 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
}
if (action_a_emettre) {
dispatch(setObjetGrapheNode(num, newNode))
}
// affiche le node sur la scene à la position cliquée, il faudrait plutôt afficher le dernier node créé (ci dessus)
ajouteNodeSurScene(num, newNode)
}
/**
* Sensiblement identique à ajouteNodeGraphe, appelée lors de l’import d’un graphe existant,
* permet de conserver les infos de numéros des noeuds
* @param node
* @param posx
* @param posy
* @param titre
*/
export function importeNodeGraphe (node, posx, posy, titre) {
const nodeTmp = conversionNoeudBibliVersDiagramme(node)
// détermine le maxNumeroNode
if (getObjetGrapheProp('maxNumeroNode') < Number(nodeTmp.nn)) {
dispatch(setObjetGrapheProp('maxNumeroNode', Number(nodeTmp.nn)))
}
// 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
}
dispatch(setObjetGrapheNode(Number(nodeTmp.nn), newNode))
// affiche le node sur la scene à la position cliquée, il faudrait plutôt afficher le dernier node créé (ci dessus)
ajouteNodeSurScene(Number(nodeTmp.nn), newNode)
}
/**
* 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.
* @param {Array} noeud
* @return {Object}
* @private
*/
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]
if (noeud.length > 2) { // on peut avoir un noeud fin du type [n, 'fin'] sans paramétrage (ni branchement)
const nodeOpts = noeud[2]
if (nodeOpts.length) {
// les premiers éléments sont les branchements de base, inutiles ici (pourquoi ?),
// on prend le dernier élément qui devraient être l’oblet des paramètres
const parametres = nodeOpts[nodeOpts.length - 1]
// sauf si c’est un branchement !
node.parametres = hasProp(parametres, 'nn') ? {} : parametres
} else {
node.parametres = {}
}
}
return node
}
// fonction qui parcourt objetgraphe (ou celui donné en param) et créé toutes les connexions qui pointent vers le node donné
// fonction utilisée lorsqu’on redessine la scène lors du undo/redo
export function connectTousLesNodes (nodeId, objetGraphe = getObjetGraphe()) {
let node
for (const nodeIndex in objetGraphe.nodes) {
if (hasProp(objetGraphe.nodes, nodeIndex)) {
node = objetGraphe.nodes[nodeIndex]
// on boucle sur les nodes sauf ceux de fin
if (node && node.branchements) {
for (const branchementIndex in node.branchements) {
if (!est_connexion(nodeIndex, nodeId) && hasProp(node.branchements, branchementIndex) && node.branchements[branchementIndex].nn && Number(node.branchements[branchementIndex].nn) === Number(nodeId)) {
// connection entre "node"+nodeIndex et "node"+nodeId
const branchement = node.branchements[branchementIndex]
const conn = connexionNodes('node' + nodeIndex, 'node' + nodeId)
branchement.branchementDomId = conn.id // c’est là où l’on maj la référence de la nouvelle connexion
// node.branchements[branchementIndex].branchementDomId = conn.id
dispatch(setObjetGrapheBranchement(nodeIndex, branchementIndex, branchement, false))
const jp_connexion = getJspConnexionFromBranchementDomId(conn.id)
jp_connexion.getOverlay('label').setLabel('Rang ' + (Number(branchementIndex) + 1).toString())
}
if (hasProp(node.branchements, branchementIndex) && node.branchements[branchementIndex].snn && Number(node.branchements[branchementIndex].snn) === Number(nodeId)) {
// const branchement = node.branchements[branchementIndex]
const conn = connexionNodes('node' + nodeIndex, 'node' + nodeId)
// on boucle pour retrouver le bon branchement à modifier
for (const branchementNumeroBis in node.branchements) {
if (hasProp(node.branchements, branchementNumeroBis) && node.branchements[branchementNumeroBis].isSnn) {
const branchement = node.branchements[branchementNumeroBis]
branchement.branchementDomId = conn.id // c’est là où l’on maj la référence de la nouvelle connexion
// node.branchements[branchementIndex].branchementDomId = conn.id
dispatch(setObjetGrapheBranchement(nodeIndex, branchementNumeroBis, branchement, false))
const jp_connexion = getJspConnexionFromBranchementDomId(conn.id)
jp_connexion.getOverlay('label').setLabel('Rang ' + (Number(branchementNumeroBis) + 1).toString())
}
}
}
}
}
}
}
}
// fonction qui parcourt objetgraphe (ou celui donné en param) et créé toutes les connexions partant du node donné
// pb : ce branchement peut déjà avoir été recréé par la fonction ci dessus (avec un autre node)
// fonction utilisée lorsqu’on redessine la scène lors du undo/redo
export function connectTousLesBranchements (nodeId, objetGraphe = getObjetGraphe()) {
let node
if (hasProp(objetGraphe.nodes, nodeId)) {
node = objetGraphe.nodes[nodeId]
if (node && node.branchements) {
for (const branchementIndex in node.branchements) {
const branchement = node.branchements[branchementIndex]
// il faut tester si le branchement existe pas déjà... ATTENTION: pas forcément de ppté nn si c’est un snn...
if (hasProp(branchement, 'nn')) {
// pb : existe déjà dans objetgraphe (meme si pas maj), il faudrait plutôt faire le test de la connexion jsplumb
// if (!getBranchementEntreDeuxNodes(nodeId, branchement.nn)) {
if (!est_connexion(nodeId, branchement.nn)) {
const conn = connexionNodes('node' + nodeId, 'node' + branchement.nn)
branchement.branchementDomId = conn.id // c’est là où l’on maj la référence de la nouvelle connexion
// node.branchements[branchementIndex].branchementDomId = conn.id
dispatch(setObjetGrapheBranchement(nodeId, branchementIndex, branchement, false))
const jp_connexion = getJspConnexionFromBranchementDomId(conn.id)
jp_connexion.getOverlay('label').setLabel('Rang ' + (Number(branchementIndex) + 1).toString())
}
} /* else { // commenté le 19/04/17 car faisait doublon pour les snn puisque recréés dans la fonction précédente
if (branchement.isSnn) {
// on parcourt à nouveau les branchements pour retrouver le snn
for (let branchementNumeroBis in node.branchements) {
if (hasProp(node.branchements[branchementNumeroBis], 'snn')) {
// if (!getBranchementEntreDeuxNodes(nodeId, String(node.branchements[branchementNumeroBis].snn), true)) {
if (!est_connexion(nodeId, String(node.branchements[branchementNumeroBis].snn))) {
const conn = connexionNodes('node' + nodeId, 'node' + node.branchements[branchementNumeroBis].snn)
branchement.branchementDomId = conn.id
dispatch(setObjetGrapheBranchement(nodeId, branchementIndex, branchement, false))
let jp_connexion = getJspConnexionFromBranchementDomId(conn.id)
jp_connexion.getOverlay('label').setLabel('Rang ' + (Number(branchementIndex) + 1).toString())
}
}
}
}
} */
}
}
}
}
/**
* Retourne le branchement (et son contexte)
* @param {string} branchementDomId
* @param [objetGraphe]
* @return {Connexion} ou null si on l’a pas trouvé
*/
export function getConnexionDansGraphe (branchementDomId, objetGraphe = getObjetGraphe()) {
let node, branchement
// on parcourt les nodes et les graphes et on s’arrête dès qu’on a trouvé
for (const nodeId in objetGraphe.nodes) {
if (hasProp(objetGraphe.nodes, nodeId)) {
node = objetGraphe.nodes[nodeId]
// on boucle sur les nodes sauf ceux de fin
if (node && node.branchements) {
for (const branchementIndex in node.branchements) {
if (hasProp(node.branchements, branchementIndex)) {
branchement = node.branchements[branchementIndex]
if (branchement.branchementDomId === branchementDomId) {
return {
sourceNodeId: nodeId,
branchementIndex,
branchement
}
}
}
}
}
}
}
console.error('avec le graphe', objetGraphe, Error(`pb dans getConnexionDansGraphe, pas trouvé la connexion #${branchementDomId}`))
return null // ne devrait pas se produire sauf bug
}
/**
* Ré-indexation des branchements d’un node (helper de supprimeNode et supprimeBranchement)
*
* Si par exemple un node possède 5 branchements de rangs : 0;1;2;3;4 et qu’on supprime le branchement de rang 2
* alors les branchements de rangs 3 et 4 sont respectivement ré-indéxés en 2 et 3
* il faut donc manipuler objetGraphe (c’est reindexObjetGrapheBranchements)
* et afficher les rangs corrects sur les branchements de la scene (c’est actualiseRangsSurScene, si la précédente à changé qqchose)
* @param nodeAReindexer
*/
export function reindexeBranchementsNode (nodeAReindexer) {
dispatch(reindexObjetGrapheBranchements(nodeAReindexer))
// maintenant on s’occupe de l’affichage sur la scene
actualiseRangsSurScene(nodeAReindexer)
}
/**
* renvoie une connexion jsPlumb à partir de son id dans le DOM
* @param {string} branchementDomId
* @param {ObjetGraphe} [objetGraphe]
* @return {JspConnexion} ou null si on l’a pas trouvé
*/
export function getJspConnexionFromBranchementDomId (branchementDomId, objetGraphe = getObjetGraphe()) {
const connexion = getConnexionDansGraphe(branchementDomId, objetGraphe)
let nodeTargetDomId
const nodeId = connexion.sourceNodeId
const nodeSourceDomId = 'node' + nodeId
// on cherche la target, ça dépend nn ou snn
if (connexion.branchement && hasProp(connexion.branchement, 'nn')) {
nodeTargetDomId = 'node' + connexion.branchement.nn
} else {
// c’est un snn... on parcourt à nouveau les branchements pour récupérer le premier snn sur ce node
// loopObjetGrapheBranchements est un branchements.forEach qui s’arrête si on return false
loopObjetGrapheBranchements(nodeId, function (branchement) {
if (hasProp(branchement, 'snn')) {
nodeTargetDomId = 'node' + branchement.snn
return false
}
}, objetGraphe)
}
if (nodeTargetDomId) {
// cf http://jsplumb.github.io/jsplumb/querying.html#getConnectionsAdvanced
// avec getConnections un true en 2e argument fait qu’on récupère un Array (et pas un objet dont chaque prop est un scope et la value un array d’objets connection)
// avec select, on peut utiliser each pour boucler sur les connections
let jspConnexion
getJsPlumbInstance().select({
source: nodeSourceDomId,
target: nodeTargetDomId
}).each(connection => {
if (connection.id === branchementDomId) jspConnexion = connection
})
if (jspConnexion) {
return jspConnexion
}
}
console.warn(`getJspConnexionFromBranchementDomId n’a pas trouvé de connexion avec branchementDomId=${branchementDomId}`)
return null // ne devrait pas se produire
}
/**
* Retourne l’id du dom de l’élément jsPlumb du branchement entre deux nodes
* (reste à traiter le cas où y’en aurait deux distincts, un en cas de réussite et l’autre après N échec)
* @param {string} nodeSourceId
* @param {string} nodeTargetId
* @returns {string} domId ou undefined si on l’a pas trouvé
*/
export function getBranchementEntreDeuxNodes (nodeSourceId, nodeTargetId) {
if (typeof nodeSourceId !== 'string') nodeSourceId = String(nodeSourceId)
if (typeof nodeTargetId !== 'string') nodeTargetId = String(nodeTargetId)
let branchementDomId
// Ajout 2022-10-17, on cherche le branchement dont le nn ou le snn est nodeTargetId
const objetGraphe = getObjetGraphe()
const branchements = objetGraphe.nodes[nodeSourceId]?.branchements
if (!branchements?.length) return console.error(Error(`Le node ${nodeSourceId} n’a pas de branchements`))
for (const branchement of branchements) {
if (String(branchement?.nn) === nodeTargetId || String(branchement?.snn) === nodeTargetId) {
return branchement.branchementDomId
}
}
// fin ajout 2022-10-17, si on a pas trouvé on laisse l’ancien code qui suit
// 2 possibilités pour un noeud fin, soit un bilan avec 'fin' (a priori vieux bilan), soit un bilan avec noeud x mais la section correspondante est une section FIN
if (nodeTargetId.toUpperCase() === 'FIN' || getObjetGrapheNode(nodeTargetId).section.toUpperCase() === 'FIN') {
// on cherche parmi les branchements du noeud de départ le premier qui mène vers une section fin
loopObjetGrapheBranchements(nodeSourceId, function (branchement, i) {
if (branchement.nn && getObjetGrapheNode(branchement.nn).section === 'fin') {
// on a trouvé un nn vers un nœud fin, on prend et on arrête là
branchementDomId = branchement.branchementDomId
return false
}
if (branchement.snn && getObjetGrapheNode(branchement.snn).section === 'fin') {
// On a trouvé un branchement avec un snn vers la fin, donc il a deux connexions
// et branchement.branchementDomId concerne la connexion nn, on veut la connexion snn
// on reboucle pour trouver une connexion sans nn
return loopObjetGrapheBranchements(nodeSourceId, function (branchement, i) {
if (!branchement.nn) {
branchementDomId = branchement.branchementDomId
return false
}
})
}
})
} else {
loopObjetGrapheBranchements(nodeSourceId, function (branchement) {
if (String(branchement.nn) === nodeTargetId || String(branchement.snn) === nodeTargetId) {
branchementDomId = branchement.branchementDomId
return false
}
})
// potentiellement une difficulté, si le noeud 3 oriente vers 4 en cas de réussite
// et aussi en cas d’echec apres plusieurs succès,
// comment faire la différence visuellement ? (il faut plus d’infos que la liste des noeuds..)
}
return branchementDomId
}
export function supprimeNode (nodeDomId, action_a_emettre = true, objetGraphe = getObjetGraphe()) {
// récupération du numéro du node
const nodeIdASupprimer = nodeDomId.substr(4) // on retire le prefixe "node"
// boucle de suppression (de la scene) des branchements dont le node est la source
loopObjetGrapheBranchements(nodeIdASupprimer, function (branchement) {
// suppression du branchement sur la scene
// ALEX : var globale ??
const branchementDomId = branchement.branchementDomId
const branchementScene = getJspConnexionFromBranchementDomId(branchementDomId, objetGraphe)
detacheBranchement(branchementScene)
}, objetGraphe)
// recherche et suppression (de la scene et dans objetGraphe) des branchements
// dont le node à supprimer est la cible
loopObjetGrapheNodes(function (node, nodeId) {
loopObjetGrapheBranchements(nodeId, function (branchement, i) {
// on évite le branchement ou source=cible, déjà traité dans la boucle précédente...
if (Number(branchement.nn) === Number(nodeIdASupprimer) && Number(nodeIdASupprimer) !== Number(nodeId)) {
// suppression du branchement sur la scene
const branchementDomId = branchement.branchementDomId
const branchementScene = getJspConnexionFromBranchementDomId(branchementDomId, objetGraphe)
detacheBranchement(branchementScene)
// suppression du branchement dans objetGraphe (ça réindexe)
if (action_a_emettre) {
dispatch(delObjetGrapheBranchement(nodeId, i))
}
}
}, objetGraphe)
}, objetGraphe)
// suppression du node (de la scene)
$('#' + nodeDomId).remove()
// suppression du node (et donc des branchements) (dans objetGraphe)
if (action_a_emettre) {
dispatch(delObjetGrapheNode(nodeIdASupprimer))
}
}
/**
* Retourne le nodeId et le node dans un objet
* @param {string} nodeDomId l’id du node dans le dom
* @returns {{nodeNumero: string, nodeParametres: Node}}
*/
export function getNodeDansGraphe (nodeDomId) {
const nodeId = nodeDomId.substr(4) // on retire le prefixe "node"
const node = getObjetGrapheNode(nodeId)
if (node) {
return {
nodeNumero: nodeId,
nodeParametres: node
}
} else {
// ne devrait pas se produire sauf bug
console.error(`node ${nodeDomId} pas trouvé dans le dom`)
return false
}
}
/**
* Insère ou actualise le rang de chaque condition sur la flèche dans la scene.
* Avec false comme paramètre, tout le graphe est réactualisé (utile suite à un chargement de graphe)
* mais on peut indiquer en paramètres le numéro du node concerné et seul ce node sera alors réactualisé
* @param nodeAReindexer
*/
export function actualiseRangsSurScene (nodeAReindexer) {
function actualiseRangsNodeSurScene (nodeAReindexer) {
loopObjetGrapheBranchements(nodeAReindexer, function (branchement, i) {
const branchementJsPlumb = getJspConnexionFromBranchementDomId(branchement.branchementDomId)
branchementJsPlumb.getOverlay('label').setLabel('Rang ' + (i + 1))
})
}
if (nodeAReindexer) {
actualiseRangsNodeSurScene(nodeAReindexer)
} else {
// on parcourt tous les nodes
loopObjetGrapheNodes(function (node, nodeId) {
if (node.branchements) actualiseRangsNodeSurScene(nodeId)
})
}
}