j3pLoad.js

/**
 * Ce module charge un graphe j3p v1 dans le dom, il est chargé dynamiquement par loader sur appel de la fct globale j3pLoad,
 * qui est le "player" j3p v1
 * {@tutorial chargement})
 *
 * Au départ, ce fichier charqeait tout (outils et moteur) avant d’instancier un unique Parcours dans le dom.
 * Petit à petit chaque section importe elle même les outils dont elle a besoin
 * et le code de ce fichier devrait se réduire.
 * @module
 */

import { addElement } from 'sesajs/dom'
import { setAppErrorHandler } from 'sesajs/error'
import log from 'sesajstools/utils/log'

import Parcours from 'src/legacy/core/Parcours'
import { j3pBaseUrl } from 'src/lib/core/constantes'
import loadJqueryDialog from 'src/lib/core/loadJqueryDialog'
import { loadSectionV1 } from 'src/lib/core/loadSection'

// faut ajouter notre css
import 'src/legacy/css/j3p.css'

/**
 * raccourci vers window
 * @type {Window}
 * @private
 */
const w = window
let j3pErrorConteneur

// pour les erreurs au chargement, quand on peut pas encore utiliser j3pShowError
const addPageError = (error) => {
  console.error(error)
  if (j3pErrorConteneur) {
    const content = error.message || error
    addElement(j3pErrorConteneur, 'p', { content })
  } else {
    // eslint-disable-next-line no-alert
    alert(error.message)
  }
}

/**
 * Transforme params.pe en un objet phrasesEtat (peKey => peValue)
 * @param params
 * @return {PlainObject} L’objet phrasesEtat (peKey => peValue)
 */
export function getPhrasesEtat (params) {
  const phrasesEtat = {}
  if (typeof params?.phrasesEtat === 'object') {
    Object.assign(phrasesEtat, params.phrasesEtat)
  } else if (Array.isArray(params?.pe)) {
    for (const pe of params.pe) {
      Object.assign(phrasesEtat, pe)
    }
  }
  return phrasesEtat
}

/**
 * Le chargeur principal du player
 * @param {HTMLElement} container
 * @param {Object} optionsChargement
 * @return {Promise<void, Error>}
 * @private
 */
function loadMain (container, optionsChargement) {
  try {
    if (typeof import.meta !== 'object') {
      // eslint-disable-next-line no-alert
      alert('Votre navigateur est trop ancien, les exercices interactifs ne fonctionneront pas correctement')
    }
  } catch (error) {
    console.error(error)
  }
  const sections = []

  // et on lance les chargements au fur et à mesure.
  // jquery-ui sera chargé par Parcours si une section n’a pas btedialogue: non dans ses paramètres
  // pour le moment on continue de le charger ici, car il faudrait être sûr que toutes les sections qui utilisent .dialog() le charge
  return loadJqueryDialog().then(() => {
    // on ajoute les sections
    const loadingPromises = []
    optionsChargement.graphe.forEach(([num, nomSection], i) => {
      if (!nomSection || typeof nomSection !== 'string') throw Error(`graphe invalide (nom de la section de l’élément ${i}`)
      // on ajoute à la première apparition son nom si c’est la 1re fois qu’il apparait
      if (nomSection.toLowerCase() !== 'fin' && !sections.includes(nomSection)) {
        sections.push(nomSection)
        loadingPromises.push(loadSectionV1(nomSection))
      }
    })
    return Promise.all(loadingPromises)
  }).then((sectionsExports) => {
    // sectionsExports est un array avec les exports des sections chargées, dans le même ordre
    for (const [index, name] of sections.entries()) {
      // on récupère ce qu’elle exporte
      const exp = sectionsExports[index]
      // pour les sections qui n’exporte qu’une seule fct, faut la mettre dans le prototype de Parcours
      if (typeof exp.default === 'function') {
        const nomFn = `Section${name}`
        Parcours.prototype[nomFn] = exp.default
        // on ajoute aussi les params, pour que Parcours puisse les récupérer
        Parcours.prototype[nomFn].parametres = exp.params?.parametres ?? []
        // et une éventuelle fct d’upgrade des params (pour compatibilité ascendante des graphes)
        if (typeof exp.upgradeParametres === 'function') Parcours.prototype[nomFn].upgradeParametres = exp.upgradeParametres

        // les pe
        Parcours.prototype[nomFn].phrasesEtat = getPhrasesEtat(exp.params)
      }
    }

    // et on peut instancier
    const parcours = new Parcours(container.id, 'Mep', optionsChargement)
    // on surcharge appErrorHandler pour planter sur les erreurs critiques
    setAppErrorHandler((error) => {
      console.error(error)
      if (error.critical) {
        // c'est une erreur bloquante => on arrête là
        // (ça va afficher dans MG "erreur interne, impossible de continuer")
        parcours.abort()
      }
    })
    w.j3p = parcours
    // log('fin du chargement, j3p est créé en global comme instance de Parcours', parcours)
    return parcours
  })
} // loadMain

