import $ from 'jquery'
import { addNode, build, getAsRef } from '@sesatheque-plugins/arbre/lib'
import { addElement } from 'sesajs/dom'
import { fetchPersoOnCurrent, fetchPrivateRessource, fetchPublicRessource, fetchRef } from 'sesatheque-client/src/fetch'
import { getBaseId } from 'sesatheque-client/src/sesatheques'
import { sourceTreeRid, typeBibliConnus } from './config'
import edgModale, { j3pModaleError } from './boiteDialogue'
import { insereNode } from './scene'
import { dispatch, getConfigProp, getObjetGrapheProp, getPastLength } from './store'
import { setConfigGraphTmp, setPositions, setPosition, setTitres, toggleExplicationsGraphique, enregistreIndex, videFutur } from './actions'
// le css pour les ressources jstree
import '@sesatheque-plugins/arbre/public/arbre.css' // pour le .jstree-themeicon-custom et #display #apercu
import '@sesatheque-plugins/arbre/public/icons.css' // pour les icones de ressources
/** @module editGraphe/menu */
/**
* Ajoute le listener pour dnd_stop
* @private
*/
function addDropListener () {
// pb car l’événement dnd_stop.vakata a une target sur le document entier,
// on peut donc pas filtrer sur la target, sauf à remonter jqObj.data.obj.0.parentNode.parentNode…
// cf l’exemple http://jsfiddle.net/38vbrbpn/ pour jstree 3.3.3
$(document).on('dnd_stop.vakata', function (event, jqObj) {
// event est un event du dom
// jqObj est un objet construit par jQuery, avec
// jqObj.event est un jQuery.Event, c’est lui qui a les infos clientX et offsetX
// jqObj.event.target un élément du dom passé par jQuery
// jqObj.event.target.id l’id html de l’élément sur lequel on drop
// jqObj.data est un objet avec des éléments de jstree
// jqObj.data.nodes un tableau des id jstree (les id jstree des items droppés)
try {
// bizarre, parfois jqObj.event.target n’existe pas
if (!jqObj.event.target || jqObj.event.target.id !== 'edgScene') return
// la liste des nodes du drop est là
jqObj.data.nodes.forEach(function (nodeId) {
const item = getAsRef($menuElt, nodeId)
if (item.type === 'arbre') console.info('drop d’un arbre ignoré')
else if (item.type && item.type !== 'error' && item.aliasOf) addItemToScene(item, jqObj.event)
})
} catch (error) {
console.error(error)
}
})
}
function addItemToScene (item, event) {
dispatch(setTitres([]))
// servira pour le positionnement des nodes, enregistré dans la bibli (champ commentaires, ppté editgraphes.positionNodes
// par défaut pas de positionnement particulier (si pas d’info dans la bibli, uniquement utile pour les graphes à plusieurs noeuds)
dispatch(setPositions([[0, 0]]))
// appel ajax pour récupérer toutes les infos qu’on n’a pas dans une Ref
const fetch = item.public ? fetchPublicRessource : fetchPrivateRessource
fetch(null, item.aliasOf, function (error, ressource) {
if (error || !ressource) {
if (error) console.error(error)
else console.error('aucune ressource ' + item.aliasOf)
const message = (error && error.message) || 'Impossible de récupérer les infos de cette ressource.'
edgModale({ titre: 'Erreur', contenu: message, booltitre: true, width: 50, height: 100 })
return
}
const indexJump = getPastLength()
dispatch(enregistreIndex(indexJump))// 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
addRessource(ressource, event)
})
}
/**
* Ajoute les ressurces persos s’il y en a
*/
function addPerso () {
// faut pas appeller cette fonction si on est pas sur une sesathèque
if (!getBaseId(document.location.origin + '/', null)) return
// 100 max
fetchPersoOnCurrent({ limit: 100 }, function (error, liste) {
if (error) return console.error(error)
liste = liste.filter((ref) => typeBibliConnus.includes(ref.type))
if (!liste.length) return
const branche = {
type: 'arbre',
titre: 'Mes ressources',
enfants: liste
}
addNode($menuElt, branche, { parentRef: '#' }, function (error, node) {
if (error) console.error(error)
})
})
}
/**
* Appelé avec le résultat de l’appel ajax vers la bibli, après un drop sur la scene
* @param ressource
* @param dropEvent le jqObj.event de dnd_stop.vakata
*/
function addRessource (ressource, dropEvent) {
let graphe, idOrigine
if (ressource.type === 'em') { // Exercice mathenpoche
const modeleMep = ressource.parametres.mep_modele
idOrigine = ressource.idOrigine
// on génère le graphe:
if (modeleMep === 1 || modeleMep === '1' || modeleMep === 'mep1') {
graphe = [[1, 'ancienexo_mep', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { exo: idOrigine }]]]
} else {
graphe = [[1, 'exo_mep', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { exo: idOrigine }]]]
}
dispatch(setTitres([ressource.titre]))
} else if (ressource.type === 'am') {
idOrigine = ressource.idOrigine
graphe = [[1, 'aide_mep', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { aide: idOrigine }]]]
dispatch(setTitres([ressource.titre]))
} else if (ressource.type === 'j3p') {
graphe = ressource.parametres.g
if (!graphe) return j3pModaleError('Cette ressource j3p ne contient pas de graphe')
try {
const offsetX = ressource.parametres.editgraphes.positionNodes[0][0]
const offsetY = ressource.parametres.editgraphes.positionNodes[0][1]
dispatch(setPositions(ressource.parametres.editgraphes.positionNodes.map(function (position) {
return [position[0] - offsetX, position[1] - offsetY]
})))
} catch (e) {
graphe.forEach(function (elt, index) {
dispatch(setPosition(index, [50 * index, 50 * index]))
})
}
if (!ressource.parametres.editgraphes || !ressource.parametres.editgraphes.titreNodes) {
if (ressource.parametres.g && ressource.parametres.g.length && ressource.parametres.g.length === 1 && ressource.titre) {
dispatch(setTitres([ressource.titre]))
} else {
// on ajoute les titres avec les n° des nodes ajoutés, mais ça va faire un 2e "Nœud 1" si y’en avait déjà un
// par ailleurs, ça écrase les titres des nodes déjà sur la scène, mais dans les titres, pas dans objetGraphe,
// ça semble pas grave (edit : ça écrase l’objet titreNodes qui sert en fait à rien... c’est bien objetGraphe qui est à maj et c’est fait à prêt avec setObjetGrapheNode)
dispatch(setTitres(graphe.map(function (noeud) {
if (noeud && noeud.length) {
return 'Nœud ' + (Number(noeud[0]) + getObjetGrapheProp('maxNumeroNode'))
}
return 'nœud vide'
})))
}
} else {
dispatch(setTitres(ressource.parametres.editgraphes.titreNodes))
}
} else if (ressource.type === 'ato') {
graphe = [[1, 'squeletteatome', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { atome: ressource.idOrigine }]]]
dispatch(setTitres([ressource.titre]))
} else if (ressource.type === 'ecjs') {
graphe = [[1, 'calculatice', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { exo: ressource.idOrigine }]]]
dispatch(setTitres([ressource.titre]))
} else if (ressource.type === 'iep') {
// cas particulier : on dispose parfois d’un XML iep, parfois non, auquel cas on a l’URL et on utilise alors la section lecteuriepparurl avec le param urlpn
if (ressource.parametres && ressource.parametres.xml && ressource.parametres.xml !== '') {
graphe = [[1, 'lecteuriep', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { script: ressource.parametres.xml }]]]
dispatch(setTitres([ressource.titre]))
} else {
if (ressource.parametres && ressource.parametres.url && ressource.parametres.url !== '') {
graphe = [[1, 'lecteuriepparurl', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { url: ressource.parametres.url }]]]
dispatch(setTitres([ressource.titre]))
} else {
j3pModaleError('La ressource IEP n’est pas correctement renseignée dans la base ')
}
}
} else if (ressource.type === 'url') {
graphe = [[1, 'url', [{ pe: 'sans condition', nn: 'fin', conclusion: 'Fin' }, { id: ressource.rid }]]]
dispatch(setTitres([ressource.titre]))
} else {
j3pModaleError(`Type de ressource non géré (${ressource.type})`)
}
// on met ce graphe dans le store dans grapheTmp et
dispatch(setConfigGraphTmp(graphe))
insereNode(dropEvent)
// on affiche l’info
if (getConfigProp('afficheExplicationsGraphique')) {
const ch = 'Un nœud a été ajouté dans le graphe, vous pouvez le paramétrer<br>' +
'à l’aide d’un clic droit sur le nœud et ajouter des liens entre les nœuds.<br>' +
"<input type='checkbox' id='cac' name=CAC value='' onchange='toggleExplicationsGraphique(this)'><em>Ne plus afficher ce message.</em> <br>"
edgModale({ contenu: ch, width: 50, height: 100 })
}
}
/**
* Appelé au clic droit "tester la ressource" (sur une ressource du menu)
* Ouvre la ressource dans une modale
* @param node Le node jstree
*/
function testRessource (node) {
const type = node.a_attr['data-type']
if (type === 'arbre') return
const url = node.a_attr.href
if (!url) {
console.error(new Error('node sans href'))
return
}
// ce n’est pas un arbre, on affiche
let longueur = 890 // au pif pour l’instant
let hauteur = 710
if (type === 'j3p') {
longueur = 890
hauteur = 710
}
if (type === 'em' || type === 'am') {
longueur = 820
hauteur = 600
}
// une modale
const modaleElt = edgModale({ width: '820', height: '600' })
// dans laquelle on ajoute un div
const contenudivD = addElement(modaleElt, 'div', { id: 'contenudivD' })
// contenant une iframe
const iframeOpts = {
id: 'iframe_affiche_ressource',
src: url,
width: longueur,
height: hauteur
}
addElement(contenudivD, 'iframe', iframeOpts)
}
/**
* Initialise l’arbre de gauche (source des éléments à dropper)
* @param {HTMLElement} menuElt
* @param {object} options
* @param {string} [options.sourceTreeRid] Sinon on prendra sourceTreeRid déclaré dans config.js
* @param next
*/
export function init (menuElt, options, next) {
// on construit le menu
if (typeof $ === 'undefined') return next(new Error('Pb de chargement de jquery'))
const rid = options.sourceTreeRid || sourceTreeRid
fetchRef(rid, function (error, arbre) {
if (error || !arbre || arbre.type !== 'arbre') {
if (error) console.error(error)
return next(new Error(`Le chargement de l’arbre des ressources j3p (${rid}) a échoué`))
}
// on a notre arbre, on peut continuer
const options = {
check_callback: function checkCallback (operation, node/*, parent, position, infos */) {
// operation can be 'create_node', 'rename_node', 'delete_node', 'move_node' or 'copy_node'
// node est le node manipulé
// parent est le node parent du node manipulé dans l’arbre source
// position est l’index de node parmis les enfants de parent (démarre à 1)
// infos peut être undefined, sinon il contient
// - dnd {boolean} (true)
// - is_foreign {boolean} devrait valoir true si on est hors de l’arbre, mais vaut tout le temps false, pénible
// - is_multi {boolean}
// - pos {string} ?
// - origin un truc interne jstree
// - ref Le node
// mais on a rien pour savoir où on est, pas de target
// on autorise la création de la branche 'Mes ressources'
if (operation === 'create_node' && node.text === 'Mes ressources') return true
// et on refuse tout le reste, pour empêcher l’utilisateur de modifier l’arbre source
return false
},
plugins: ['dnd', 'contextmenu'],
// dnd: {
// cf https://www.jstree.com/api/#/?q=$.jstree.defaults.dnd&f=$.jstree.defaults.dnd.always_copy
// a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is false
// always_copy: true
// },
contextmenu: {
items: function (node) {
// on veut pas de menu contextuel sur les arbres
if (node.a_attr['data-type'] === 'arbre') return
return {
// nom de propriété arbitraire
testRessource: {
separator_before: false,
separator_after: false,
label: 'Tester la ressource',
action: () => testRessource(node)
}
}
}
}
} // options
// ça c’est indispensable pour écouter les événements ici (sinon on entend rien
// car on aura pas la même instance de jquery que jstree)
options.jQuery = $
build(menuElt, arbre, options).then(($tree) => {
if (!$tree) return next(new Error('La construction de l’arbre des ressources j3p a échoué'))
$menuElt = $tree
addDropListener()
addPerso()
// ce truc marche pour récupérer l’info dans le drop (alors qu’il peut la lire directement ayant les mêmes arguments qu’ici)
// mais on arrive pas à récupérer cette info dans check_callback, faut trouver un moyen de l’attacher au node
// de toute façon check_callback n’est appelé que lorsque l’on survole l’arbre, plus une fois qu’on en est sorti
// faudrait donc probablement changer l’icone croix rouge ou coche verte ici…
$(window.document).on('dnd_move.vakata', function (event, jqObj) {
if (!jqObj.event.target) return console.error(Error('jqObj.event sans target'), jqObj)
// on attache un boolean aux datas, qu’on peut récupérer dans le drop
jqObj.data.isOnScene = jqObj.event.target.id === 'edgScene' || jqObj.event.target.closest('#edgScene') // target.id peut être un node jsplumb
})
next()
})
})
}
// on créé une variable pour l’élément jqueryfié
// (pour éviter de relancer la construction de $('#edgMenu') à chaque fois qu’on en aura besoin
let $menuElt
// on ajoute cette fct globale dès le require (et pas à l’exec d’init)
window.toggleExplicationsGraphique = function () {
dispatch(toggleExplicationsGraphique())
}