modules/loaders.js

/* global jQuery */
import loadjs from 'loadjs'
import { context } from './context.js'
import { UserFriendlyError } from './messages.js'
import { clavierLongueur } from './interactif/claviers/longueur_ANCIEN.js'
import { clavierTrigo } from './interactif/claviers/trigo.js'
import { clavierCollege } from './interactif/claviers/college.js'
import { clavierLycee } from './interactif/claviers/lycee.js'
import { clavierConfiguration } from './interactif/claviers/claviersUnites.js'
import { clavierCollege6eme } from './interactif/claviers/college6eme.js'
/**
 * Nos applis prédéterminées avec la liste des fichiers à charger
 * @type {Object}
 */
const apps = {
  giac: '/assets/externalJs/giacsimple.js',
  mathgraph: 'https://www.mathgraph32.org/js/mtgLoad/mtgLoad.min.js',
  prism: ['/assets/externalJs/prism.js', '/assets/externalJs/prism.css'],
  scratchblocks: 'assets/externalJs/scratchblocks-v3.5-min.js',
  slick: ['/assets/externalJs/semantic-ui/semantic.min.css', '/assets/externalJs/semantic-ui/semantic.min.js', '/assets/externalJs/semantic-ui/components/state.min.js']
}

/**
 * Charge une appli listée dans apps (pour mutualiser l'appel de loadjs)
 * @private
 * @param {string} name
 * @return {Promise<undefined, Error>} promesse de chargement
 */
async function load (name) {
  // on est dans une fct async, si l'une de ces deux lignes plantent ça va retourner une promesse rejetée avec l'erreur
  if (!apps[name]) throw UserFriendlyError(`application ${name} inconnue`)
  // cf https://github.com/muicss/loadjs
  try {
    if (!loadjs.isDefined(name)) await loadjs(apps[name], name, { returnPromise: true })
  } catch (error) {
    console.error(error)
    throw new UserFriendlyError(`Le chargement de ${name} a échoué`)
  }
  // loadjs.ready veut une callback, on emballe ça dans une promesse
  return new Promise((resolve, reject) => {
    loadjs.ready(name, {
      success: resolve,
      // si le chargement précédent a réussi on voit pas trop comment on pourrait arriver là, mais ça reste plus prudent de gérer l'erreur éventuelle
      error: () => reject(new UserFriendlyError(`Le chargement de ${name} a échoué`))
    })
  })
}

/**
 * Attend que xcas soit chargé (max 60s), car giacsimple lance le chargement du wasm|js suivant les cas
 * @return {Promise<undefined,Error>} rejette en cas de timeout
 */
function waitForGiac () {
  /* global Module */
  if (typeof Module !== 'object' || typeof Module.ready !== 'boolean') return Promise.reject(Error('Le loader giac n’a pas été correctement appelé'))
  const timeout = 60 // en s
  const tsStart = Date.now()
  return new Promise((resolve, reject) => {
    const monInterval = setInterval(() => {
      const delay = Math.round((Date.now() - tsStart) / 1000)
      if (Module.ready === true) {
        clearInterval(monInterval)
        resolve()
      } else if (delay > timeout) {
        clearInterval(monInterval)
        reject(UserFriendlyError(`xcas n’est toujours pas chargé après ${delay}s`))
      }
    }, 500)
  })
}

/**
 * Charge giac
 * @return {Promise} qui peut échouer…
 */
export async function loadGiac () {
  await load('giac')
  // attention, le load précédent résoud la promesse lorsque giacsimple est chargé,
  // mais lui va charger le webAssembly ou js ensuite, d'où le besoin de waitForGiac
  await waitForGiac()
}

/**
 * Charge une animation iep dans un élément
 * @param {HTMLElement} elt
 * @param {string} xml Le script xml de l'animation ou son url absolue
 * @return {Promise<iepApp>} L'appli iep
 */
export async function loadIep (elt, xml) {
  try {
    const { default: iepLoadPromise } = await import('instrumenpoche')
    const iepApp = await iepLoadPromise(elt, xml, { zoom: true, autostart: false })
    return iepApp
  } catch (error) {
    console.error(error)
    throw UserFriendlyError('Le chargement d’instrumenpoche a échoué')
  }
}

/**
 * Charge mathgraph dans l'élément fourni
 * @param {HTMLElement} elt
 * @param {Object} svgOptions Options du svg créé (taille et id, cf {@link https://www.mathgraph32.org/documentation/loading/global.html#mtgLoad})
 * @param {Object} mtgOptions Options pour l'appli (boutons, menus, etc., cf {@link https://www.mathgraph32.org/documentation/loading/global.html#MtgOptions}
 * @return {Promise<MtgApp>} l'appli mathgraph {@link https://www.mathgraph32.org/documentation/loading/MtgApp.html}
 */
