editGraphe/menu.js

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())
}