// ************************************
// Les méthodes exportées de ce module
// ************************************

/**
 * Initialise des params
 * @param {object} options Doit contenir la propriété j3pBaseUrl (url absolue du site j3p),
 * @param {string} options.j3pBaseUrl (url absolue du site j3p pour y charger css & co)
 * @param {number|string} [options.logLevel]
 * @private
 */
function init (options) {
  if (!options) throw Error('Paramètres de chargement absents')
  if (options.logLevel !== undefined) log.setLogLevel(options.logLevel)
}

/**
 * Charge un graphe (v1 only) dans le conteneur indiqué
 * @param {string|HTMLElement} container
 * @param {J3pLoadOptions} options
 * @param {Array} options.graphe
 * @param {Object} [options.editgraphes] Un objet avec les positions des nœuds dans la représentation graphique du graphe, pour viewer
 * @param {number} [options.indexInitial] index du nœud par lequel commencer dans le graphe (démarre à 0)
 * @param {boolean} [options.isDebug=false] Passer true pour instancier Parcours en mode debug
 * @param {Object} [options.lastResultat] Un éventuel dernier résultat obtenu
 * @param {number|string} [options.logLevel] Niveau de log souhaité (0-4 ou debug|notice|warning|error|critical)
 * @param {function} [options.resultatCallback] Sera appelée avec le résultat
 * @param {string} [options.baseUrl] Passer ici l’url absolue du domaine où charger j3p (indispensable si la page courante n’y est pas)
 * @param {Language} [options.language] Une éventuelle langue autre que fra parmi ara|deu|eng|spa
 * @param {boolean} [options.isTest] Passer true pour le mode test (@todo à implémenter)
 * @param {function} [loadCallback] rappelée avec (error, parcours)
 * @returns {Promise|undefined} undefined si loadCallback a été fournie, Promise sinon
 */
function j3pLoad (container, options, loadCallback) {
  try {
    if (typeof container === 'string') container = document.getElementById(container)
    if (!container || typeof options !== 'object') throw Error('paramètres manquants, chargement impossible')
    if (options.lastResultat && !options.graphe) {
      options.graphe = options.lastResultat.contenu?.graphe
    }
    const { graphe } = options
    // on vérifie que le graphe est au moins un tableau de tableaux
    if (!Array.isArray(graphe) || !graphe.length || !graphe.every(e => Array.isArray(e))) throw Error('graphe invalide, chargement impossible')
    if (options.baseUrl) {
      init({ j3pBaseUrl: options.baseUrl, logLevel: options.logLevel })
    } else if (!j3pBaseUrl) {
      throw Error('Il faut appeler init avant de lancer le chargement ou passer l’option baseUrl à ce chargeur')
    }
    // on vide
    while (container.lastChild) container.removeChild(container.lastChild)
    // log('On va charger ce graphe', stringify(graphe), 'avec les options', stringify(options))

    const optionsChargement = {
      // on accepte les graphe avec un 1er elt vide ou pas, s’il y en a un on le vire
      graphe: graphe[0].length ? graphe : graphe.slice(1),
      baseUrl: j3pBaseUrl,
      isDebug: Boolean(options.isDebug)
    }
    // options facultatives
    if (typeof options.resultatCallback === 'function') optionsChargement.resultatCallback = options.resultatCallback
    if (typeof options.editgraphes === 'object') optionsChargement.editgraphes = options.editgraphes
    if (typeof options.lastResultat === 'object') optionsChargement.lastResultat = options.lastResultat
    if (typeof options.indexInitial === 'number') optionsChargement.indexInitial = options.indexInitial
    if (typeof options.isDebug === 'boolean') optionsChargement.isDebug = options.isDebug
    if (typeof options.language === 'string') {
      optionsChargement.language = options.language
    }
    // surcharge éventuelle d'après l'url
    const urlParams = new URLSearchParams(window.location.search)
    if (urlParams.has('language')) optionsChargement.language = urlParams.get('language')

    // on lance analyse du graphe et chargement des outils puis lancement
    // mais faut forcer cet id qui est en dur un peu partout dans le code j3p, on créé un div pour ça
    j3pErrorConteneur = addElement(container, 'div', { className: 'j3pErrors' })
    const j3pContainer = addElement(container, 'div', { id: 'Mepact' })

    const promise = loadMain(j3pContainer, optionsChargement)
    if (typeof loadCallback !== 'function') return promise

    // sinon on gère la callback passée en param
    const onSuccess = (parcours) => loadCallback(null, parcours)
    const onFailure = error => {
      addPageError(error)
      loadCallback(error)
    }
    promise
      .then(onSuccess, onFailure)
      .catch(addPageError) // au cas où loadCallback plante
  } catch (error) {
    return (typeof loadCallback === 'function') ? loadCallback(error) : Promise.reject(error)
  }
} // j3pLoad

export default j3pLoad