export async function loadMG32 (elt, svgOptions, mtgOptions) {
  try {
    if (typeof window.mtgLoad !== 'function') await load('mathgraph')
    if (typeof window.mtgLoad !== 'function') throw Error('mtgLoad n’existe pas')
    // cf https://www.mathgraph32.org/documentation/loading/global.html#mtgLoad
    // la syntaxe qui retourne une promesse fonctionne avec un import seulement (il faudrait mettre mathgraph dans nos dépendances et l'importer)
    // avec le chargement du js via un tag script il faut fournir une callback
    return new Promise((resolve, reject) => {
      window.mtgLoad(elt, svgOptions, mtgOptions, (error, mtgApp) => {
        if (error) return reject(error)
        if (mtgApp) return resolve(mtgApp)
        reject(Error('mtgLoad ne retourne ni erreur ni application'))
      })
    })
  } catch (error) {
    console.error(error)
    throw new UserFriendlyError('Erreur de chargement de Mathgraph')
  }
}

/**
 * Charge prism
 * @return {Promise<undefined>}
 */
export function loadPrism () {
  return load('prism')
}

/**
 * Charge scratchblocks
 * @return {Promise} qui peut échouer…
 */
export function loadScratchblocks () {
  return load('scratchblocks')
}

/**
 * Charge MathLive et personnalise les réglages
 * MathLive est chargé dès qu'un tag math-field est créé
 */
export async function loadMathLive () {
  const champs = document.getElementsByTagName('math-field')
  if (champs.length > 0) {
    await import('mathlive')
    for (const mf of champs) {
      mf.setOptions(clavierCollege)

      // Evite les problèmes de positionnement du clavier mathématique dans les iframes
      if (context.vue === 'exMoodle') {
        const events = ['focus', 'input']
        events.forEach(e => {
          mf.addEventListener(e, () => {
            setTimeout(() => { // Nécessaire pour que le calcul soit effectué après la mise à jour graphique
              const position = jQuery(mf).offset().top + jQuery(mf).outerHeight() + 'px'
              document.body.style.setProperty('--keyboard-position', position)
            })
          })
        })
      }

      if ((('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0))) {
        // Sur les écrans tactiles, on met le clavier au focus (qui des écrans tactiles avec claviers externes ?)
        mf.setOptions({
          virtualKeyboardMode: 'onfocus'
        })
      }
      const listeParamClavier = mf.classList
      if (mf.className.indexOf('nite') !== -1 || mf.className.indexOf('nité') !== -1) {
        let jj = 0
        while (listeParamClavier[jj].indexOf('nites') === -1 & listeParamClavier[jj].indexOf('nités') === -1) { jj++ }
        const contenuUnites = listeParamClavier[jj].split('[')[1].split(']')[0].split(',')
        mf.setOptions(clavierConfiguration(contenuUnites))
      }
      if (mf.classList.contains('lycee')) {
        mf.setOptions(clavierLycee)
      }
      if (mf.classList.contains('college6eme')) {
        mf.setOptions(clavierCollege6eme)
      }
      if (mf.classList.contains('longueur')) {
        mf.setOptions(clavierLongueur)
      }
      if (mf.classList.contains('grecTrigo')) {
        mf.setOptions(clavierTrigo)
      }
      let style = 'font-size: 20px;'

      if (mf.classList.contains('inline')) {
        if (mf.classList.contains('nospacebefore')) {
          style += 'margin-left:5px;'
        } else {
          style += 'margin-left: 25px;'
        }
        style += ' display: inline-block; vertical-align: middle; padding-left: 5px; padding-right: 5px; border-radius: 4px; border: 1px solid rgba(0, 0, 0, .3);  '
        if (!mf.classList.contains('largeur10') && !mf.classList.contains('largeur25') && !mf.classList.contains('largeur50') && !mf.classList.contains('largeur75')) {
          style += ' width: 25%;'
        }
      } else {
        style += ' margin-top: 10px; padding: 10px; border: 1px solid rgba(0, 0, 0, .3); border-radius: 8px; box-shadow: 0 0 8px rgba(0, 0, 0, .2);'
      }
      if (mf.classList.contains('largeur10')) {
        style += ' width: 10%;'
      }
      if (mf.classList.contains('largeur25')) {
        style += ' width: 25%;'
      }
      if (mf.classList.contains('largeur50')) {
        style += ' width: 50%;'
      }
      if (mf.classList.contains('largeur75')) {
        style += ' width: 75%;'
      }
      mf.style = style
    }
  }
  // On envoie la hauteur de l'iFrame après le chargement des champs MathLive
  if (context.vue === 'exMoodle') {
    const hauteurExercice = window.document.querySelector('section').scrollHeight
    window.parent.postMessage({ hauteurExercice, iMoodle: parseInt(new URLSearchParams(window.location.search).get('iMoodle')) }, '*')
    const domExerciceInteractifReady = new window.Event('domExerciceInteractifReady', { bubbles: true })
    document.dispatchEvent(domExerciceInteractifReady)
  }
}