legacy/core/functions.js

/**
 * Regroupe la plupart des fonctions génériques j3pXxx (ancien J3Poutils.js)
 * @module legacy/core/functions
 */
import $ from 'jquery'

import { loadJs, isDomElement } from 'sesajs/dom'
import { notify, UserError } from 'sesajs/error'
import { fetchJson, fetchText, fetchXml } from 'sesajs/fetch'
import { pgcd } from 'sesajs/number'
import { captureIntervalleFermeDecimaux, captureIntervalleFermeEntiers } from 'sesajs/regexp'
import { isValidId } from 'sesajs/string'

import { j3pCreeSegment, j3pCreeSVG } from 'src/legacy/core/functionsSvg'
import { j3pBaseUrl } from 'src/lib/core/constantes'
import { getJ3pConteneur } from 'src/lib/core/domHelpers'
import { getMqValue, j3pAffiche, mqAjoute, mqCommandes } from 'src/lib/mathquill/functions'
import { getCleanStyle, getCssDimension, setStyle } from 'sesajs/css'
import { hasProp, isPlainObject } from 'sesajs/object'
import { convertRestriction } from 'src/lib/utils/regexp'

/**
 * Le nombre utilisé pour les arrondis (considérer qu’un float est tel int, ou qu’on a zéro), 1e-10
 * @type {number}
 * @private
 */
const epsilon = 1e-10
const lettersList = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ')

/**
 * Retourne un tableau [x, y] avec la taille de la fenêtre courante (utilise jQuery)
 * @return {Number[]}
 */
export function j3pDimfenetre () {
  return [$(window).width(), $(window).height()]
}

/**
 * Retourne les paramètres a,b,c de l’équation de la droite (ax + by + c = 0) passant par les deux points
 * @param {{x: number, y: number}} p1
 * @param {{x: number, y: number}} p2
 * @return {{a: number, b: number, c: number}}
 */
export function j3pGetParamsDroite (p1, p2) {
  return {
    a: p1.y - p2.y,
    b: p2.x - p1.x,
    c: (p2.y - p1.y) * p1.x - (p2.x - p1.x) * p1.y
  }
}

/**
 * Retourne la distance entre deux points. Accepte deux points, deux tableaux de 2 number ou 4 number
 * @param {Point} pt1
 * @param {Point} pt2
 * @returns {number}
 */
export function j3pDistance () {
  const args = []
  for (const arg of arguments) {
    if (typeof arg === 'number') {
      args.push(arg)
    } else if (Array.isArray(arg) && arg.length === 2) {
      for (const n of arg) {
        if (typeof n === 'number') args.push(n)
        else if (typeof n === 'string') args.push(Number(n))
        else throw Error(`Argument invalide : ${n} (${typeof n})`)
      }
    } else if (hasProp(arg, 'x') && hasProp(arg, 'y')) {
      args.push(arg.x)
      args.push(arg.y)
    } else {
      throw Error(`Argument invalide : ${arg}`)
    }
  }
  if (args.length === 4) {
    const [x1, y1, x2, y2] = args
    return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
  }
  console.error('arguments invalides, on reçoit', arguments, 'qui devient', args)
  throw Error('arguments invalides, il faut passer deux points ou 4 coordonnées')
}

/**
 * Calcule le point d’intersection de deux droites
 * @param {number[]} d1 coefs abc de la 1re droite
 * @param {number[]} d2 coefs abc de la 2e droite
 * @returns {number[]|undefined} Les coordonnées du point (undefined si d1//d2)
 */
export function j3pIntersection (tab1, tab2) {
  const det = tab1[0] * tab2[1] - tab1[1] * tab2[0]
  if (det === 0) return
  return [(tab1[2] * tab2[1] - tab1[1] * tab2[2]) / det, (tab1[0] * tab2[2] - tab2[0] * tab1[2]) / det]
}

export function j3pPointDansSegment (point, ext1, ext2) {
  const d1 = j3pDistance(point, ext1)
  const d2 = j3pDistance(point, ext2)
  const d = j3pDistance(ext1, ext2)
  return Math.abs(d - d1 - d2) < 0.000000001
}

/**
 * @typedef Droite
 * @property {number} a
 * @property {number} b
 * @property {number} c
 */

/**
 * @typedef Point
 * @property {number} x
 * @property {number} y
 */

/**
 * Retourne le point d’intersection de deux droites (undefined si y’en a pas)
 * @param {Droite} d1
 * @param {Droite} d2
 * @returns {Point}
 */
export function j3pIntersectionDroites (d1, d2) {
  const { a, b, c } = d1
  const { a: a2, b: b2, c: c2 } = d2
  const det = a * b2 - b * a2
  if (det === 0) return
  return { x: (c * b2 - b * c2) / det, y: (a * c2 - a2 * c) / det }
}

/**
 * Ajoute une case à cocher avec un comportement normal de checkbox
 * Ex: ```
 * var input = j3pAjouteCaseCoche(elt, 'mon label')
 * // pour désactiver l’input (correction par ex)
 * j3pDesactive(input)
 * // pour barrer le label
 * j3pBarre(input.label)
 * // pour changer la couleur du label (par ex)
 * input.label.style.color = '#f00'
 * ```
 * @param {HTMLElement|string} conteneur
 * @param {string|object} [options] Si fourni en string, idem options.label
 * @param {string} [options.label] Permet d’insérer le label qui devient cliquable (comme la case). On peut mettre du mathquill dans le label (appel de j3pAffiche dans ce cas)
 * @param {string} [options.id] Permet de donner un id à la zone (c’est à faire quand on a du mathquill, pas besoin autrement)
 * @return {HTMLElement} L’élément &lt;input type="checkbox"> inséré, avec une propriété label ajouté contenant l’élément label
 */
export function j3pAjouteCaseCoche (conteneur, options) {
  let label = ''
  if (typeof options === 'string') label = options
  if (typeof options !== 'object') options = {}
  if (typeof options.label === 'string') label = options.label
  const props = {
    type: 'checkbox',
    style: {
      marginRight: '15px',
      marginLeft: '15px',
      zIndex: 0
    }
  }
  // si l’id existe déjà ça va râler en console et ne pas l’ajouter ici
  if (options.id && typeof options.id === 'string' && !j3pElement(options.id, false)) props.id = options.id
  const labelElt = j3pAddElt(conteneur, 'label')
  const input = j3pAddElt(labelElt, 'input', null, props)
  // on ajoute une prop label à l’objet input pour permettre plus simplement à l’appelant de récupérer le label
  // (s’il veut ensuite le barrer à la correction par ex)
  input.label = labelElt
  // Si on a du mathquill, on utilise j3pAffiche
  if (label.indexOf('$') !== -1) {
    j3pAffiche(labelElt, props.id ? props.id + 'label' : '', label)
  } else {
    j3pAddTxt(labelElt, label)
  }
  return input
}// j3pAjouteCaseCoche

/**
 * Affiche une modale
 * @param {Object} options
 * @param {string} options.titre
 * @param {string} options.contenu
 * @param {HTMLElement|string} [options.divparent=body]
 * @param {Function} [options.onClose]
 * @param {boolean} [options.closeOnMask=false] passer true pour fermer la modale avec un clic sur le masque (en plus de la croix)
 * @returns {HTMLElement} le div de cette modale (#modale)
 */
export function j3pModale (options) {
  function close () {
    $(masque, modale).fadeOut(function () {
      j3pDetruit(masque, modale)
      if (typeof options.onClose === 'function') {
        options.onClose()
      }
    })
  }

  let container = options.divparent || document.body
  if (typeof container === 'string') container = j3pElement(container)
  if (!j3pIsHtmlElement(container, true)) return
  // Y’a eu une époque où on le prenait en param mais
  // - tous passaient 'modale'
  // - on ne peut pas gérer plus d’une modale
  // => on l’impose
  const id = 'modale'
  // on vérifie d’abord qu’il n’y a pas de modale déjà ouverte
  let modale = document.getElementById(id)
  if (modale) {
    console.error(Error('Il y avait déjà une modale ouverte, on la vire d’abord'))
    j3pDetruit(modale, 'j3pmasque')
  }
  modale = j3pAddElt(container, 'div', '', {
    id,
    className: 'modale',
    style: {
      display: 'none'
    }
  })
  // croix pour fermer
  const croix = j3pAddElt(modale, 'div', '', {
    className: 'croix'
  })
  // titre
  j3pAddElt(modale, 'div', options.titre, {
    id: id + 'titre',
    style: {
      fontSize: '24px',
      marginTop: '-15px',
      borderBottom: '1px solid #000'
    }
  })
  // contenu
  j3pAddElt(modale, 'div', options.contenu, {
    id: id + 'contenu',
    style: {
      fontSize: '18px',
      whiteSpace: 'pre',
      marginTop: '10px'
    }
  })
  // masque
  const masque = j3pAddElt(container, 'div', '', {
    id: 'j3pmasque',
    className: 'masqueModale',
    style: {
      display: 'none'
    }
  })
  $(masque).fadeIn(500)
  $(modale).fadeIn()
  croix.addEventListener('click', close)
  const dim = j3pDimfenetre()
  modale.style.left = (dim[0] - modale.offsetWidth) / 2 + 'px'
  modale.style.top = (dim[1] - modale.offsetHeight) / 2 + 'px'
  modale.masque = masque
  return modale
} // j3pModale

/**
 * Retire du dom les éléments html dont les ids sont passés en argument
 * Si un id n’existe pas dans le dom, ne dit rien (et ne fait rien), pas besoin de tester l’existence avant
 * @param {...(HTMLElement|string)} elt Un élément à détruire (ou son id), on peut passer autant d’arguments que l’on veut (pour détruire plusieurs elts en un seul appel)
 */
export function j3pDetruit (...elts) {
  for (let elt of elts) {
    if (typeof elt === 'string') elt = j3pElement(elt, null)
    if (!elt) return // on accepte qu’on nous passe des id qui n’existent pas encore
    if (isDomElement(elt, { warnIfNotElt: true })) {
      if (elt.parentNode) {
        elt.parentNode.removeChild(elt)
      } else {
        j3pEmpty(elt)
        console.error(Error('L’élément est bien un HTMLElement mais il n’a pas de parentNode, on le vide sans le détruire #' + elt.id), elt)
      }
    }
  }
}

/**
 * toggle display none sur les éléments passés en arguments
 * @param {HTMLElement|string} ...elts
 */
export function j3pBasculeAffichage (...elts) {
  for (const elt of elts) {
    const targetElement = typeof elt === 'string' ? j3pElement(elt) : elt
    if (targetElement.style.display === 'none') {
      targetElement.style.display = ''
    } else {
      targetElement.style.display = 'none'
    }
  }
}

/**
 * Retourne true si la différence entre terme1 et terme 2 est inférieure à erreur
 * @param {number} terme1
 * @param {number} terme2
 * @param {number} erreur
 * @return {boolean}
 */
export function j3pEnvironEgal (terme1, terme2, erreur) {
  return Math.abs(terme2 - terme1) < erreur
}

/**
 * Si id contient inputmq on retourne la string LaTeX de l’input, sinon l’attribut value (ou une chaine vide s’il n’existe pas)
 * Renvoie toujours une string, même si id n’existe pas
 * @param {HTMLElement|string} elt L’élément ou son id
 * @return {string}
 */
export function j3pValeurde (elt) {
  if (typeof elt === 'string') elt = j3pElement(elt)
  if (!j3pIsHtmlElement(elt, true)) return ''
  if (elt.id?.includes('inputmq')) {
    try {
      return getMqValue(elt)
    } catch (error) {
      console.error(error)
      j3pShowError('Expression invalide ou trop complexe.')
      return ''
    }
  }
  if (typeof elt.value !== 'string') {
    console.error(Error('Élément invalide pour récupérer une saisie'), elt)
    return ''
  }
  return elt.value.trim()
}

/**
 * Retourne la norme du vecteur (x, y)
 * @param {number} x
 * @param {number} y
 * @return {number}
 */
export function j3pNormeVecteur (x, y) {
  return Math.sqrt(x * x + y * y)
}

// Doc
export function j3pScalaire (xu, yu, xv, yv) {
  return (xu * xv + yu * yv)
}

// Doc
export function j3pAngleVecteurs (xu, yu, xv, yv) {
  let signe
  if ((xu * yv - yu * xv) < 0) {
    signe = 1
  } else {
    signe = -1
  }
  return (signe * Math.acos(j3pScalaire(xu, yu, xv, yv) / (j3pNormeVecteur(xu, yu) * j3pNormeVecteur(xv, yv)))) * (180 / Math.PI)
}

// Doc
export function j3pImageRotation (xO, yO, angle, xM, yM) {
  const resultat = []
  angle = (angle / 180) * Math.PI
  resultat[0] = xO + (xM - xO) * Math.cos(angle) - (yM - yO) * Math.sin(angle)
  resultat[1] = yO + (xM - xO) * Math.sin(angle) + (yM - yO) * Math.cos(angle)
  return resultat
}

export function j3pAjouteArea (zone, objet) {
  const cible = j3pElement(zone)
  const area = document.createElement('textarea')
  area.setAttribute('id', objet.id)
  area.setAttribute('cols', objet.cols)
  area.setAttribute('rows', objet.rows)

  if (typeof objet.readonly !== 'undefined') {
    if (objet.readonly) {
      area.setAttribute('readonly', 'readonly')
    }
  }

  let ch = 'padding:5px;'
  if (typeof objet.taillepolice !== 'undefined') {
    ch += 'font-size:' + objet.taillepolice + 'pt;'
  }
  if (typeof objet.police !== 'undefined') {
    ch += 'font-family:' + objet.police + ';'
  }
  if (typeof objet.couleurpolice !== 'undefined') {
    ch += 'color:' + objet.couleurpolice + ';'
  }
  area.setAttribute('style', ch)
  cible.appendChild(area)
}

/**
 * @typedef j3pObjetBulle
 * @property {string} [contenu] si présent sera préfixé par "j3pBULLE"
 * @property {string} [texte] doit être présent si contenu est absent
 * @property {number} [longueur=200] en pixels
 */

/**
 * @typedef j3pDivOptions
 * @property {string} [id] Id du div à créer
 * @property {string} [contenu] Le contenu éventuel du div, html autorisé, attention à passer la propriété param si y’a du £ dans ce contenu
 * @property {number[]} [coord] tableau [x, y] pour positionner le div
 * @property {object|string} [style] préférer la notation object, plus lisible
 * @property {object} [param] La liste des chaînes de remplacement (dans contenu, les £prop£ seront remplacés par param[prop])
 */

/**
 * Ajoute un div, 2 types d’appel possibles
 * j3pDiv(conteneurId, options)
 * j3pDiv(conteneurId, id, contenu, coord, style, bulle)
 * Ex : j3pDiv(this.zones.MG, "exemple", "un texte", [100,435], j3p.styles.grand.enonce)
 * revient à
 * j3pDiv(this.zones.MG, {id: 'exemple', contenu: 'un texte', coord: [100,435], style: j3p.styles.grand.enonce})
 * @param {HTMLElement|string} conteneur
 * @param {j3pDivOptions} options
 * @returns {HTMLElement}
 */
export function j3pDiv (conteneur, options) {
  let id, contenu, coord, style, param
  if (arguments.length > 2 || typeof options === 'string') {
    id = arguments[1]
    contenu = arguments[2]
    // attention, on a aussi des appels sans coord !
    if (Array.isArray(arguments[3])) {
      coord = arguments[3]
      style = arguments[4]
    } else if (arguments.length === 4 && typeof arguments[3] === 'object') {
      style = arguments[3]
    }
  } else {
    if (!options) options = {}
    id = options.id
    contenu = options.contenu
    coord = options.coord
    style = options.style
    param = options.param
  }

  // on traite les £ avec j3pRemplace
  if (typeof contenu === 'string' && param) {
    while (contenu.indexOf('£') !== -1) {
      const indice1 = contenu.indexOf('£')
      const indice2 = contenu.indexOf('£', indice1 + 1)
      const prop = contenu.substring(indice1 + 1, indice2)
      if (hasProp(param, prop)) contenu = j3pRemplace(contenu, indice1, indice2, param[prop])
      else console.error(Error('Paramètre manquant : ' + prop), 'dans les params fournis : ', param)
    }
  }
  // init des props du div avec le style s’il existe
  const props = (style) ? _clonePropsCleanStyle({ style }) : {}

  // on impose top & left si on passe coord
  if (coord) {
    if (!props.style) props.style = {}
    // on surcharge (j3pAjouteDiv vérifie et ajoute l’unité px si besoin), sauf si on nous a demandé une position via le style
    if (!props.style.position) props.style.position = 'absolute'
    props.style.left = coord[0]
    props.style.top = coord[1]
  }
  /* on avait ajouté ça pour corriger des pbs de positionnement, mais c'était une mauvaise idée, ça en crée d’autres
     else {
     if (!props.style) props.style = {}
     if (!props.style.position) {
       props.style.position = 'relative'
       props.style.left = '0'
       props.style.top = '0'
     }
   } */

  // et on retourne le div avec j3pAjouteDiv (qui pourra décaler top & left suivant le contexte)
  return j3pAjouteDiv(conteneur, id, contenu, props)
}

/**
 * Ajoute un div dans le conteneur.
 *
 * Important : si props.style contient un positionnement ça va décaler top & left en fonction des zones présentes.
 *
 * Sinon, il vaut mieux utiliser directement `j3pAddElt(conteneur, 'div', contenu, props)`
 * @param {HTMLElement|string} conteneur
 * @param {string} [divId] Passer '' (ou n’importe quelle valeur falsy) pour ne pas mettre d’id sur le div créé
 * @param {string} [content] Contenu éventuel, html autorisé mais HTMLElement ou HTMLElement[] ou NodeList préférable
 * @param {object} [props] Attributs éventuel du div (ajoutera l’unité px si style.top ou style.left sont des nombres sans unités)
 * @return {HTMLDivElement}
 */
export function j3pAjouteDiv (conteneur, divId, content, props) {
  if (typeof conteneur === 'string') conteneur = j3pElement(conteneur)
  if (!j3pIsHtmlElement(conteneur, true)) return
  if (!props) props = {}
  if (divId) props.id = divId

  const isMgOrMdOrBody = conteneur.id === 'body' || /M[GD]$/.test(conteneur.id)

  // check style et clone au passage
  props = _clonePropsCleanStyle(props)
  // raccourci d’écriture
  const st = props.style

  // décalage éventuel des positions
  let decal
  if (st && st.position) {
    // top & left obligatoires si c’est absolute
    if (hasProp(st, 'top') && hasProp(st, 'left')) {
      // ça va râler en console et affecter 0 si foireux
      // on veut des number pour décaler ensuite
      st.top = _cssToNb(st.top)
      st.left = _cssToNb(st.left)
      if (isMgOrMdOrBody) {
        decal = j3pC()
        st.left += decal.x
        st.top += decal.y
      }
      // j3pAddElt ajoutera l’unité px
    } else if (st.position === 'absolute') {
      // on ne râle qu’une seule fois s’il manque un truc
      let msg = ''
      if (!hasProp(st, 'top') && !hasProp(st, 'bottom')) {
        msg = 'Avec un positionnement absolu j3pAjouteDiv veut top ou bottom (top imposé à 0)'
        st.top = 0
      }
      if (!hasProp(st, 'left') && !hasProp(st, 'rigth')) {
        st.left = 0
        msg += msg
          ? ', mais également left ou right (left imposé à 0)'
          : 'Avec un positionnement absolu j3pAjouteDiv veut left ou right (left imposé à 0)'
      }
      if (msg) console.error(Error(msg))
    }
  }
  // fin décalage des positions

  try {
    return j3pAddElt(conteneur, 'div', content, props)
  } catch (error) {
    console.error(error)
    throw Error('un problème interne est survenu')
  }
} // j3pAjouteDiv

/**
 * Retourne une fonction qui exécutera fn à condition de ne pas avoir été appelé moins de minDelay ms plus tôt
 * @param {function} fn
 * @param {Object} [options]
 * @param {number} [options.minDelay=1000] en ms, passer -1 pour que fn ne soit appelée qu’une seule fois
 * @param {string} [options.message=''] Le message à afficher en cas de clic trop rapide après le précédent
 * @param {number} [options.vanishAfter=0] Durée en s avant d'éffacer le message en cas de clic trop rapide
 * @return {function}
 */
export function j3pGetDebouncedFunction (fn, { minDelay = 1000, message = '', vanishAfter = 0 } = {}) {
  if (minDelay === 0) {
    console.error(Error('Inutile d’appeler j3pGetDebouncedFunction avec un délai de 0'))
    return fn
  }
  let lastClick = 0
  let lastError = 0
  return function debounced () {
    const now = Date.now()
    if ((minDelay < 0 && lastClick === 0) || (minDelay > 0 && now - lastClick > minDelay)) {
      lastClick = now
      fn()
    } else if (message) {
      // on évite d'afficher le message N fois
      if (now - lastError > vanishAfter * 1000) {
        j3pShowError(message, { vanishAfter })
        lastError = now
      }
    }
  }
}

/**
 * Ajoute un &lt;button> dans container
 * L’ancienne syntaxe `j3pAjouteBouton (container, id, className, value, onClick)` est toujours acceptée
 * Attention si clickListener plante l'erreur sera affichée à l'écran (mettre un try/catch dedans pour gérer soi-même l'erreur)
 * @param {HTMLElement|string} container l’élément ou son id
 * @param {function} clickListener fonction qui sera appelée au clic (avec un this qui sera le HTMLElement bouton, event click en argument)
 * @param {Object} [props] Propriétés du &lt;button> que l’on va créer (liste documentée ici non exhaustive {@link https://developer.mozilla.org/fr/docs/Web/HTML/Element/button})
 * @param {string} [props.id] id du bouton à créer
 * @param {string} [props.value] Texte du bouton (sinon utilisez l’élément retourné pour faire ensuite du j3pAddElt dedans)
 * @param {string} [props.className] classe css
 * @param {Object} [props.style] des styles éventuels
 * @param {Object} [options]
 * @param {string} [options.message]
 * @param {boolean} [options.isButton=false] Passer true pour créer un &lt;button> plutôt qu’un &lt;input type="button"> (indispensable si vous voulez une image ou d’autres éléments html dedans)
 * @param {number} [options.minDelay=0] Un délai pour interdire deux appels de listener dans cet intervalle (en ms). Passer 0 pour désactiver ce debounce, et -1 pour être sûr que le listener ne sera appelé qu’une seule fois
 * @param {string} [options.debounceMessage=''] Le message a afficher si on re-clique sur le bouton avant minDelay ms
 * @param {number} [options.vanishAfter=0] Une durée (en s) d'affichage de l'éventuel debounceMessage
 * @returns {HTMLButtonElement|HTMLInputElement} le bouton
 */
export function j3pAjouteBouton (container, clickListener, props = {}, options = {}) {
  if (arguments.length === 5 || typeof clickListener === 'string' || typeof props === 'string') {
    // ancienne syntaxe j3pAjouteBouton (container, id, className, value, onClick)
    const className = arguments[2]
    // attention, arguments est la liste des symboles, pas des valeurs
    // c’est comme si on avait arguments = [container, clickListener, props, options]
    // donc affecter props affecte arguments[2]
    props = {}
    // facultatif, arguments[1] (aka clickListener) est id
    if (clickListener && typeof clickListener === 'string') props.id = clickListener
    // arguments[2] ne contient plus l’argument initial, c’est maintenant notre objet props
    // mais className contient ce qui a été initialement passé
    if (className && typeof className === 'string') props.className = className
    // facultatif, arguments[3] (aka options) est value
    if (options && typeof options === 'string') props.value = options
    options = {}
    // facultatif, on accepte de créer un bouton sans listener, ce sera à l’appelant de l’ajouter sur ce qu’on retourne
    // arguments[4] est clickListener
    if (arguments[4] && (typeof arguments[4] === 'function' || typeof arguments[4] === 'string')) {
      clickListener = arguments[4]
    }
  } else if (typeof props !== 'object') {
    props = {}
  }
  if (typeof options !== 'object') {
    console.error(Error('options invalides'), options)
    options = {}
  }
  if (typeof container === 'string') container = j3pElement(container)
  if (!j3pIsHtmlElement(container, true)) return

  // on peut y aller, tout le monde a le bon type
  const { isButton = false, minDelay = 0, debounceMessage = '', vanishAfter = 0 } = options
  const tag = isButton ? 'button' : 'input'
  if (!isButton) props.type = 'button'
  const elt = j3pAddElt(container, tag, '', props)
  if (clickListener) {
    switch (typeof clickListener) {
      case 'function':
        if (minDelay > 0) {
          clickListener = j3pGetDebouncedFunction(clickListener, { minDelay, message: debounceMessage, vanishAfter })
        }
        elt.addEventListener('click', () => {
          // faut un try/catch car l'erreur ne serait pas catché par le moteur (c'est un listener)
          try {
            clickListener()
          } catch (error) {
            j3pShowError(error)
          }
        })
        break
      case 'string':
        console.warn('j3pAjouteBouton appelé avec un attribut onClick en string, il faudrait plutôt passer une fonction')
        elt.setAttribute('onclick', clickListener)
        break
      default:
        console.error(Error('argument clickListener invalide (il faut passer une fonction, ou éventuellement une string même si c’est déconseillé)'), clickListener)
    }
  }
  return elt
} // j3pAjouteBouton

/**
 * @deprecated Utiliser plutôt les fonctions {@link j3pGetRandomBool}, {@link j3pGetRandomInt}, {@link j3pGetRandomFixed} ou {@link j3pGetRandomFloat}
 * @param {string|number[]} ch L’intervalle sous la forme [1;3] ou \{1;3;8}
 * @return {string|number|*}
 */
export function j3pRandom (ch) {
  // on traite d’abord le tableau de number (on vérifie pas le type, ça marche pour string|number mais plantera si c’est pas du numberLike)
  if (Array.isArray(ch) && ch.length === 2 && typeof ch[0] === 'number' && typeof ch[1] === 'number') {
    console.warn('j3pRandom est obsolète, remplacer j3pRandom(tab) par j3pGetRandomInt(tab[0], tab[1]) (avec en prime l’ordre indifférent des bornes)')
    return j3pGetRandomInt(ch[0], ch[1])
  }
  if (typeof ch !== 'string') return console.error(Error('arguments invalides'), arguments)
  console.warn('Il faudrait éviter l’appel de j3pRandom inutilement très gourmand, utiliser à la place j3pGetRandomInt, ou j3pGetRandomElt pour tirer parmi une liste fournie')
  // 'ch = [2;9]' Renvoie un entier appartenant à l’intervalle
  const fourchette = ch

  // ATTENTION, ce charCodeAt retourne un number pour le premier caractère seulement, les suivants sont ignorés
  // '0'.charCodeAt() => 48
  // '9'.charCodeAt() => 57
  // 'A'.charCodeAt() => 65
  // 'Z'.charCodeAt() => 65
  // ce code semble donc gérer du [B;G] pour tirer une lettre au hasard entre B et G
  const terme1 = fourchette.substring(1, fourchette.indexOf(';')).charCodeAt()
  const terme2 = fourchette.substring(fourchette.indexOf(';') + 1, fourchette.indexOf(']')).charCodeAt()
  if ((terme1 >= 65) && (terme2 <= 90)) {
    // on passe ici si la partie avant le ; commence par un char > A, et la partie après commence par un char < Z
    const choix = j3pGetRandomInt(terme1, terme2)
    return String.fromCharCode(choix)
  }
  // fin du code à supprimer

  const hasAccolade = fourchette.indexOf('\\{') !== -1
  let int1, int2, valeur
  if (hasAccolade) {
    /** @type {number[]} */
    const listeNb = []
    const pos1 = fourchette.indexOf('\\{')
    const pos2 = fourchette.indexOf('}', pos1)
    // la chaine entre {}
    const str = fourchette.substring(pos1 + 2, pos2)
    // on récupère les morceaux en number (float acceptés)
    str.split(';').forEach(function (chunk) {
      listeNb.push(Number(chunk))
    })
    // FIXME c’est quoi ce binz ! on réanalyse la même chaine x fois et on tire au hasard jusqu'à tomber sur un des nb qu’on avait déjà
    // bonjour la boucle infinie si les nbs de la liste contiennent tous les entiers de l’intervalle
    // (enfin, si ça fonctionnait comme attendu, là on est sauvé par le bug de l’analyse
    // => toujours NaN donc même avec NaN dans la liste on ne passera qu’une fois dans le while, car NaN === NaN est toujours faux, NaN == NaN aussi)
    // on ajoute une sécurité de principe, sans toucher à ce #@?! de code
    // (faudrait au moins sortir l’affectation int1 & int2 du while, mais de toute manière ce code foireux renverra toujours NaN)
    const i = 0
    while (true) {
      // on cherche un [n;m], mais on est dans le cas \{1;2;42} !
      // donc int1 sera toujours NaN avec du parseInt('{1')
      int1 = parseInt(fourchette.substring(1, fourchette.indexOf(';')))
      int2 = parseInt(fourchette.substring(fourchette.indexOf(';') + 1, fourchette.indexOf(']')))
      // on tire un nb entre n et m
      valeur = Math.floor(Math.random() * (1 + int2 - int1)) + int1
      // si valeur n’est pas dans notre listeNb on arrête là
      if (listeNb.indexOf(valeur) === -1) break
      if (i > 1000) return console.error(Error('Après 1000 tirages toujours pas trouvé de nb parmi ' + listeNb.join(' ') + ' avec ' + ch))
    }
  } else {
    int1 = parseInt(fourchette.substring(1, fourchette.indexOf(';')))
    int2 = parseInt(fourchette.substring(fourchette.indexOf(';') + 1, fourchette.indexOf(']')))
    valeur = Math.floor(Math.random() * (1 + int2 - int1)) + int1
  }
  if (Number.isNaN && Number.isNaN(valeur)) console.error(Error('j3pRandom(' + ch + ') va retourner NaN'))
  return valeur
}

/**
 * Retourne un nb décimal dans l’intervalle demandé
 * @param {string} ch par ex '[2.5;9],3' pour récupérer un nombre avec trois décimales entre 2,5 et 9
 * @return {number}
 */
export function j3pRandomdec (ch) {
  // 'ch = [2;9],3'  Renvoie un decimales-3 appartenant à l’intervalle
  const intervalle = ch.substring(0, ch.indexOf(','))
  const nbDecimales = Number(ch.substring(ch.indexOf(',') + 1))
  const borneinf = Number(intervalle.substring(1, intervalle.indexOf(';')))
  const bornesup = Number(intervalle.substring(intervalle.indexOf(';') + 1, intervalle.indexOf(']')))
  const random = j3pGetRandomFloat(borneinf, bornesup)
  return j3pArrondi(random, nbDecimales)
}

/**
 * Un pile ou face qui retourne un booléen
 * @return {boolean}
 */
export function j3pGetRandomBool () {
  return Math.random() < 0.5
}

/**
 * Retourne les bornes entières d’un intervalle
 * @param {string} intervalle
 * @param {Object} [options]
 * @param {Object} [options.sameOrder]
 * @param {Object} [options.strict] passer true pour throw en cas de pb d’ordre
 * @param {Object} [options.onlyPositives] passer true pour throw en cas de nb négatifs
 * @param {Object} [options.alsoFloat] passer true pour autoriser les décimaux
 * @return {number[]} les deux bornes (dans l’ordre de l’intervalle)
 * @throw {Error} si intervalle n’est pas un intervalle d’entiers, ou si options.strict et que les bornes ne sont pas dans le même ordre, ou si options.onlyPositives avec des nb négatifs
 */
export function j3pGetBornesIntervalle (intervalle, { sameOrder, strict, onlyPositives, alsoFloat } = {}) {
  if (typeof intervalle !== 'string') throw Error('argument invalide')
  const regexp = alsoFloat ? captureIntervalleFermeDecimaux : captureIntervalleFermeEntiers
  const chunks = regexp.exec(intervalle)
  if (!chunks) throw Error('intervalle invalide')
  const nb1 = Number(chunks[1])
  const nb2 = Number(chunks[2])
  if (onlyPositives && (nb1 < 0 || nb2 < 0)) throw Error(`Intervalle ${intervalle} invalide, on attend des ${alsoFloat ? 'nombres' : 'entiers'} positifs`)
  if (nb2 < nb1) {
    if (strict) throw Error(`intervalle ${intervalle} invalide avec ${nb2} < ${nb1}`)
    if (sameOrder) return [nb1, nb2]
    return [nb2, nb1]
  }
  return [nb1, nb2]
}

/**
 * Retourne un entier pris au hasard entre les deux nombres (compris)
 * Accepte une syntaxe (déconseillée) avec un seul argument string de la forme [n;m] où n & m sont entiers, retournera 0 si la forme n’est pas respectée
 * @param {number|string} nb1
 * @param {number|string} nb2
 * @return {number}
 */
export function j3pGetRandomInt (nb1, nb2) {
  if (typeof nb1 === 'string' && arguments.length === 1) {
    const result = j3pGetBornesIntervalle(nb1)
    nb1 = result[0]
    nb2 = result[1]
  } else {
    if (typeof nb1 === 'string') nb1 = Number(nb1)
    if (typeof nb2 === 'string') nb2 = Number(nb2)
  }
  if (Number.isNaN(nb1) || Number.isNaN(nb2)) {
    console.error(Error(`Paramètres invalides : "${nb1}" et "${nb2}"`))
    return Number.isNaN(nb1) ? Math.round(nb2) : Math.round(nb1)
  }
  // faut prendre les parties entières, on prend round pour gérer d’éventuels pbs d’arrondis
  // sur les nb qu’on nous file (2,9999999 pris pour 3 et pas 2)
  // FIXME expliquer pourquoi finalement on prend ceil et floor, qui semblent marcher et donnent les bonnes proba, mais sont incohérents avec la remarque précédente (qui semblait sensée)
  const min = Math.ceil(Math.min(nb1, nb2))
  const max = Math.floor(Math.max(nb1, nb2))
  if (min === max) return min
  const valeur = Math.floor(Math.random() * (max - min + 1) + min)
  if (Number.isNaN(valeur)) console.error(Error(`j3pGetRandomInt va retourner NaN avec les arguments "${nb1}" et "${nb2}"`))
  return valeur
}

/**
 * Retourne un nb pris au hasard dans [nb1, nb2] (en string, avec 0 non significatifs ajoutés)
 * (utilise j3pGetRandomFloat qui exclu nb2, mais avec l’arrondi on peut récupérer nb2)
 * @param {number|string} nb1
 * @param {number|string} nb2
 * @param {number|string} nbDecimales
 * @return {string} Renverra 'NaN' si un des arguments n’est pas un nombre ou nbDecimales &lt; 0 ou nbDecimales non entier
 */
export function j3pGetRandomFixed (nb1, nb2, nbDecimales) {
  if (typeof nb1 === 'string') nb1 = Number(nb1)
  if (typeof nb2 === 'string') nb2 = Number(nb2)
  if (typeof nbDecimales === 'string') nbDecimales = Number(nbDecimales)
  if (nbDecimales === 0) console.warn('Utiliser j3pGetRandomInt plutôt que j3pGetRandomFixed pour récupérer des entiers')
  if (!Number.isInteger(nbDecimales) || nbDecimales < 0 || nbDecimales > 99) {
    console.error(Error('nbDecimales invalide ' + nbDecimales))
    return 'NaN'
  }
  const valeur = j3pGetRandomFloat(nb1, nb2)
  if (Number.isNaN && Number.isNaN(valeur)) {
    // pas de console.error, j3pGetRandomFloat l’a déjà fait
    return 'NaN'
  }
  return valeur.toFixed(nbDecimales)
}

/**
 * Retourne un nb pris au hasard dans [nb1, nb2[
 * @param {number|string} nb1
 * @param {number|string} nb2
 * @return {number}
 */
export function j3pGetRandomFloat (nb1, nb2) {
  if (typeof nb1 === 'string') nb1 = Number(nb1)
  if (typeof nb2 === 'string') nb2 = Number(nb2)
  const min = Math.min(nb1, nb2)
  const max = Math.max(nb1, nb2)
  const valeur = Math.random() * (max - min) + min
  // si NaN on râle mais on le retourne
  if (Number.isNaN && Number.isNaN(valeur)) console.error(Error('Arguments invalides => retourne NaN'), nb1, nb2)
  return valeur
}

/**
 * Retourne un élément pris au hasard dans tab
 * @param {Array} tab
 * @return {*} Un des éléments
 */
export function j3pGetRandomElt (tab) {
  if (!Array.isArray(tab)) throw Error('j3pGetRandomElt n’accepte que des tableaux')
  if (!tab.length) throw Error('tableau vide')
  const index = Math.floor(Math.random() * tab.length)
  return tab[index]
}

/**
 * Retourne un tableau de nb éléments pris parmi elts
 * @param {Array} elts
 * @param {number} nb
 * @return {Array} Un tableau de nb éléments pris aléatoirement dans elts
 */
export function j3pGetRandomElts (elts, nb) {
  if (!Array.isArray(elts) || typeof nb !== 'number') throw Error('arguments invalides')
  if (nb > elts.length) throw Error(`Impossible de retourner ${nb} éléments parmi une liste de longueur ${elts.length}`)
  const pioche = [...elts]
  const choix = []
  while (choix.length < nb) {
    const indexChoisi = j3pGetRandomInt(0, pioche.length - 1)
    // on ajoute l’élément choisi
    choix.push(pioche[indexChoisi])
    // on le vire de la pioche
    pioche.splice(indexChoisi, 1)
  }
  return choix
}

/**
 * Retourne un tableau de nb lettres majuscules (pris dans A-Z)
 * @param {number} nb
 * @return {string[]}
 */
export function j3pGetRandomLetters (nb) {
  return j3pGetRandomElts(lettersList, nb)
}

/**
 * Retourne le tableau tab mélangé (sans modifier tab,
 * mais les éléments du tableau retourné sont des références aux éléments de tab,
 * modifier l’un modifiera l’autre si ce sont des objets)
 * @param {Array} tab
 * @return {Array} Un tableau avec les même éléments mélangés
 */
export function j3pShuffle (tab) {
  if (!Array.isArray(tab)) throw Error('argument invalide')
  // cf https://javascript.info/task/shuffle
  const clone = []
  for (let i = 0; i < tab.length; i++) clone.push(tab[i])
  for (let i = clone.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)) // random index from 0 to i
    // swap i & j
    const tmp = clone[i]
    clone[i] = clone[j]
    clone[j] = tmp
  }
  return clone
}

/**
 * Retourne le contenu d’un fichier html|js|json|txt|xml
 * Pour js ça retourne tous les exports (sauf si y’a un seul export par défaut, alors retourné).
 * Pour json ça retourne l’objet, et pour les autres le contenu en string.
 * Attention, async, n’oubliez pas le catch !
 * @param {string} path chemin relatif sans le slash de début (dans sectionsAnnexes pour les js qui sont toujours dossier/fichier.ext, docroot/annexes pour le reste)
 * @return {Promise<string|Object>} Object pour js|json, string pour les autres
 */
export async function j3pImporteAnnexe (path) {
  // avant le 20/02/2023 on avait des ' dans les noms de fichiers, il peut donc y en avoir dans les params de section (dans les graphes)
  path = path.replace('\'', 'Prim')
  // Avec vite (sans le plugin dynamic-import) l’extension doit être mise en statique, sinon ça donne du
  // invalid import "../sectionsAnnexes/${path}". A file extension must be included in the static part of the import. For example: import(`./foo/${bar}.js`).
  // il faut mettre une variable par dossier, car ce qui suit marche pas si path contient un /…
  // cf https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#globs-only-go-one-level-deep
  // on ne met qu’un niveau de dossier dans sectionsAnnexes, le code des sections a été adapté mais pas forcément tous les graphe => on remplace / par -
  const [, pathAndName, ext] = /(.*)\.(html|js|json|txt|xml)$/.exec(path) ?? []
  if (pathAndName == null) throw Error('j3pImporteAnnexe ne gère que les fichiers html|js|json|txt|xml, pour le reste il faut l’importer directement dans la section')
  const chunks = pathAndName.split('/')
  const basename = chunks.pop()
  const folder = chunks.join('-')
  if (chunks.length > 1) console.warn(Error(`Le chemin spécifié contient plus d’un niveau de dossier, il faut corriger le graphe ${path} => ${folder}/${basename}.${ext})`))
  if (chunks.length < 1) throw Error(`Le chemin invalide (${path}), il faut préciser le dossier de sectionsAnnexes : dossier/fichier.ext`)

  if (ext === 'js') {
    // cf https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import
    const module = await import(`src/legacy/sectionsAnnexes/${folder}/${basename}.js`)
    // au 2022-05-02 toutes les sections qui utilisent ça chargent un module qui n’a qu’un seul export par défaut, et s’attendent à récupérer l’objet
    if (module.default && Object.keys(module).length === 1) return module.default
    // mais si y’a plusieurs exports on les retourne tous
    return module
  }

  // pour les autres types, on importe pas, on fetch dans static/
  switch (ext) {
    case 'html':
    case 'txt':
      return fetchText(`${j3pBaseUrl}annexes/${folder}/${basename}.${ext}`)
    case 'xml':
      return fetchXml(`${j3pBaseUrl}annexes/${folder}/${basename}.xml`)
    case 'json':
      return fetchJson(`${j3pBaseUrl}annexes/${folder}/${basename}.json`)
    default:
      throw Error(`Erreur interne, extension ${ext} autorisée mais non gérée`)
  }
}

// Doc

export function j3pMax (tab) {
  let max = tab[0]
  let indice = 0

  for (let i = 1; i < tab.length; i++) {
    if (tab[i] > max) {
      indice = i
      max = tab[i]
    }
  }

  return {
    max,
    indice
  }
}

// Doc

export function j3pMin (tab) {
  let min = tab[0]
  let indice = 0
  for (let i = 1; i < tab.length; i++) {
    if (tab[i] < min) {
      indice = i
      min = tab[i]
    }
  }
  return {
    min,
    indice
  }
}

/**
 * Retourne nb arrondi avec maxDecimales (0 non significatifs retirés, utiliser https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed pour les conserver)
 * Pour rectifier les approximations de js sur les décimaux utiliser plutôt `fixArrondi(nb)` (dans lib/utils/number)
 * @param {number|string} nb
 * @param {number} maxDecimales (si &lt;0 on retournera l’arrondi de la partie entière avec une erreur en console)
 * @return {number}
 */
export function j3pArrondi (nb, maxDecimales) {
  if (maxDecimales > 0) return Number(Number(nb).toFixed(maxDecimales))
  if (maxDecimales === 0) return Math.round(nb)
  console.warn(Error('Le nombre de décimales doit être positif, on suppose que l’opération voulue est l’arrondi de la partie entière mais il faut utiliser j3pArrondi10 pour cela'))
  return j3pArrondi10(nb, -maxDecimales)
}

/**
 * Arrondi un nombre à la puissance de 10 voulue
 * @param {number|string} nb
 * @param {number} puiss10 Passer 2 pour arrondir à la centaine la plus proche et -2 pour le centième
 * @return {number}
 */
export function j3pArrondi10 (nb, puiss10) {
  if (puiss10 < 0) return j3pArrondi(nb, -puiss10)
  if (puiss10 === 0) return Math.round(nb)
  const n = Math.round(puiss10)
  if (Math.abs(puiss10 - n) > 1e-10) throw Error('Puissance de 10 invalide : ' + puiss10)
  return Math.round(nb / Math.pow(10, n)) * Math.pow(10, n)
}

/**
 * Utilisez plutôt formatNumber (dans lib/utils/number) qui est plus strict (et gère mieux la notation scientifique)
 *
 * Retourne le nombre formaté, avec une espace tous les 3 chiffres, séparateur virgule.
 * Appelé avec autre chose qu’une string|number ça retourne '' et râle en console (sauf avec strict où ça plante)
 * j3pNombreBienEcrit('') retourne ''
 * @deprecated
 * @param {string|number} nb
 * @param {Object} [options]
 * @param {boolean} [options.garderZeroNonSignificatif]
 * @param {number} [options.maxDecimales] entre 0 et 12, ignoré sinon
 * @param {boolean} [options.strict] Passer true pour planter plutôt que de râler en console en cas de nombre invalide
 * @return {string}
 */
export function j3pNombreBienEcrit (nb, { garderZeroNonSignificatif, maxDecimales, strict } = {}) {
  if (strict && !Number.isFinite(nb)) throw Error(`Nombre invalide : ${nb}`)
  let strNb
  if (typeof nb === 'string') {
    if (!nb) {
      if (strict) throw Error('Aucun nombre fourni')
      return ''
    }
    strNb = nb
      .replace(/ /g, '') // vire tous les espaces
      .replace(',', '.') // , => .
    nb = Number(strNb)
  }

  if (!Number.isFinite(nb)) {
    console.error(Error(`Nombre invalide : ${strNb || nb}`))
    return ''
  }

  const isNegative = nb < 0
  if (isNegative) {
    nb = -nb
    if (strNb) strNb = strNb.substring(1)
  }
  // nb et strNb sont positifs

  // traitement maxDecimales
  if (maxDecimales && Number.isInteger(maxDecimales) && maxDecimales < 13) {
    let partieEntiereAgarder
    if (garderZeroNonSignificatif && strNb) {
      // Il il faut garder d’éventuels 0 au début, on prend toute la partie entière
      partieEntiereAgarder = strNb.replace(/\..*/, '')
    }
    strNb = nb.toFixed(maxDecimales)
    if (partieEntiereAgarder) strNb = strNb.replace(/.*\./, partieEntiereAgarder + '.')
  }

  // la partie entière (positive)
  const strPartieEntiere = (garderZeroNonSignificatif && strNb)
    ? strNb.replace(/\..*/, '')
    : String(Math.trunc(nb))

  // on démarre l’écriture du nombre
  const r = strPartieEntiere.length % 3
  const firstPartLength = (r === 0) ? 3 : r
  let nbBienEcrit = strPartieEntiere.substring(0, firstPartLength) + strPartieEntiere.substring(firstPartLength).replace(/([0-9]{3})/g, ' $1')
  // on ajoute le signe éventuel
  if (isNegative) nbBienEcrit = '-' + nbBienEcrit

  // si c'était un entier (ou presque), on a fini
  if (Math.abs(nb - Math.trunc(nb)) < 1e-13) return nbBienEcrit

  // pour la partie décimale, nb - Math.trunc(nb) n’est pas fiable car ça peut donner des erreurs d’arrondi
  // (addition d’un float et d’un int en js peut donner des arrondis inattendus)
  let strPartieDecimale
  if (strNb) {
    strPartieDecimale = strNb.replace(/^\d+\./, '')
    if (!garderZeroNonSignificatif) strPartieDecimale = strPartieDecimale.replace(/0+$/, '')
  } else if (Math.abs(Math.trunc(nb)) < 1) {
    // pour les 0.xxx, le cast en string est pas fiable car ça peut donner du 1e-4
    strPartieDecimale = nb.toFixed(12).replace(/^([^.]+\.)/, '').replace(/0+$/, '')
  } else {
    // pour le reste, .toFixed peut arrondir la partie décimale, on prend le cast en string
    strPartieDecimale = String(nb).replace(/^[^.]+\./, '')
  }

  // et on ajoute ça à notre nombre
  const nbCarFin = strPartieDecimale.length % 3 || 3 // si 0 => 3
  const fin = strPartieDecimale.substr(-nbCarFin)
  return nbBienEcrit + ',' + strPartieDecimale.substr(0, strPartieDecimale.length - nbCarFin).replace(/([0-9]{3})/g, '$1 ') + fin
}

/**
 * Renvoie true si les deux tableaux ont des éléments tous "égaux"
 * ATTENTION, sans le 3e param à true c’est de la comparaison laxiste, j3pCompareTab(['', '1'], [0, 1]) renverra true
 * @param {Array} tab1
 * @param {Array} tab2
 * @param {boolean} [strict=false] passer true pour comparer avec une égalité stricte (préférable)
 * @return {boolean}
 */
export function j3pCompareTab (tab1, tab2, strict) {
  if (tab1.length !== tab2.length) return false
  for (let k = 0; k < tab1.length; k++) {
    if (strict) {
      if (tab1[k] !== tab2[k]) return false
    } else {
      if (tab1[k] != tab2[k]) return false // eslint-disable-line eqeqeq
    }
  }
  return true
}

/**
 * Retourne un tableau contenant le résulat des comparaisons des éléments des tableaux deux à deux
 * @param {Array} tab1
 * @param {Array} tab2
 * @param {boolean} [strict=false] passer true pour comparer avec une égalité stricte
 * @return {boolean[]} Si les tableaux ne sont pas de même longueur, la longueur du résulat sera la plus petite des deux (donc la lecture des éléments suivants donnera undefined plutôt qu’un booléen)
 */
export function j3pCompareTab2 (tab1, tab2, strict) {
  const res = []
  const min = Math.min(tab1.length, tab2.length)
  for (let k = 0; k < min; k++) {
    res[k] = strict
      ? (tab1[k] === tab2[k])
      : (tab1[k] == tab2[k]) // eslint-disable-line eqeqeq
  }
  return res
}

/**
 * Retourne les coordonnées de la souris par rapport à un élément du dom (objet {x,y})
 * @param {MouseEvent} event
 * @param {HTMLElement} [elt] si non fourni on prendra la target de l’event (donc ça retournera les coordonnées par rapport à l’élément sur lequel on a cliqué)
 * @return {{x: number, y: number}}
 */
export function j3pGetMousePositionInElt (event, elt) {
  // cf https://stackoverflow.com/questions/3234256/find-mouse-position-relative-to-element/42111623#42111623
  // on utilise getBoundingClientRect qui est géré par tous les navigateurs qu’on gère
  // https://caniuse.com/?search=getBoundingClientRect
  if (!elt) elt = event.target
  // avec du tactile on a pas de clientX directement sur l’event (pour le multitouch)
  if (!event.clientX && event.changedTouches && event.changedTouches[0] && event.changedTouches[0].clientX) {
    event = event.changedTouches[0]
  }
  const ref = elt.getBoundingClientRect()
  return {
    x: event.clientX - ref.left,
    y: event.clientY - ref.top
  }
}

/**
 * Restreint la saisie d’un input à une plage de caractères précisé par une RegExp
 * Vous pouvez utiliser les regex prédéfinies j3pRestriction.(minuscules|majuscules|lettres|lettresAccents|alpha|entier|decimaux)
 * @param {HTMLElement|string} input
 * @param {string|RegExp} restriction Si string, on construira une RegExp en englobant la string avec [].
 */
export function j3pRestriction (input, restriction) {
  if (typeof input === 'string') input = j3pElement(input)
  if (!j3pIsHtmlElement(input, true)) return
  let re
  if (typeof restriction === 'string') {
    try {
      re = convertRestriction(restriction)
    } catch (error) {
      return console.error('restriction invalide', error)
    }
  } else if (restriction instanceof RegExp) {
    // si qqun réaffecte la variable qu’il a passé à la fct ça marcherait,
    // mais pas s’il modifie l’objet regexp, on clone en créant une nouvelle regexp
    re = new RegExp(restriction.source, restriction.flags)
  } else {
    return console.error(Error('restriction doit être une RegExp (ou éventuellement une string)'))
  }
  input.addEventListener('keypress', function (event) {
    j3pRestrict(input, event, re)
  })
}

// on ajoute une liste de regex prédéfinies comme propriétés de la fct
j3pRestriction.minuscules = /[a-z]/g
j3pRestriction.majuscules = /[A-Z]/g
j3pRestriction.lettres = /[a-zA-Z]/g
j3pRestriction.lettresAccents = /[a-zA-ZáäâàéëêèíïîìóöôòúüûùÿÁÄÂÀÉËÊÈÍÏÎÌÓÖÔÚÜÛÙŸæÆœŒçÇñÑ]/g
j3pRestriction.alpha = /[a-zA-Z0-9]/g
j3pRestriction.entiers = /[0-9]/g
j3pRestriction.decimaux = /[0-9,.]/g

/**
 * Listener (keyup ou change) à mettre sur un input pour remplacer les . par des , à la volée
 * Il était souvent mis avec `j3pElement("afficheReponseinput1").setAttribute("onkeyup","j3pRemplacePoint(this)")` mais ça passe pas dans sesaparcours (où les fcts ne sont plus globales)
 * Il faut le mettre avec `elt.addEventListener('input', j3pRemplacePoint)` (car pas sûr que ça fonctionne avec keyup sur tablettes, et keyup match pas un ctrl+c ou un clic milieu)
 */
export function j3pRemplacePoint () {
  if (this && typeof this.value === 'string') {
    // on remplace le premier point et vire les éventuels suivants
    this.value = this.value.replace('.', ',').replace(/\./g, '')
  }
}

/**
 * Prévu pour être un listener keypress, this est supposé être le HTMLElement de l’input
 * Si ça retourne false le caractère tapé n’est pas mis dans l’input
 * @todo faire une fct addRestrictListener(input, restriction, eventType) qui ajouterai le listener et renverrait une fct pour le virer
 * On pourrait aussi imaginer que ça crée une zone de feedback et informe que le caractère saisi est refusé (et voir si on peut pas plutôt mettre ça sur du change, pour autoriser les copier / coller et ne regarder que le résultat, qu’on corrige éventuellement)
 * @private
 * @param {HTMLElement} input ignoré
 * @param {Event} event
 * @param {RegExp} restriction
 * @return {boolean}
 */
export function j3pRestrict (input, event, restriction) {
  try {
    let code
    if (!event) event = window.event
    let passageKeycode = false // c’est pour repérer les flèches, début, fin, suppr ou backspace qui ont autrement le même code que certains caractères
    if (event.keyCode && event.keyCode !== event.charCode) {
      code = event.keyCode
      passageKeycode = true
    } else if (event.which) {
      code = event.which
    }

    // gauche droite, suppr et backspace
    if (([13, 35, 36, 37, 38, 39, 40, 46, 8].indexOf(code) !== -1) && passageKeycode) return true // touche flèches, début, fin, suppr ou backspace
    // if ((code==46)||(code==8)) return true

    // if they pressed esc... remove focus from field...
    if (code === 27) {
      this.blur()
      return false
    }

    const character = String.fromCharCode(code)
    // ignore if they are press other keys
    // strange because code: 39 is the down key AND ' key...
    // and DEL also equals .
    //  if (!event.ctrlKey && code!=9 && code!=8 && code!=36 && code!=37 && code!=38 && (code!=39 || (code==39 && character=="'")) && code!=40) {
    if (!event.ctrlKey && code !== 9 && code !== 8) { // &&  code!=37 && (code!=39 || (code==39 && character=="'"))) {
      if (character.match(restriction)) {
        return true
      }
      event.preventDefault()
      return false
    }
  } catch (error) {
    console.error(error)
    // ça a planté donc par défaut on interdit ce caractère
    // (sinon ça risquerait de planter plus loin et rendre le bug plus dur à trouver)
    event.preventDefault()
    return false
  }
}

export function j3pStylepolice (id, objet) {
  // objet = {police","taille","couleur",cadre}
  const elt = j3pElement(id)

  if (objet.couleur) {
    elt.style.color = objet.couleur
  }
  if (objet.cadre) {
    switch (objet.cadre) {
      case 'cadre1':
        elt.style.padding = '5px'
        elt.style.borderStyle = 'solid'
        elt.style.borderWidth = '1px'
        break
      case 'cadre2':
        elt.style.padding = '5px'
        elt.style.borderStyle = 'solid'
        elt.style.borderWidth = '2px'
        break
    }
  }
  if (objet.police) elt.style.fontFamily = objet.police
  if (objet.taille) elt.style.fontSize = objet.taille
}

export function j3pEspaces (nb) {
  let ch = ''
  for (let k = 1; k <= nb; k++) {
    ch += '&nbsp'
  }
  return ch
}

/**
 * Utiliser mqNormalise à la place (plus fiable et plus performante)
 * @deprecated
 * @param {string|number} chaine
 * @returns {string}
 */
export function j3pMathquillXcas (chaine) {
  function accoladeFermante (chaine, index) {
    // fonction qui donne la position de l’accolade fermante correspondant à l’accolade ouvrante positionnée en index de la chaine
    // penser à une condition d’arrêt pour ne pas faire planter le truc si la saisie est mal écrite
    // (ptet au debut de conversion mathquill : même nb d’accolades ouvrantes que fermantes)
    // pour l’instant 1 accolade ouvrante (à l’appel de la fonction)
    let indexaccolade = 1
    let indexvariable = index
    while (indexaccolade && indexvariable < chaine.length) {
      // je peux avoir des accolades internes (par groupe de 2 necessairement)
      // for (var indexvariable=index+1; indexvariable<chaine.length; indexvariable++){
      indexvariable++
      if (chaine.charAt(indexvariable) === '{') {
        indexaccolade++
      }
      if (chaine.charAt(indexvariable) === '}') {
        indexaccolade--
      }
    }
    return indexvariable
  }

  function necessiteFois (chaine, index) {
    // fonction qui renvoit "*" si à l’index courant on a un chiffre ou i -ou toute autre lettre minuscule- (renvoit une chaine vide sinon)
    // fonction utile à la conversion d’une chaine mathquill (pour laquelle e^{2i\pi} est compréhensible) vers xcas (qui ne comprend pas e^(2i(PI) alors que e^i(PI) oui...)
    const char = chaine.charAt(index)
    return (/[0-9a-z]/.test(char)) ? '*' : ''
  }

  function estParentheseFermante (chaine, index) {
    return chaine.charAt(index) === ')'
  }

  function estParentheseOuvrante (chaine, index) {
    return chaine.charAt(index) === '('
  }

  function ajouteEspace (chaine, tab) {
    for (let i = 0; i <= tab.length - 1; i++) {
      const longueurChaine = tab[i].length
      for (let k = 0; k <= chaine.length - 1; k++) {
        if (chaine.substring(k, k + longueurChaine) === tab[i]) {
          chaine = chaine.substring(0, k + longueurChaine) + ' ' + chaine.substring(k + longueurChaine)
        }
      }
    }
    return chaine
  }

  let pos, i
  // Pour être sûr que c’est une chaine... (pas le cas si 0.5 par exemple)
  let chaineXcas = chaine + ''
  // je vire d’abord les espaces :
  while (chaineXcas.indexOf(':') !== -1) {
    pos = chaineXcas.indexOf(':')
    chaineXcas = chaineXcas.substring(0, pos - 1) + chaineXcas.substring(pos + 1)
  }
  // je vire un éventuel signe multiplié
  while (chaineXcas.indexOf('\\cdot') !== -1) {
    pos = chaineXcas.indexOf('\\cdot')
    chaineXcas = chaineXcas.substring(0, pos) + '*' + chaineXcas.substring(pos + 5)
  }
  while (chaineXcas.indexOf('\\times') !== -1) {
    pos = chaineXcas.indexOf('\\times')
    chaineXcas = chaineXcas.substring(0, pos) + '*' + chaineXcas.substring(pos + 6)
  }
  // On vire un espace inutile parfois présent (cad qu’on trouve dans le code mathquill \cdot  avec ou sans espace juste après
  // commenté car pose pb avec ln x corrigé en lnx non compris par xcas... : parade détecté ln sans parenthèse après et insérer un espace...
  // PB (vu après) : XCAS ne comprend pas la commande ln x, il faut absolument renseigner ln(x)...
  while (chaineXcas.indexOf(' ') !== -1) {
    pos = chaineXcas.indexOf(' ')
    chaineXcas = chaineXcas.substring(0, pos) + chaineXcas.substring(pos + 1)
  }
  // un effet de bord : les codes ln x, sin 2x+3 etc n’ont plus d’espaces et ne sont donc pas correctement interprétés par xcas d’où la rustine suivante :
  chaineXcas = ajouteEspace(chaineXcas, ['ln', 'cos', 'sin', 'tan'])
  // on peut se retrouver avec \\ln x et non ln x
  while (chaineXcas.indexOf('ln (') > -1) {
    chaineXcas = chaineXcas.replace('ln (', 'ln(')
  }
  while (chaineXcas.indexOf('\\ln \\left(') > -1) {
    chaineXcas = chaineXcas.replace('\\ln \\left(', 'ln(')
  }
  while (chaineXcas.indexOf('ln \\left(') > -1) {
    chaineXcas = chaineXcas.replace('ln \\left(', 'ln(')
  }
  const lnxRegexp = /\\ln\s[a-z]/ig
  if (lnxRegexp.test(chaineXcas)) {
    const tabLnX = chaineXcas.match(lnxRegexp)
    for (let i = 0; i < tabLnX.length; i++) {
      const lettre = tabLnX[i][tabLnX[i].length - 1]
      chaineXcas = chaineXcas.replace('\\ln ' + lettre, 'ln(' + lettre + ')')
    }
  }

  // je transforme la virgule (possible avec Mathquill) en point :
  while (chaineXcas.indexOf(',') !== -1) {
    pos = chaineXcas.indexOf(',')
    chaineXcas = chaineXcas.substring(0, pos) + '.' + chaineXcas.substring(pos + 1)
  }

  let pos2, pos3, mult, mult2
  // si le mot clé \frac est présent, on l’enlève et on convertit \frac{A}{B} en A/B
  // CODE QUI SUIT PEUT ETRE MIS DANS UNE FONCTION RECURSIVE (en remplaçant while par if) : si tous les if ne donnent rien, cette fonction renvoit le string passé en paramètre
  while (chaineXcas.indexOf('\\frac') !== -1) {
    // il y a une fraction
    pos = chaineXcas.indexOf('\\frac')
    // on vire le frac, pour 2+\frac{3}{4}, il reste ensuite 2+{3}{4}
    chaineXcas = chaineXcas.substring(0, pos) + chaineXcas.substring(pos + 5)
    // je detecte la fin du numérateur pour extraire A :
    pos2 = accoladeFermante(chaineXcas, pos)
    // AVEC LA FONCTION RECURSIVE : var chaineXcasA=recur(chaineXcas.substring(pos+1,pos2))
    const chaineXcasA = chaineXcas.substring(pos + 1, pos2)
    // pareil pour B :
    pos3 = accoladeFermante(chaineXcas, pos2 + 1)
    // AVEC LA FONCTION RECURSIVE : var chaineXcasB=recur(chaineXcas.substring(pos2+1,pos3))
    const chaineXcasB = chaineXcas.substring(pos2 + 2, pos3)
    if (necessiteFois(chaineXcas, pos3 + 1) === '*' || estParentheseOuvrante(chaineXcas, pos3 + 1)) {
      mult2 = '*'
    } else {
      mult2 = ''
    }
    chaineXcas = chaineXcas.substring(0, pos) + '(' + chaineXcasA + ')/(' + chaineXcasB + ')' + mult2 + chaineXcas.substring(pos3 + 1)
  }
  while (chaineXcas.indexOf('\\sqrt') !== -1) {
    // il y a une racine, il faut remplacer \sqrt{A} par sqrt(A)
    // on teste aussi s’il faut un * avant car xcas ne comprend pas 2sqrt(3)
    pos = chaineXcas.indexOf('\\sqrt')
    pos2 = pos + 4
    if (necessiteFois(chaineXcas, pos - 1) === '*' || estParentheseFermante(chaineXcas, pos - 1)) {
      mult = '*'
      pos2++
    } else {
      mult = ''
    }
    // je vire le \ en ajoutant le signe * si besoin
    chaineXcas = chaineXcas.substring(0, pos) + mult + chaineXcas.substring(pos + 1)
    // je remplace { par (
    chaineXcas = chaineXcas.substring(0, pos2) + '(' + chaineXcas.substring(pos2 + 1)
    // je detecte la fin de ce qu’il y a dans la racine'
    pos3 = accoladeFermante(chaineXcas, pos2)
    chaineXcas = chaineXcas.substring(0, pos3) + ')' + chaineXcas.substring(pos3 + 1)
  }
  while (chaineXcas.indexOf('\\mathrm{e}') !== -1) {
    pos = chaineXcas.indexOf('\\mathrm{e}')
    chaineXcas = chaineXcas.substring(0, pos) + 'e' + chaineXcas.substring(pos + 10)
  }

  // un soucis ici : si un seul terme dans l’exponentielle, MQ ne met pas d’accolade donc il faut refaire le test avec 'e^' pour savoir si necessite fois...
  while (chaineXcas.indexOf('e^{') !== -1) {
    // il y a un exponentielle, il faut remplacer e^{A} par e^(A) + test si signe * necessaire
    pos = chaineXcas.indexOf('e^{')
    pos2 = accoladeFermante(chaineXcas, pos + 2)
    if (necessiteFois(chaineXcas, pos - 1) === '*' || estParentheseFermante(chaineXcas, pos - 1)) {
      mult = '*'
    } else {
      mult = ''
    }
    if (necessiteFois(chaineXcas, pos2 + 1) === '*' || estParentheseOuvrante(chaineXcas, pos2 + 1)) {
      mult2 = '*'
    } else {
      mult2 = ''
    }
    chaineXcas = chaineXcas.substring(0, pos) + mult + chaineXcas.substring(pos, pos + 2) + '(' + chaineXcas.substring(pos + 3, pos2) + ')' + mult2 + chaineXcas.substring(pos2 + 1)
  }
  // je traite le cas particulier :
  for (let i = 0; i < chaineXcas.length; i++) {
    if (chaineXcas.charAt(i) === 'e' && chaineXcas.charAt(i + 1) === '^' && chaineXcas.charAt(i + 2) !== '(') {
      if (necessiteFois(chaineXcas, i - 1) === '*' || estParentheseFermante(chaineXcas, i - 1)) {
        mult = '*'
      } else {
        mult = ''
      }
      if (necessiteFois(chaineXcas, i + 3) === '*' || estParentheseFermante(chaineXcas, i + 3)) {
        mult2 = '*'
      } else {
        mult2 = ''
      }
      chaineXcas = chaineXcas.substring(0, i) + mult + chaineXcas.substring(i, i + 3) + mult2 + chaineXcas.substring(i + 3)
      i++
    }
  }
  // il y a parfois un pb avec un signe * qui s’est inséré juste avant une parenthèse fermante (cas (e^2) qui s'écrit (e^2*)
  // je ne sais pas d’où vient l’erreur (et je ne veux pas créer de nouveau bug) donc je corrige ici
  i = 0
  while (i <= chaineXcas.length) {
    if ((chaineXcas.charAt(i) === '*') && (chaineXcas.charAt(i + 1) === ')')) {
      chaineXcas = chaineXcas.substring(0, i) + chaineXcas.substring(i + 1)
    } else {
      i++
    }
  }
  // On autorise la saisie en direct de exp(), du coup il faut tester s’il faut rajouter un signe fois avant :
  for (let i = 0; i < chaineXcas.length; i++) {
    if (chaineXcas.substring(i, i + 3) === 'exp' && (necessiteFois(chaineXcas, i - 1) === '*' || estParentheseFermante(chaineXcas, i - 1))) {
      chaineXcas = chaineXcas.substring(0, i) + '*' + chaineXcas.substring(i)
      i++
    }
  }

  while (chaineXcas.indexOf('\\pi') !== -1) {
    pos = chaineXcas.indexOf('\\pi')
    // je remplace le \ par (
    if (necessiteFois(chaineXcas, pos - 1) === '*' || estParentheseFermante(chaineXcas, pos - 1)) {
      mult = '*'
    } else {
      mult = ''
    }
    chaineXcas = chaineXcas.substring(0, pos) + mult + '(PI)' + chaineXcas.substring(pos + 3)
  }

  // A VIRER AUSSI : \left et \right pour les parenthèses
  while (chaineXcas.indexOf('\\left') !== -1) {
    pos = chaineXcas.indexOf('\\left')
    if (necessiteFois(chaineXcas, pos - 1) === '*' || estParentheseFermante(chaineXcas, pos - 1)) {
      mult = '*'
    } else {
      mult = ''
    }
    chaineXcas = chaineXcas.substring(0, pos) + mult + chaineXcas.substring(pos + 5)
  }
  while (chaineXcas.indexOf('\\right') !== -1) {
    pos = chaineXcas.indexOf('\\right')
    if (necessiteFois(chaineXcas, pos + 7) === '*' || estParentheseOuvrante(chaineXcas, pos + 7)) {
      mult = '*'
    } else {
      mult = ''
    }
    chaineXcas = chaineXcas.substring(0, pos) + ')' + mult + chaineXcas.substring(pos + 7)
  }

  // pour gérer un pb  : (2+racine(3))i n’est pas compris par xcas, j’ajoute un fois...
  for (let i = 0; i <= chaineXcas.length; i++) {
    if (chaineXcas.charAt(i) === 'i' && (necessiteFois(chaineXcas, i - 1) === '*' || estParentheseFermante(chaineXcas, i - 1))) {
      chaineXcas = chaineXcas.substring(0, i) + '*' + 'i' + chaineXcas.substring(i + 1)
      i++
    }
  }

  // même genre de pb  : i3 n’est pas compris par xcas, j’ajoute un fois...(mais pas à "i)" d’où la modif de code pour estParentheseFermante
  for (let i = 0; i <= chaineXcas.length; i++) {
    if (chaineXcas.charAt(i) === 'i' && necessiteFois(chaineXcas, i + 1) === '*') {
      chaineXcas = chaineXcas.substring(0, i) + 'i' + '*' + chaineXcas.substring(i + 1)
      i++
    }
  }
  // A nouveau 2ln(2x+3) n’est pas compris par xcas:
  for (let i = 0; i < chaineXcas.length; i++) {
    if (chaineXcas.substring(i, i + 2) === 'ln' && (necessiteFois(chaineXcas, i - 1) === '*' || estParentheseFermante(chaineXcas, i - 1))) {
      chaineXcas = chaineXcas.substring(0, i) + '*' + chaineXcas.substring(i)
      i++
    }
  }
  while (chaineXcas.indexOf('{') !== -1) {
    // les puissances par exemple, on remplace x^{n+1} par x^(n+1)
    pos = chaineXcas.indexOf('{')
    pos2 = accoladeFermante(chaineXcas, pos)
    chaineXcas = chaineXcas.substring(0, pos) + '(' + chaineXcas.substring(pos + 1)
    chaineXcas = chaineXcas.substring(0, pos2) + ')' + chaineXcas.substring(pos2 + 1)
    // on vire le frac, pour 2+\frac{3}{4}, il reste ensuite 2+{3}{4}
  }

  return chaineXcas
}

/**
 * Insère souschaine dans ch à la place de ce qu’il y avait entre position1 et position2 (inclus)
 * j3pRemplace('portnawak', 2, 5, 'FOO') => poFOOwak
 * @param {string} ch
 * @param {number} position1
 * @param {number} position2
 * @param {string} souschaine
 * @return {string}
 */
export function j3pRemplace (ch, position1, position2, souschaine) {
  if (position2 < position1) return console.error(Error('position invalide'))
  // si y’a un pb de type on laisse la suite planter
  return ch.substring(0, position1) + souschaine + ch.substring(position2 + 1)
}

/**
 * Remplace dans ch les £a par values.a et les £{toto} par values.toto
 *   j3pChaine("Quelle est la somme des entiers £a, £{truc}, £a et £foo ?",{a: 1, foo: 2, truc: 3})
 * retourne
 *   Quelle est la somme des entiers 1, 3, 1 et 2 ?
 * Attention à utiliser la syntaxe avec {} si plusieurs variables on le même début (f et foo par ex, car le £f de
 * £foo risque d'être substitué, l’ordre des substitutions n’étant pas garanti)
 * @param {string} ch
 * @param {Object} values Les clés doivent être l
 * @return {string}
 */
export function j3pChaine (ch, values) {
  // Il arrive que cette fonction soit appelée alors qu’il n’y a pas de variable à remplacer.
  // Dans ce cas, cette fonction ne fait rien...
  if (!values || typeof values !== 'object') {
    console.error(Error('valeurs incorrectes'), values)
    return ch
  }
  for (const [prop, value] of Object.entries(values)) {
    // avec ou sans {}, indépendamment de la longueur de p (une lettre ou plusieurs)
    ch = ch
      .replace(new RegExp('£{' + prop + '}', 'g'), value)
      .replace(new RegExp('£' + prop, 'g'), value)
  }
  return ch
}

export function j3pDefinirclasse (id, className) {
  const elt = j3pElement(id)
  if (elt) elt.className = className
}

/**
 * Un getElementById amélioré, qui râle en console s’il ne trouve pas l’élément (ou s’il le trouve et qu’il devrait pas), mais aussi si y’en a plusieurs
 * @param {string} id
 * @param {boolean|null} [shouldExists=true] Avec undefined|true il râle s’il ne trouve pas, avec false il râle s’il trouve, et avec tout le reste il reste muet (donc passer null pour le faire taire indépendamment du résultat)
 * @return {HTMLElement|null}
 */
export function j3pElement (id, shouldExists) {
  if (id instanceof HTMLElement) {
    console.error(Error('j3pElement appelé avec un HTMLElement'))
    return id
  }
  if (typeof id !== 'string') return console.error(TypeError('j3pElement ne traite que des string'))
  // on utilise querySelectorAll plutôt que getElementById parce que l’on veut savoir s’il y en a plusieurs
  // Mais y’a des sections qui affectent n’importe quoi comme id (qui commence par un chiffre ou contient des caractères invalides comme des parenthèses)
  // => ça plante avec du `xxx is not a valid selector.`, d’où le test préalable (y’avait un try/catch dans le commit 604d3284)
  // cf https://www.w3.org/TR/1999/REC-html401-19991224/types.html#type-name
  let elts
  if (isValidId(id)) {
    elts = document.querySelectorAll('#' + id)
  } else {
    const elt = document.getElementById(id)
    elts = elt ? [elt] : [] // si y’en a pas on veut une liste vide et pas [null]
  }

  if (elts.length) {
    if (elts.length > 1) console.error(Error(`Il y a ${elts.length} éléments dans le dom avec l’id ${id} !`))
    else if (shouldExists === false) console.error(Error(`Il y a déjà un élément d’id ${id} dans le DOM`))
    return elts[0]
  }

  // rien trouvé…
  if (!elts.length && (shouldExists === undefined || shouldExists === true)) {
    // Il faut le signaler
    console.error(Error('Aucun élément ' + id + ' dans le document courant'))
  }

  return null
}

/**
 * Vérifie que elt est un élément html (ou l’id d’un élément qui existe dans le dom).
 * Peut donc être utilisé à la place de j3pElement (avec un try/catch autour) pour récupérer l’élément sans tester avant si c’est une string ou un élément
 * @param {HTMLElement|string} elt
 * @return {HTMLElement}
 * @throws {Error} si elt n’est pas un HTMLElement
 */
export function j3pEnsureHtmlElement (elt) {
  if (typeof elt === 'string') elt = j3pElement(elt)
  if (!isDomElement(elt, { htmlOnly: true })) throw Error(`élément invalide (${Object.prototype.toString.call(elt)})`)
  return elt
}

/**
 * Retourne true si elt est un HTMLElement (ou HTMLDivElement ou HTML…Element)
 * @param {*} elt
 * @param {boolean} [warnIfNotElt=false] Passer true pour que ça ajoute une erreur en console si elt n’était pas un HTMLElement
 * @return {boolean}
 */
export function j3pIsHtmlElement (elt, warnIfNotElt) {
  // attention, String(elt) marche dans la plupart des cas, mais pas pour un elt <a> par ex,
  // parce que HTMLAnchorElement a sa propre méthode toString (va savoir pourquoi)
  // String(document.createElement('b')) => "[object HTMLElement]"
  // String(document.createElement('span')) => "[object HTMLSpanElement]"
  // String(document.createElement('tr')) => "[object HTMLTableRowElement]"
  // String(document.createElement('a')) => ""
  // HTMLElement.prototype.toString.call(document.createElement('a')) // => "[object HTMLAnchorElement]"
  // HTMLAnchorElement.prototype.toString.call(document.createElement('a')) // => ""
  const str = HTMLElement.prototype.toString.call(elt)
  const ok = /^\[object HTML[a-zA-Z]*Element]$/.test(str)
  if (!ok && warnIfNotElt) console.error(Error(`élément invalide (${str})`), elt)
  return ok
}

/**
 * Retourne true si elt est un SVGElement (ou SVGLineElement ou SVG…Element)
 * @param {*} elt
 * @param {boolean} [warnIfNotElt=false] Passer true pour que ça ajoute une erreur en console si elt n’était pas un SVGElement
 * @return {boolean}
 */
export function j3pIsSvgElement (elt, warnIfNotElt) {
  const ok = /^\[object SVG[a-zA-Z]*Element]$/.test(String(elt))
  if (!ok && warnIfNotElt) console.error(Error('élément invalide'))
  return ok
}

/**
 * Retourne un id inutilisé dans le dom (sous la forme prefixe + nb)
 * @param {string} [prefixe=id]
 * @param {boolean} [preserve] Passer true pour que prefixe soit renvoyé tel quel si l’id est libre (et sinon les suffixes demarreront à 2)
 * @return {string}
 */
export function j3pGetNewId (prefixe, preserve) {
  if (!prefixe || typeof prefixe !== 'string') prefixe = 'id'
  isValidId(prefixe) // si KO ça râlera en console mais on laisse continuer, ça va casser trop de choses si on devient strict après autant d’années de laxisme ;-)
  let i = 0
  if (preserve) {
    // le premier sera sans suffixe et les suivants auront un suffixe qui démarrera à 2
    if (!document.getElementById(prefixe)) return prefixe
    i = 2
  }
  const max = 50000
  while (document.getElementById(`${prefixe}_${i}`) && i < max) i++
  const id = (i < max) ? `${prefixe}_${i}` : ''
  if (!id) console.error(Error(`Il y a déjà plus de ${max} éléments préfixés par ${prefixe} => on arrête là (aucun id généré)`))
  // si on a demandé à préserver l’id et qu’on a pas pu le faire, on le signale
  if (preserve) console.error(Error(`l’id ${prefixe} existait déjà, il a été remplacé par "${id}"`))
  return id
}

/**
 * Ajoute un input text dans un conteneur
 * @param {HTMLElement|string} container
 * @param {object} [options]
 * @param {string} [options.id] id à mettre sur l’input créé
 * @param {string} [options.texte] texte pour initialiser l’input
 * @param {object} [options.style]
 * @param {RegExp|string} [options.restrict] Si fourni on ajoutera un listener sur keypress pour limiter la saisie possible à cette plage de caractères
 * @param {number} [options.taille] prop size de l’input
 * @param {number} [options.maxchars] nb de char max (prop maxLength), par défaut sera mis à 20 (ou la taille du texte si > 20)
 * @param {number} [options.tailletexte] On ajoutera px pour le mettre dans style.fontSize
 * @param {string} [options.couleur] sera mis dans style.color
 * @param {string} [options.fond] sera mis dans style.backgroundColor
 * @param {string} [options.police] sera mis dans style.fontFamily
 * @param {number} [options.width] On ajoutera px pour le mettre dans style.width
 * @param {number} [options.top] On ajoutera px pour le mettre dans style.top (en ajoutant style.position = 'absolute' si ça n’a pas été précisé autrement via options.style.position)
 * @param {number} [options.left] On ajoutera px pour le mettre dans style.top (en ajoutant style.position = 'absolute' si ça n’a pas été précisé autrement via options.style.position)
 * @param {string} [options.tabindex] Si fourni sera appliqué (comme attribut) à l’input
 * @return {HTMLElement} L’input ajouté dans le DOM
 */
export function j3pAjouteZoneTexte (container, options) {
  if (typeof container === 'string') container = j3pElement(container)
  if (!j3pIsHtmlElement(container, true)) return
  if (!options) options = {}
  const defaultStyle = { padding: '5px' }
  const props = {
    type: 'text',
    value: options.texte || '',
    autocomplete: 'off',
    size: options.taille || options.maxchars || Math.max((options.texte && options.texte.length) || 0, 20),
    style: Object.assign(defaultStyle, options.style)
  }
  if (options.id) props.id = options.id
  if (Number.isInteger(Number(options.maxchars))) {
    props.maxLength = Number(options.maxchars)
  }
  // modif de style, un raccourci
  const st = props.style
  if (options.tailletexte) st.fontSize = options.tailletexte + 'px'
  if (options.fond) st.backgroundColor = options.fond
  if (options.couleur) st.color = options.couleur
  if (options.police) st.fontFamily = options.police
  if (options.width) st.width = options.width + 'px'
  if (options.top) {
    st.position = (typeof st.position === 'string' && st.position) || 'absolute'
    st.top = options.top + 'px'
    st.left = options.left + 'px'
  }
  const inputElt = j3pAddElt(container, 'input', '', props)
  if (options.tabindex) inputElt.setAttribute('tabindex', options.tabindex)
  if (options.restrict) {
    let restriction = options.restrict
    if (typeof restriction === 'string') {
      try {
        restriction = convertRestriction(restriction, { flags: 'g' })
      } catch (error) {
        return console.error('restriction invalide', error)
      }
    }
    if (restriction instanceof RegExp) {
      inputElt.addEventListener('keypress', function (event) {
        j3pRestrict(inputElt, event, restriction)
      })
    } else {
      return console.error(Error('options.restrict invalide'), options.restrict)
    }
  }
  // y’a eu ça à une époque, à priori plus personne ne l’utilise
  if (options.correction) {
    console.error(Error('L’option correction n’est pas gérée par j3pAjouteZoneTexte'))
  }
  return inputElt
} // j3pAjouteZoneTexte

/**
 * Applique toutes les clés / valeurs de styleProps à l’attribut style de l’élément
 * (idem setStyle de lib/utils/css mais sans planter si elt n’est pas un élément, ici ça râle seulement)
 * @param {HTMLElement|string} elt
 * @param {object} styleProps
 */
export function j3pStyle (elt, styleProps) {
  if (!elt) return console.error(Error('Il faut passer un élémént'))
  if (typeof elt === 'string') {
    elt = j3pElement(elt)
    if (!elt) return // y’a déjà eu un message en console
  }
  try {
    if (typeof styleProps === 'object' && (styleProps.couleur || styleProps.taillepolice)) _convertStyleExoticProps(styleProps)
    setStyle(elt, styleProps)
  } catch (error) {
    console.error(error, elt)
  }
}

export function j3pEcritBienAxPlusB (a, b) {
  // on peut nous passer des strings
  if (typeof a !== 'number') a = Number(a)
  if (typeof b !== 'number') b = Number(b)
  // les deux nuls
  if (a === 0 && b === 0) return '0'
  // b nul
  if (b === 0) {
    if (a === 1) return '$x$'
    if (a === -1) return '$-x$'
    return '$£ax$'
  }
  // a nul
  if (a === 0) return '$£b$'
  // les deux non nuls
  if (b > 0) {
    if (a === 1) return '$x+£b$'
    if (a === -1) return '$-x+£b$'
    return '$£ax+£b$'
  }
  // b négatif
  if (a === -1) return '$-x£b$'
  if (a === 1) return '$x£b$'
  return '$£ax£b$'
}

export function j3pEcritBienAxCarre (a) {
  if (!Number.isFinite(a)) throw Error('paramètre invalide')
  if (a === 1) return '$x^2$'
  if (a === -1) return '$-x^2$'
  return '$£ax^2$'
}

/**
 * Affiche La palette d’outils mathquill
 * @param {HTMLElement|string} container
 * @param {HTMLElement|string} input Id de l’input dans lequel les clics sur les boutons doivent écrire
 * @param {object} [options]
 * @param {string[]} [options.liste] La liste des boutons voulus (cf source de j3pPaletteMathquill pour avoir la liste des possibles)
 * @param {string} [options.id=MepBarreBoutonsMQ] Id du div créé. S’il existait déjà on le détruit avant de recréer une nouvelle palette (ça arrive souvent si la même palette change d’input de destination)
 * @param {object} [options.position] OBSOLETE, passer plutôt par le style pour gérer le positionnement. Si fourni ça impose style.position = 'absolute' comme avant
 * @param {number} [options.position.left]
 * @param {number} [options.position.top]
 * @param {object} [options.style]
 * @return {HTMLElement} Le div créé
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pPaletteMathquill (container, input, options) {
  if (!container) return console.error(Error('ce conteneur n’existe pas (ou plus)'), container)
  container = j3pEnsureHtmlElement(container)
  // liste des boutons connus, avec la fct pour l’ajouter
  // pour ajouter un bouton il faut
  // - construire un png
  // - ajouter la classe dans css/j3pboutons.css
  // - ajouter la fct j3pAjouteMqXxx

  // Création d’un div BarreBoutonsMQ dans le container
  // j3pPaletteMathquill(this.zones.IG,"reponse1",{liste:["racine","fraction","puissance","pi","exp"]})
  // Possibilité de spécifier un id plutôt que la création auto de BarreBoutonsMQ...
  // j3pPaletteMathquill(this.zones.IG,"reponse1",{liste:["racine","fraction","puissance","pi","exp"],nomdiv:"MepBarreBoutonsMQ2"})

  if (typeof options !== 'object') options = {}
  const props = {}
  // style
  if (typeof options.style === 'object') {
    props.style = j3pClone(options.style)
  } else {
    props.style = {}
  }
  // position
  const pos = options.position
  const oldBehaviour = (typeof pos === 'object' && hasProp(pos, 'top') && hasProp(pos, 'left'))
  if (oldBehaviour) {
    console.warn('le paramètre position est obsolète pour j3pPaletteMathquill, le supprimer devrait suffire dans la plupart des cas, sinon passer par style.position, style.top et style.left')
    props.style.position = 'absolute'
    props.style.top = pos.top
    props.style.left = pos.left
  } else {
    // il faut forcer le positionnement du conteneur de la palette, car les boutons sont positionnés (par mathquill)
    // on impose relative, sauf si on a exigé autre chose
    if (!props.style.position) props.style.position = 'relative'
    // et on positionne le parent s’il ne l’est pas
    if (!container.style.position) container.style.position = 'relative'
  }

  // conteneur de la palette div#BarreBoutonsMQ
  // @todo ne plus utiliser d’id (ça évitera d’en avoir plusieurs identiques)
  props.id = options.id || options.nomdiv || 'barreBoutonsMQ'
  let div = j3pElement(props.id, null)
  if (div) {
    j3pDetruit(div)
  }
  // si on passe top & left on respecte
  div = oldBehaviour
    ? j3pAjouteDiv(container, props.id, '', props) // ça décale top & left, comme avant
    : j3pAddElt(container, 'div', '', props)
  // si on fourni pas de liste on met seulement ces 3 boutons (avant le 26/01/2020 c'était tous)
  const liste = (Array.isArray(options.liste) && options.liste.length)
    ? options.liste
    : ['fraction', 'puissance', 'racine']
  for (const buttonName of liste) {
    const isKnown = Boolean(mqCommandes[buttonName])
    // une ou deux classes css (toujours mqButton)
    const className = 'mqBtn' + (isKnown ? ` mqBtn${buttonName}` : '')
    const cb = isKnown
      ? mqCommandes[buttonName].bind(null, input)
      : mqAjoute.bind(null, buttonName, input)
    // des espaces pour les buttonName connus (sauf cette liste), sinon le buttonName
    const value = (isKnown && !['ln', 'log', 'cos', 'sin', 'tan'].includes(buttonName)) ? '     ' : buttonName
    j3pAjouteBouton(div, cb, { className, value })
  }

  return div
} // j3pPaletteMathquill

/**
 * Parse une string pour récupérer des éléments de tableau
 * Chaque élément sera converti en nombre si c’est un nombre, sinon ses éventuelles espaces de début et fin seront enlevées
 * @param {string} tabInString La string décrivant un tableau
 * @return {Array<string|number>} Toujours un tableau, vide si tabInString n’était pas de la forme '[…]'
 */
export function j3pParseArray (tabInString) {
  if (typeof tabInString !== 'string' || !/^\[.*]$/.test(tabInString)) {
    console.error(Error('argument invalide'), tabInString)
    return []
  }
  // le cas du tableau vide (sinon le map suivant retournerait un tableau
  // avec un élément qui serait une string vide)
  if (tabInString === '[]') return []
  return tabInString.substr(1, tabInString.length - 2)
    .split(',')
    .map(function (item) {
      // on cast en number et retourne le résultat si c’est un nb, sinon la string d’origine
      const n = Number(item)
      if (isNaN(n)) return item.trim()
      return n
    })
}

/**
 * Retourne les bornes d’un intervalle
 * @param {string} intervalle sous la forme [xxx;yyy]
 * @return {{min: number, max: number}}
 */
export function j3pParseInter (intervalle) {
  if (typeof intervalle !== 'string') throw Error('argument invalide (' + typeof intervalle + ')')
  const chunks = /^\[([0-9.-]+);([0-9.-]+)]$/.exec(intervalle.trim())
  if (!chunks) throw Error('argument invalide (' + intervalle + ')')
  return {
    min: Number(chunks[1]),
    max: Number(chunks[2])
  }
}

/**
 * Retourne numérateur, dénominateur et valeur d’une fraction (lance une erreur si c'était pas une fraction valide)
 * @param {string} frac
 * @return {{num: number, den: number, value: number}}
 */
export function j3pParseFrac (frac) {
  if (typeof frac !== 'string') throw Error('type incorrect')
  const chunks = /^([+-]*[0-9e]+)\/([0-9]+)$/.exec(frac)
  if (!chunks) throw Error('il faut passer une fraction')
  const num = Number(chunks[1])
  const den = Number(chunks[2])
  if (isNaN(num)) throw Error('numérateur invalide')
  if (isNaN(den)) throw Error('dénominateur invalide')
  if (den === 0) throw Error('division par zéro')
  return {
    num,
    den,
    value: num / den
  }
}

/**
 * Retourne numérateur et dénominateur en simplifiant la fraction (le dénominateur sera toujours un entier positif, le numérateur toujours entier)
 * @param {number|string} num
 * @param {number|string} den
 * @return {{num: number, den: number}}
 */
export function j3pSimplifieQuotient (num, den) {
  if (typeof num === 'string') num = j3pNombre(num)
  if (typeof den === 'string') num = j3pNombre(den)
  if (!Number.isFinite(num)) throw Error(`Numérateur invalide ${num}`)
  if (Math.abs(num) >= 1e13) throw RangeError(`Numérateur trop grand (1e13 max) : ${num}`)
  if (!Number.isFinite(den)) throw Error(`Dénominateur invalide ${den}`)
  if (Math.abs(den) >= 1e13) throw RangeError(`Dénominateur trop grand (1e13 max) : ${den}`)
  if (Math.abs(den) < epsilon) throw RangeError('division par 0')
  // le cas nul
  if (Math.abs(num / den) < epsilon) return { num: 0, den: 1 }

  // on veut un dénomimateur positif
  if (den < 0) {
    den = -den
    num = -num
  }
  // gommage d’éventuels pbs d’arrondi js
  num = j3pMeilleurArrondi(num)
  den = j3pMeilleurArrondi(den)

  // le cas dénominateur unité
  if (Math.abs(den - 1) < epsilon) return { num, den: 1 }

  // on veut une fraction d’entiers, on regarde s’il faut multiplier num&den par une puissance de 10
  const [puissNum, puissDen] = [num, den].map((nb) => {
    if (Number.isInteger(nb)) return 0
    // attention, il faut traiter le cas 1e-7
    const strNb = (Math.abs(nb) < 1)
      ? nb.toFixed(12).replace(/0+$/g, '') // on vire les 0 non significatifs
      : String(nb)
    // on retourne le nb de chiffres après la virgule
    return Number(strNb.replace(/^.*\./, '').length)
  })
  const puissMax = Math.max(puissNum, puissDen)
  if (puissMax) {
    // Math.round est insdispensable ici pour ganrantir que num et den sont bien des entiers.
    num = Math.round(num * Math.pow(10, puissMax))
    den = Math.round(den * Math.pow(10, puissMax))
  }
  // on regarde si on peut simplifier la fraction
  const div = pgcd(Math.abs(num), den)
  if (div > 1) {
    num = j3pMeilleurArrondi(num / div)
    den = j3pMeilleurArrondi(den / div)
  }

  if (Math.abs(num) >= 1e13) throw RangeError(`Le numérateur final est trop grand (${num}/${den})`)

  return { num, den }
}

/**
 * Retourne le pgcd de deux nombres, avec pas mal d’options pour gérer les cas sans pgcd (utilisez plutôt {@link module:lib/utils/number.pgcd} si vous voulez un vrai pgcd)
 * @param {number} x
 * @param {number} y
 * @param {Object} [options]
 * @param {boolean} [options.quiet=false] Passer true pour ne pas râler en console si le PGCD n’est pas possible
 * @param {boolean} [options.returnOtherIfZero] passer true pour retourner l’autre si l’un des deux est nul
 * @param {number} [options.valueIfZero] valeur à retourner si l’un des deux est nul
 * @param {number} [options.negativesAllowed] passer true pour prendre les valeurs absolues avant de calculer le pgcd
 * @return {number|undefined} undefined si le calcul du PGCD n’est pas possible (nombres négatif sans avoir passé negativesAllowed, ou un 0 sans avoir précisé returnOtherIfZero ou valueIfZero)
 */
export function j3pPGCD (x, y, { quiet, returnOtherIfZero, negativesAllowed, valueIfZero } = {}) {
  try {
    // on gère d’abord la liste d’exceptions
    if (negativesAllowed) {
      x = Math.abs(x)
      y = Math.abs(y)
    }
    if (returnOtherIfZero) {
      if (x < epsilon) return y
      if (y < epsilon) return x
    }
    if (valueIfZero) {
      if (x < epsilon || y < epsilon) return valueIfZero
    }
    // on retourne le pgcd
    return pgcd(x, y)
  } catch (error) {
    if (!quiet) console.error(error)
  }
}

/**
 * Fixe inputElt.style.width d’après ce qu’il contient
 * @param {HTMLInputElement|string} inputElt
 */
export function j3pAutoSizeInput (inputElt) {
  if (typeof inputElt === 'string') inputElt = j3pElement(inputElt)
  if (String(inputElt) !== '[object HTMLInputElement]') return console.error(Error('Zone de saisie invalide'), String(inputElt), inputElt)
  const { fontSize, fontFamily } = getComputedStyle(inputElt)
  const longueur = $(inputElt).textWidth(inputElt.value, fontFamily, fontSize)
  // parfois il y avait un décalage de 16px pour firefox, parfois ça décalait aussi pour iOS.
  // le test du userAgent étant vraiment peu fiable on décale de 16px pour tout le monde (y’aura un peu plus de marge sous chrome, vraiment pas grave)
  inputElt.style.width = (longueur + 16) + 'px'
}

/**
 * Retourne le décalage par rapport au conteneur j3p (#Mepact)
 * @param {HTMLElement|string} elt
 * @return {{x: number, y: number}}
 */
export function j3pFindpos (elt) {
  if (typeof elt === 'string') elt = j3pElement(elt)
  if (!j3pIsHtmlElement(elt, true)) return {}
  const conteneur = getJ3pConteneur(elt)
  if (!conteneur) return console.error(Error('cet élément n’est pas dans un conteneur j3p'), elt)
  const { top, left } = conteneur.getBoundingClientRect()
  const { top: topElt, left: leftElt } = elt.getBoundingClientRect()
  return { x: leftElt - left, y: topElt - top }
}

/**
 * Retourne la position absolue du coin supérieur gauche de elt (donc par rapport au document et pas .j3pContainer), que l’élément soit positionné ou pas.
 * Attention en cas d’élément inline multiligne, c’est le coin supérieur gauche du début de l’élément, pas du rectangle englobant.
 * Utiliser plutôt directement `const { x, y } = elt.getBoundingClientRect()`
 * @deprecated
 * @param {HTMLElement|string} elt
 * @return {{}|{x: number, y: number}}
 */
export function j3pGetPos (elt) {
  if (typeof elt === 'string') elt = j3pElement(elt)
  if (!j3pIsHtmlElement(elt, true)) return {}
  const { x, y } = elt.getBoundingClientRect()
  return { x, y }
}

/**
 * Retourne la position de l’élément (utilise jQuery(elt).offset()
 * ou si c’est pas dispo ajouter les $.position() en remontant tous les parents
 * Utiliser plutôt `const { x, y } = elt.getBoundingClientRect()`
 * @deprecated
 * @param {HTMLElement} elt
 * @return {{x: number, y: number}}
 */
export function j3pGetPosition (elt) {
  let $elt = $(elt)
  try {
    return {
      x: $elt.offset().left,
      y: $elt.offset().top
    }
  } catch (error) {
    // ça plante si l’élément est en display:none (il a pas de position),
    // mais pas en visibility: hidden (car dans ce cas il n’est pas affiché mais conserve sa place et sa taille)
    console.error('$.offset() renvoie rien, on execute l’ancien code avec $.position() sur les parents', error)
  }
  let left = 0
  let top = 0
  while (elt.offsetParent) {
    // On ajoute la position de l’élément parent
    // Usage de JQUERY qui gère bien les différences entre navigos (qui renvoient des valeurs différentes dans offsetLeft et Top - inclusion des marges ou non)
    // left += elt.offsetLeft + (elt.clientLeft != null ? elt.clientLeft : 0);
    $elt = $(elt)
    left += $elt.position().left
    top += $elt.position().top
    elt = elt.offsetParent
  }

  return {
    x: left,
    y: top
  }
}

/**
 * Retourne le décalage de elt par rapport à ref (tableau [x, y])
 * @param {HTMLElement} elt
 * @param {HTMLElement|Node} ref
 * @return {number[]}
 */
export function j3pGetPositionZone (elt, ref) {
  const o1 = j3pGetPosition(elt)
  const o2 = j3pGetPosition(ref)
  return [o1.x - o2.x, o1.y - o2.y]
}

/**
 * Affiche un trait en diagonale sur l’élément
 * Attention, le parent
 * @param {HTMLElement|string} elt L’élement à barrer (ou son id)
 * @return {SVGElement} Le svg mis dans le parent de l’élément
 */
export function j3pBarre (elt) {
  let id
  if (typeof elt === 'string') {
    id = elt
    elt = j3pElement(elt)
  }
  if (!elt) return console.error(Error('Élément invalide'))
  elt.setAttribute('data-barre', 'yes') // pour j3pDesactive
  const long1 = elt.offsetWidth
  const larg1 = elt.offsetHeight
  let parent = elt
  // on cherche le premier parent div ou span ou table ou p
  while (parent.parentNode && !['div', 'p', 'span', 'td'].includes(parent.tagName?.toLowerCase())) {
    parent = parent.parentNode
  }

  // si le parent, par rapport auquel sera calculée la position de la barre n’a pas de position,
  // le positionnement de la barre ne fonctionnera pas
  if (!parent.style.position) {
    // on le signale en console pour éviter au dev qq arrachages de cheveux
    console.info('j3pBarre ajoute du position: relative sur cet elt', parent)
    parent.style.position = 'relative'
  }

  const [left, top] = j3pGetPositionZone(elt, parent)
  let props = {
    style: {
      position: 'absolute',
      left,
      top,
      boxShadow: '0 0 0'
    }
  }
  // id inutile, mais c'était là avant, donc on le laisse au cas où certains l’utiliseraient
  const divId = id ? id + 'zonesvg' : ''
  const div = j3pAjouteDiv(parent, divId, '', props)
  const svg = j3pCreeSVG(div, {
    width: long1,
    height: larg1,
    style: {
      position: 'relative'
    }
  })
  props = {
    x1: 0,
    y1: 0,
    x2: long1,
    y2: larg1,
    couleur: '#000000',
    epaisseur: 2,
    opacite: 0.5
  }
  if (id) props.id = 'lignebarreesvg' + id + 'zonesvg' // inutile, mais c'était là avant, donc on le laisse au cas où certains l’utiliseraient
  j3pCreeSegment(svg, props)
  // Ceci va empêcher tout modif du contenu de elt (y compris dans les outils de développement du navigo)
  // Ajouté le 18/06/2021 (Rémi)
  j3pFreezeElt(elt)
  return svg
} // j3pBarre

/**
 * Retourne un tableau avec tous les tableaux passés en arguments (autant qu’on veut) dont tous les éléments auront été mélangés de la même manière
 * j3pShuffleMulti([1, 2, 3], ['a', 'b', 'c'], ['un', 'deux', 'trois']) retournera par ex
 * [[3, 0, 1], ['c', 'a', 'b'], ['trois', 'un', 'deux']]
 * @param {...Array<*>} tabs
 * @return {Array<Array<*>>}
 * @throw {Error} si les tableaux passés en argument ne sont pas tous de même taille
 */
export function j3pShuffleMulti (...tabs) {
  if (tabs.length < 2 || !tabs.every(tab => Array.isArray(tab) && tab.length === tabs[0].length)) {
    console.error('j3pShuffleMulti reçoit les arguments', tabs)
    throw Error('arguments invalides')
  }
  // le cas où y’a rien à faire
  if (!tabs[0].length) return tabs

  // le tableau de tableaux que l’on va retourner, tous vides pour le moment
  const resultTabs = tabs.map(() => [])
  // on clone la liste de tableaux reçue (on pourra virer des éléments de chaque tabsTmp sans modifier les tableaux de tabs)
  const tmpTabs = tabs.map(tab => tab.slice())
  while (tmpTabs[0].length) {
    const indiceMax = tmpTabs[0].length - 1
    // on tire un indice au hasard
    const indiceRandom = indiceMax > 0 ? j3pGetRandomInt(0, indiceMax) : 0
    tmpTabs.forEach((tmpTab, tabIndex) => {
      // on enlève de chaque tableau temporaire l’élément ayant cet indice
      const elt = tmpTab.splice(indiceRandom, 1)[0]
      // que l’on ajoute dans le tableau retourné
      resultTabs[tabIndex].push(elt)
    })
  }
  return resultTabs
}

/**
 * Retourne une valeur choisie alétoirement dans tab2
 * tab1[i] est la probabilité d’obtenir aléatoirement tab2[i]
 * @param tab1 tableau des choix aléatoires proposés
 * @param tab2 tableau de flottants compris entre 0 et 1 dont la somme vaut 1 (de la même longueur que tab1). C’est la proba de tomber sur la valeur associée de tab1
 */
export function j3pRandomTab (tab1, tab2) {
  const cumul = []
  cumul[0] = Math.round(1e9 * tab2[0] * 100) / 1e9
  for (let k = 1; k < tab2.length; k++) {
    cumul[k] = Math.round(1e9 * (cumul[k - 1] + tab2[k] * 100)) / 1e9
  }
  if (cumul[tab2.length - 1] !== 100) {
    return 'erreur. Total différent de 100'
  }
  const tirage = j3pGetRandomInt(1, 99)
  let k = 0
  while (cumul[k] < tirage) k++
  return tab1[k]
}

/**
 * Retourne la position du conteneur j3p (Mepact)
 * @return {{x: number, y: number}}
 */
export function j3pC () {
  return j3pGetPosition(j3pElement('Mepact'))
}

/**
 * Retourne la string du monome correctement formatée (séparateur virgule)
 * j3pMonome(1, 2, 3, 'y') => 3y^2
 * j3pMonome(2, 3, 4) => +4x^3
 * @param {number} rang Si 1 alors le signe + éventuel sera omis (sinon laissé)
 * @param {number} puissance
 * @param {number|string} coef Nombre entier ou décimal (utiliser j3pLatexMonome sinon)
 * @param {string} [nomVar=x]
 * @return {string}
 */
export function j3pMonome (rang, puissance, coef, nomVar = 'x') {
  const rangN = typeof rang === 'string' ? Number(rang) : rang
  if (!Number.isInteger(rangN)) throw Error(`rang ${rang} invalide`)
  const isFirst = rangN === 1
  const puissN = typeof puissance === 'string' ? Number(puissance) : puissance
  if (!Number.isFinite(puissance)) throw Error(`puissance ${puissance} invalide`)
  if (!Number.isInteger(puissance)) console.error(Error('puissance non entière (ça va fonctionner mais ce n\'est probablement pas voulu)'))
  const coefN = typeof coef === 'string'
    ? j3pNombre(coef)
    : coef
  if (!Number.isFinite(coefN)) {
    throw Error(`coef ${coef} invalide`)
  }
  if (coefN === 0) return ''

  const varPuiss = puissN === 0
    ? ''
    : puissN === 1
      ? nomVar
      // faut des accolades autour de la puissance si elle fait plus d'un caractère
      : String(puissN).length > 1
        ? `${nomVar}^{${puissN}}`
        : `${nomVar}^${puissN}`
  let monome
  switch (coefN) {
    case 1:
      monome = varPuiss || '1'
      break
    case -1:
      monome = '-' + (varPuiss || '1')
      break
    default:
      monome = j3pVirgule(coefN) + varPuiss
  }
  if (!isFirst && coefN > 0) return `+${monome}`
  return monome
} // j3pMonome

/**
 * Retourne le polynome en string (termes formatés par j3pMonome), par exemple 5x^{12}+4x^2-3x+5 pour le tableau [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -3, 5]
 * @param {number[]} coefs
 * @param {string} [nomVar=x]
 * @return {string}
 * @throws {Error} Si coefs n’est pas un tableau de number
 */
export function j3pPolynome (coefs, nomVar = 'x') {
  if (!Array.isArray(coefs)) throw Error('Coefficients invalides (pas un tableau)')
  if (!coefs.every(coef => Number.isFinite(coef))) throw Error('Les coefficients doivent tous être des nombres')
  const degreMax = coefs.length - 1
  return coefs.map((coef, i) => j3pMonome(i + 1, degreMax - i, coef, nomVar)).join('')
}

/**
 * Active un input standard
 * ATTENTION, j3pDesactive gère les champs mathquill mais pas cette fct
 * @param {string} id
 */
export function j3pActive (id) {
  j3pElement(id).disabled = false
}

/**
 * Retourne la string sous forme de number, en remplaçant la virgule éventuelle
 * Retourne NaN en cas de param incorrect (pas un number ni une string numérique) ou string vide
 * Attention, une string vide retourne 0
 * @param {string|number} ch
 * @return {number}
 */
export function j3pNombre (ch) {
  if (typeof ch === 'number') return ch
  if (typeof ch === 'string') return Number(ch.trim().replace(',', '.'))
  console.error(Error('paramètre invalide'), ch)
  return Number.NaN
}

/**
 * Retourne le nombre en string avec séparateur virgule.
 * À utiliser avant affichage pour éviter les pb d’arrondi style 1.99999999999999 (en précisant un maxDecimales entre 0 et 12)
 * @param {number|string} x
 * @param {number} [maxDecimales] si précisé, on arrondi à ce max de décimale (sans les 0 non significatifs)
 * @return {string}
 */
export function j3pVirgule (x, maxDecimales) {
  let nb, nbStr
  if (typeof x === 'string') {
    nb = j3pNombre(x) // il pourrait y avoir déjà une virgule dedans
    if (!Number.isFinite(nb)) {
      console.error(Error(`paramètre invalide, j3pVirgule veut un nombre fini et on lui a passé ${x}`))
      // on remet le comportement précédent, en attendant de corriger tous ceux qui provoquent ce cas
      return x.replace('.', ',')
    }
  } else if (typeof x === 'number') {
    // en cas de nombre foireux on le signale mais on laisse continuer comme avant (ça retournera 'Infinite' ou '-Infinite' ou 'NaN')
    if (!Number.isFinite(x)) console.error(Error(`Il faut passer un nombre fini : ${x}`))
    nb = x
  } else {
    console.error(Error(`paramètre invalide (${typeof x} ${x})`))
    return ''
  }
  // nb est un number
  if (Number.isInteger(maxDecimales) && maxDecimales >= 0) {
    if (maxDecimales === 0) return String(Math.round(nb))
    nbStr = nb.toFixed(maxDecimales)
  } else {
    nbStr = nb.toPrecision(15)
  }
  // nbStr est un nb en string avec séparateur décimal point "presque" toujours présent (sauf si y’avait pile 15 chiffres)
  if (!nbStr.includes('.')) return nbStr
  return nbStr
    // vire les 0 non significatifs (mis par toFixed ou toPrecision)
    // (y’a toujours le point, pas de risque de virer un 0 des unités)
    .replace(/0+(e[+-][0-9]+)?$/, '$1')
    .replace(/\.$/, '') // vire le . s’il n’y a plus de décimales
    .replace('.', ',') // met le séparateur virgule
}

/**
 * Donne le focus à l’élément (idem elt.focus(), sauf pour les input mathquill ou on fait du $(…).mathquill('focus'))
 * La détection mq ou pas est plus fiable avec un HTMLElement (on regarde sa classe css) qu’avec l’id
 * (on regarde si son id contient inputmq, donc ça ne fonctionne que pour les champs créés par j3pAffiche)
 * @param {HTMLElement|string} elt
 */
export function j3pFocus (elt) {
  // pour debug on garde ça, car ça change le comportement sur tablette si on affiche ça à l’écran avec la fct addConsoleOnScreen
  // si on affiche l’erreur de la ligne suivante ça fait apparaître le clavier virtuel de l’iPad, si on commente la ligne ça le fait plus
  // console.error(Error('fake Error j3pFocus'))
  try {
    if (typeof elt === 'string') elt = j3pElement(elt)
    if (!j3pIsHtmlElement(elt, true)) return
    if (elt.classList.contains('mq-editable-field')) {
      const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0// Attention c-dessous elt.isFocusedByTouch peut être undefined
      // const isTouchDevice = false
      if (elt.virtualKeyboard && isTouchDevice) elt.virtualKeyboard.show()
      const $elt = $(elt)
      // Le blur ci-dessous semble indispensable depuis les modifs faites par Yves pour gérer le focus
      // on teste pas l'existence de $.fn.mathquill car c'est forcément le cas avec des champs mq-editable-field
      $elt.mathquill('blur')
      setTimeout(function () {
        $elt.mathquill('focus')
      })
      // y’a un truc async dans la propagation du focus, on vérifie avec un setTimeout
      // (dans de nombreux cas il fallait passer 2× avant de mettre ce setTimeout, mais pas toujours,
      // depuis qu’il y est y’a apparemment plus trop besoin de le refaire une 2e fois)
    } else {
      elt.focus()
    }
  } catch (error) {
    console.error(error)
  }
}

export function j3pSupprimeEspaces (ch) {
  while (ch.includes(' ')) {
    const pos = ch.indexOf(' ')
    ch = ch.substring(0, pos) + ch.substring(pos + 1)
  }
  return ch
}

/**
 * Affiche input + curseur, bouger le curseur modifie le nombre de l’input
 * @param {string|HTMLElement} conteneur (que l’on va vider pour le remplacer par nos inputs)
 * @param {object} options
 * @param {string} options.label Le texte à afficher à gauche de l’input texte
 * @param {number} options.min valeur minimale du curseur
 * @param {number} options.max valeur max du curseur
 * @param {number} options.valeur valeur initiale du curseur
 * @param {number} [options.nbDecimales=0] nb de décimales pour arrondir le nombre dans l’input text
 * @param {number} [options.width=200] largeur du curseur (et du div englobant les inputs)
 * @return {HTMLElement} L’input curseur (sa value est un number)
 */
export function j3pCurseur (conteneur, options) {
  if (typeof conteneur === 'string') conteneur = j3pElement(conteneur)
  if (!j3pIsHtmlElement(conteneur, true)) return
  const min = (typeof options.min === 'number') ? options.min : Number(options.min)
  const max = (typeof options.max === 'number') ? options.max : Number(options.max)
  const valeur = (typeof options.valeur === 'number') ? options.valeur : Number(options.valeur)
  if (isNaN(valeur) || isNaN(min) || isNaN(max)) throw Error('argument invalide')
  let nbDecimales = (typeof options.nbDecimales === 'number') ? options.nbDecimales : Number(options.nbDecimales || 0)
  if (nbDecimales < 0 || !Number.isInteger(nbDecimales)) {
    console.error(Error('nbDecimales invalide'))
    nbDecimales = 0
  }
  const step = Math.pow(10, -nbDecimales)
  const width = (options.width || 200) + 'px'
  const div = j3pAddElt(conteneur, 'div', options.label, { style: { width, margin: '0.5em 1em' } })
  const inputTxt = j3pAddElt(div, 'input', '', {
    type: 'text',
    value: String(valeur),
    style: { width: '40%', marginLeft: '1ch' }
  })
  const span = j3pAddElt(div, 'span')
  j3pAddElt(div, 'br')
  const curseur = j3pAddElt(div, 'input', '', { type: 'range', value: valeur, min, max, step, style: { width: '100%' } })
  inputTxt.addEventListener('change', function () {
    const v = j3pNombre(inputTxt.value)
    if (isNaN(v) || v < min || v > max) {
      const rectif = j3pVirgule(curseur.value, nbDecimales)
      span.innerText = 'Nombre ' + inputTxt.value + ' invalide => ' + rectif
      inputTxt.value = rectif
    } else {
      span.innerText = ''
      if (Math.abs(v - curseur.value) < step) return // pour éviter du ping-pong de change
      curseur.value = v
    }
  })
  curseur.addEventListener('input', function () {
    inputTxt.value = j3pVirgule(curseur.value, nbDecimales)
  })
  return curseur
}

/**
 * Retourne le nombre en string avec l’espace en séparateur de milliers
 * @param {number} nb
 * @return {string}
 */
export function j3pEntierBienEcrit (nb) {
  if (typeof nb === 'string') nb = Number(nb)
  if (!Number.isInteger(typeof nb === 'string' ? Number(nb) : nb)) {
    console.error(Error(`j3pEntierBienEcrit veut un entier, ${nb} fourni`))
    return typeof nb === 'string' ? nb : String(nb)
  }
  return j3pNombreBienEcrit(nb)
}

/**
 * Ajoute un &lt;input type="radio"> dans container, avec éventuellement un &lt;label> autour
 * Si id, alors ce sera comme avant 2020-02, un tag input avec id suivi d’un tag label (avec id='label'+id)
 * @param {HTMLElement|string} container
 * @param {string} [id]
 * @param {string} name
 * @param {string} value
 * @param {string} [label] Si non fourni on ajoutera pas de &lt;label> autour de l'&lt;input>
 * @return {HTMLElement} Le tag &lt;input> (prendre parentNode pour avoir le label)
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pBoutonRadio (container, id, name, value, label) {
  container = j3pEnsureHtmlElement(container)
  const inputProps = {
    type: 'radio',
    value,
    name
  }
  const labelProps = {
    style: {
      paddingLeft: '1ch', // pour remplacer l’espace inséré avant 2020-02
      paddingRight: '3ch' // pour remplacer les 3 &nbsp; ajoutés avant 2020-02
    }
  }
  let input
  if (id) {
    // ancien fonctionnement, <input id=…> suivi de <label for=… id=…>,
    // car certaines sections récupèrent le label d’après l’id qu’on lui fixe ici pour le modifier ensuite…
    inputProps.id = id
    input = j3pAddElt(container, 'input', '', inputProps)
    labelProps.htmlFor = id // attention, l’attribut for correspond à la propriété htmlFor
    labelProps.id = 'label' + id
    j3pAddElt(container, 'label', label, labelProps)
  } else {
    // fonctionnement plus académique avec <label> autour de l'<input>
    if (label) {
      container = j3pAddElt(container, 'label', label)
    }
    input = j3pAddElt(container, 'input', '', inputProps)
  }
  return input
}

/**
 * Retourne l’index et la valeur de l’input radio coché (pour les boutons radio de nom radioName)
 * @param {string} radioName
 * @return {Array<number, string>} un tableau [index, value] de l’élément coché (si aucun n’est coché index vaut -1 et value '')
 */
export function j3pBoutonRadioChecked (radioName) {
  const inputs = document.querySelectorAll(`input[type=radio][name=${radioName}]`)
  let index = 0
  for (const input of inputs) {
    if (input.checked) return [index, input.value]
    index++
  }
  return [-1, '']
}

export function j3pAjouteTableau (zone, objet) {
  const cible = (typeof zone === 'string') ? j3pElement(zone) : zone
  const table = document.createElement('table')
  table.setAttribute('style', 'border-width: 2px solid #5C9CCC;border-spacing: 1px;border-style: outset;border-color: gray;border-collapse: separate;background-color: white;')
  cible.appendChild(table)
  table.setAttribute('id', objet.id)
  const mef = objet.tabcss.length > 0
  const objcol = []

  for (let lig = 0; lig < objet.nbli; lig++) {
    const ligne = document.createElement('tr')
    if (mef) ligne.setAttribute('height', objet.tabcss[1][lig])
    ligne.setAttribute('align', 'center')
    ligne.setAttribute('style', 'border-width: 1px;padding: 4px;border-style: inset;border-color: gray;background-color: white;')
    for (let col = 0; col < objet.nbcol; col++) {
      let colonne
      if (lig === 0) {
        colonne = document.createElement('th')
        objcol.push(colonne)
        if (mef) colonne.setAttribute('width', objet.tabcss[0][col])
      } else {
        colonne = document.createElement('td')
      }
      colonne.setAttribute('id', objet.tabid[lig][col])
      const div = document.createElement('div')
      div.setAttribute('id', 'div' + objet.tabid[lig][col])
      colonne.appendChild(div)

      colonne.setAttribute('style', 'font-size:' + objet.taille + 'px;border-width: 1px;padding: 4px;border-style: inset;border-color: gray;background-color: white;')
      ligne.appendChild(colonne)
    }
    table.appendChild(ligne)
    for (let col = 0; col < objet.nbcol; col++) {
      objcol[col].style.fontWeight = 'normal'
    }
  }
}

/**
 * Ajoute un span dans conteneur et le retourne (wrapper de j3pAddElt qui impose span + position relative + left 0)
 * @param {HTMLElement|string} conteneur
 * @param {Object} [props] Propriété à coller sur l’élément (passé à j3pAddElt, sauf propriété contenu)
 * @param {string|HTMLElement|NodeList} [props.contenu] Du contenu à mettre dans le span
 * @param {string} [props.id] un id éventuel pour ce span
 * @return {HTMLElement|HTMLSpanElement}
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pSpan (conteneur, props) {
  conteneur = j3pEnsureHtmlElement(conteneur)
  if (typeof props === 'object') {
    props = _clonePropsCleanStyle(props, { addEmpty: true })
  } else {
    props = { style: {} }
  }
  let content = ''
  if (props.contenu) {
    content = props.contenu
    delete props.contenu
  }
  props.style.position = 'relative'
  props.style.left = 0
  return j3pAddElt(conteneur, 'span', content, props)
}

/**
 * Ajoute une image (dans un div qu’on crée) et la retourne
 * @param {object} options
 * @param {HTMLElement|string} options.conteneur conteneur ou son id
 * @param {string} [options.id] Id du div à créer dans le conteneur
 * @param {string} options.src url de l’image
 * @param {number} [options.larg] largeur à fixer à l’image
 * @return {HTMLElement} Le tag img du dom
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pImage ({ id, conteneur, src, larg }) {
  conteneur = j3pEnsureHtmlElement(conteneur)
  const div = j3pAddElt(conteneur, 'div')
  if (id) div.id = id
  return j3pAddElt(div, 'img', '', { src, width: larg + 'px' })
}

/**
 * Affiche une boule (effet ombrage 3D) dans le svg
 * @param {SVGElement} svg
 * @param {string} options.couleur peut contenir "rouge" ou "bleu" ou "vert" ou n’importe quel code en hexa (avec ou sans #) (mais "maroon", "red", etc... ne sont pas acceptés)
 * @param {number} options.cx
 * @param {number} options.cy
 * @param {number} options.diametre
 * @param {string} [options.id]
 * @returns {SVGCircleElement} la "boule" ajoutée au svg
 */
export function j3pBoule (svg, { couleur, cx, cy, diametre, id }) {
  // objet peut contenir couleur:
  function hexToR (h) {
    return parseInt((cutHex(h)).substring(0, 2), 16)
  } // fonction pour convertir le code hexa en rgb
  function hexToG (h) {
    return parseInt((cutHex(h)).substring(2, 4), 16)
  }

  function hexToB (h) {
    return parseInt((cutHex(h)).substring(4, 6), 16)
  }

  function cutHex (h) {
    return (h.charAt(0) === '#') ? h.substring(1, 7) : h
  }

  let dstop1, dstop2, dstop3, degradeboule
  if (typeof couleur !== 'undefined') {
    if (couleur === 'rouge') {
      dstop1 = '#FF4D4D'
      dstop2 = '#CC0000'
      dstop3 = '#550000'
      degradeboule = 'degradeboulerouge'
    } else if (couleur === 'bleu') {
      dstop1 = '#8888DD'
      dstop2 = '#0000CC'
      dstop3 = '#000033'
      degradeboule = 'degradeboulebleu'
    } else if (couleur === 'vert') {
      dstop1 = '#00B300'
      dstop2 = '#008000'
      dstop3 = '#003300'
      degradeboule = 'degradeboulevert'
    } else {
      const compRed = hexToR(couleur)
      const compGreen = hexToG(couleur)
      const compBlue = hexToB(couleur)
      dstop1 = 'rgb(' + Math.max(125, compRed * 2) + ',' + Math.max(125, compGreen * 2) + ',' + Math.max(125, compBlue * 2) + ')' // clair
      dstop2 = 'rgb(' + compRed + ',' + compGreen + ',' + compBlue + ')' // correspond à couleur si couleur possède un # au début
      dstop3 = 'rgb(' + Math.floor(compRed / 3) + ',' + Math.floor(compGreen / 3) + ',' + Math.floor(compBlue / 3) + ')' // foncé
      degradeboule = 'degradeboule' + cutHex(couleur)
    }
  } else {
    dstop1 = '#FF4D4D'
    dstop2 = '#CC0000'
    dstop3 = '#550000'
    degradeboule = 'degradeboulerouge'
  }
  const urldegradeboule = 'url(#' + degradeboule + ')'
  // var defs = document.createElementNS ("http://www.w3.org/2000/svg", "defs");
  const defs = (document.getElementsByTagName('defs').length === 0) ? document.createElementNS('http://www.w3.org/2000/svg', 'defs') : document.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'defs')[0]
  let grad = j3pElement(degradeboule, null)
  if ((!grad) || (grad.nodeName !== 'radialGradient')) {
    grad = document.createElementNS('http://www.w3.org/2000/svg', 'radialGradient')
    grad.setAttributeNS(null, 'id', degradeboule)
    grad.setAttributeNS(null, 'cx', '0')
    grad.setAttributeNS(null, 'cy', '0')
    grad.setAttributeNS(null, 'r', '100%')
    const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
    stop1.setAttributeNS(null, 'offset', '20%')
    stop1.setAttributeNS(null, 'stop-color', dstop1)// FF4D4D
    const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
    stop2.setAttributeNS(null, 'offset', '70%')
    stop2.setAttributeNS(null, 'stop-color', dstop2)// CC0000
    const stop3 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
    stop3.setAttributeNS(null, 'offset', '100%')
    stop3.setAttributeNS(null, 'stop-color', dstop3)// 550000
    grad.appendChild(stop1)
    grad.appendChild(stop2)
    grad.appendChild(stop3)
    defs.appendChild(grad)
    svg.appendChild(defs)
  }
  const boule = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
  boule.setAttribute('id', id)
  boule.setAttribute('cx', cx)
  boule.setAttribute('cy', cy)

  boule.setAttribute('style', 'stroke-width:0;stroke:maroon;fill:' + urldegradeboule + ';')
  boule.setAttribute('r', diametre / 2)
  svg.appendChild(boule)
  return boule
} // j3pBoule

/**
 * Retourne nb arrondi pour garder 13 chiffres significatifs ?
 * Description à confirmer ! (phrase ci-dessous incompréhensible)
 * Sert surtout à gérer les décimaux avec les pbs d’arrondis de js, elle laisse la valeur décimale telle que calculée
 * @param {number} nb
 * @returns {number}
 */
export function j3pMeilleurArrondi (nb) {
  if (Number.isInteger(nb)) return nb
  let puisPrecision = 14
  if (Math.abs(nb) > 1e-13) {
    const ordrePuissance = Math.ceil(Math.log(Math.abs(nb)) / Math.log(10))
    puisPrecision -= ordrePuissance
  }
  // pour 2000 ordrePuissance vaut 4 et puisPrecision vaut 10
  // si l’arrondi d’ordre 10 est < 1e-10, alors on arrondi à 1e-9 près, sinon on retourne le nombre
  const rounded = (n, ordre) => Math.round(n * Math.pow(10, ordre)) / Math.pow(10, ordre)
  return Math.abs(rounded(nb, puisPrecision) - nb) < Math.pow(10, -puisPrecision)
    ? rounded(nb, puisPrecision - 1)
    : nb
}

export function j3pAjouteFoisDevantX (texte, variable) {
  let newTexte
  const signeRegExp = /[-+*/(^]/i
  if (texte === '') {
    newTexte = ''
  } else {
    newTexte = texte.charAt(0)
    for (let pos = 1; pos < texte.length; pos++) {
      if (texte.charAt(pos) === variable) {
        if (signeRegExp.test(texte.charAt(pos - 1))) {
          // j’ai un signe devant la variable, donc je n’ajoute rien
          newTexte += texte.charAt(pos)
        } else {
          // je mets un signe de mult
          newTexte += '*' + texte.charAt(pos)
        }
      } else {
        // je mets un signe de mult
        newTexte += texte.charAt(pos)
      }
    }
  }
  return newTexte
}

export function j3pMelangeitemliste (tab) {
  // la fonction renvoie un tableau contenant 2 tableaux : le premier est le tableau mélangé
  // le second est la position de ième élément de tab dans le tableau mélangé
  // tab est le tableau initial
  // tab[0] est vide
  // tab[1] est la bonne réponse
  const newtab = []
  newtab[0] = ['']
  newtab[1] = [0]
  const tab2 = tab.slice(1, tab.length)
  const lgTab2 = tab2.length
  for (let i = 0; i < lgTab2; i++) {
    const pioche = Math.floor(Math.random() * tab2.length)
    newtab[0].push(tab2[pioche])
    tab2.splice(pioche, 1)
  }
  for (let i = 1; i < tab.length; i++) {
    newtab[1].push(newtab[0].indexOf(tab[i]))
  }
  return newtab
}

export function j3pExtraireCoefsFctAffine (fctAffine, variable) {
  // on donne une fonction affine : fctAffine
  // on revoie un tableau de 2 éléments : coefA et coefB de l’écriture ax+b au format latex
  // variable est optionnel (par défaut x)
  function enleveParenthese (express) {
    // express est une fonction affine
    // je dois gérer la présence de parenthèses sous la forme (ax)+(b) et enlever ces parenthèses inutiles
    // cast en string au cas où ce serait un nombre
    express = String(express).replace(/\\(left|right)/g, '')
    while (express.includes('(')) {
      let posParOuvrante = express.indexOf('(')
      let i = posParOuvrante + 1
      let posParFermante
      while (i < express.length) {
        if (express[i] === '(') {
          posParOuvrante = i
        } else if (express[i] === ')') {
          posParFermante = i
          i = express.length
        } else {
          i += 1
        }
      }
      if (!posParFermante) {
        console.error(Error(`expression avec parenthèse ouvrante sans fermante : ${express}`))
        return express
      }
      const bloc = express.substring(posParOuvrante, posParFermante + 1)
      // je regarde le signe de l’expression dans la parenthèse et celui qui est avant
      if (posParOuvrante === 0) {
        // il n’y a donc pas de signe devant. On vire juste les parenthèses
        express = express.replace(bloc, bloc.substring(1, bloc.length - 1))
      } else {
        const signeAvant = express.charAt(posParOuvrante - 1)
        const carApres = express.charAt(posParOuvrante + 1)
        if (signeAvant === '-') {
          express = (carApres === '-')
            ? express.replace('-' + bloc, '+' + bloc.substring(2, bloc.length - 1))
            : (carApres === '+')
                ? express.replace('-' + bloc, '-' + bloc.substring(2, bloc.length - 1))
                : express.replace('-' + bloc, '-' + bloc.substring(1, bloc.length - 1))
        } else if (signeAvant === '+') {
          express = (carApres === '-')
            ? express.replace('+' + bloc, '-' + bloc.substring(2, bloc.length - 1))
            : (carApres === '+')
                ? express.replace('+' + bloc, '+' + bloc.substring(2, bloc.length - 1))
                : express.replace('+' + bloc, '+' + bloc.substring(1, bloc.length - 1))
        } else {
          express = express.replace(bloc, bloc.substring(1, bloc.length - 1))
        }
      }
    }
    return express
  }

  const nomVar = variable || 'x'
  let coefA, coefB
  if ((fctAffine[0] === '(') && (fctAffine[fctAffine.length - 1] === ')')) {
    fctAffine = fctAffine.substring(1, fctAffine.length - 1)
  }
  if (!fctAffine.substring(1).includes('+') && !fctAffine.substring(1).includes('-')) {
    // c’est qu’il n’y a qu’un seul terme dans la fonction affine
    if (!fctAffine.includes(nomVar)) {
      // c’est une fonction constante
      coefA = '0'
      coefB = fctAffine
    } else {
      // c’est une fonction linéaire
      coefA = fctAffine.substring(0, fctAffine.length - nomVar.length)
      coefA = (coefA === '')
        ? '1'
        : (coefA === '-')
            ? '-1'
            : coefA
      coefB = '0'
    }
  } else {
    fctAffine = enleveParenthese(fctAffine)
    const posDecoup = Math.max(fctAffine.lastIndexOf('+'), fctAffine.lastIndexOf('-'))// cela me donne la position du signe entre b et ax
    if (posDecoup <= 0) {
      // c’est que c’est de la forme ax donc b vaut 0'
      coefA = fctAffine.substring(0, fctAffine.length - 1) || 1
      if (coefA === '-') coefA = '-1'
      coefB = '0'
    } else {
      if (fctAffine.substring(fctAffine.length - nomVar.length) === nomVar) {
        // fctAffine est écrit sous la forme b+ax
        coefB = fctAffine.substring(0, posDecoup)
        coefA = (fctAffine[posDecoup] === '-') ? fctAffine.substring(posDecoup, fctAffine.length - nomVar.length) : fctAffine.substring(posDecoup + 1, fctAffine.length - nomVar.length)
        coefA = (coefA === '') ? '1' : (coefA === '-') ? '-1' : coefA
      } else {
        // fctAffine est écrit sous la forme ax+b
        coefA = fctAffine.substring(0, posDecoup - nomVar.length)
        coefB = (fctAffine[posDecoup] === '-') ? fctAffine.substring(posDecoup) : fctAffine.substring(posDecoup + 1)
        coefA = (coefA === '') ? '1' : (coefA === '-') ? '-1' : coefA
      }
    }
  }
  // si on avait a*x au lieu de ax, il faut se débarrasser du signe de multiplication
  if (coefA.charAt(coefA.length - 1) === '*') {
    coefA = coefA.substring(0, coefA.length - 1)
  }
  coefA = j3pFractionLatex(coefA)
  coefB = j3pFractionLatex(coefB)
  return [coefA, coefB]
}

export function j3pFractionLatex (texte) {
  // texte est une expression dans laquelle on peut retrouver une fraction sous la forme .../... ou (.../...)
  // cette fonction renvoie le texte au format latex
  const fractReg1 = /\([0-9+\-*a-zA-Z]+\)\/\([0-9+\-*a-zA-Z]+\)/g // on cherche (...)/(...)
  const fractReg2 = /\([0-9*a-zA-Z]+\/[0-9*a-zA-Z]+\)/g // on cherche (.../...)
  let texteLatex = texte
  if (fractReg1.test(texteLatex)) {
    // l’expression (...)/(...) est présente'
    const tabFract1 = texteLatex.match(fractReg1)
    for (let i = 0; i < tabFract1.length; i++) {
      const tabNumDen = tabFract1[i].split('/')
      const num = (tabNumDen[0][tabNumDen[0].length - 1] === ')') ? tabNumDen[0].substring(1, tabNumDen[0].length - 1) : tabNumDen[0].substring(1, tabNumDen[0].length)
      const den = (tabNumDen[1][0] === '(') ? tabNumDen[1].substring(1, tabNumDen[1].length - 1) : tabNumDen[1].substring(0, tabNumDen[1].length - 1)
      texteLatex = texteLatex.replace(tabFract1[i], '\\frac{' + num + '}{' + den + '}')
    }
  }
  if (fractReg2.test(texteLatex)) {
    // l’expression (...)/(...) est présente'
    const tabFract2 = texteLatex.match(fractReg2)
    for (let i = 0; i < tabFract2.length; i++) {
      const tabNumDen = tabFract2[i].split('/')
      const num = tabNumDen[0].substring(1)
      const den = tabNumDen[1].substring(0, tabNumDen[1].length - 1)
      texteLatex = texteLatex.replace(tabFract2[i], '\\frac{' + num + '}{' + den + '}')
    }
  }
  if (String(texteLatex).indexOf('/') > -1) {
    // il reste encore une fraction à gérer à la main, sans doute du style .../...
    const signeReg = /[+\-()*]+/g
    const tabSansSigne = texteLatex.split(signeReg)
    for (let i = 0; i < tabSansSigne.length; i++) {
      if (tabSansSigne[i].indexOf('/') > -1) {
        const newTab = tabSansSigne[i].split('/')
        texteLatex = texteLatex.replace(newTab[0] + '/' + newTab[1], '\\frac{' + newTab[0] + '}{' + newTab[1] + '}')
      }
    }
  }
  return texteLatex
}

/**
 * Récupère la baseUrl d’une sesathèque
 * @private
 * @param {string} [sesathequeBaseId] Si non fourni on prendra sesabibli|sesabidev (initialisé dans j3p. par outils/methodesmodele.js)
 * @return {string|undefined} baseUrl ou undefined si sesathequeBaseId était inconnue
 * @throws {Error} Si sesathequeBaseId n’est pas fourni et qu’on a pas trouvé la baseUrl de la sésathèque par défaut
 */
function getBibliBaseUrl (sesathequeBaseId) {
  const baseId = sesathequeBaseId || 'sesabibli'
  const biblis = getBibliConnues()
  const baseUrl = biblis[baseId]
  // si on avait pas précisé d’argument, on veut une url, on throw ici si y’en a pas
  if (!sesathequeBaseId && !baseUrl) {
    const error = Error('Pas d’url trouvée pour la sésathèque ' + baseId)
    j3pShowError(error)
    throw (error)
  }
  // sinon on retourne éventuellement undefined
  return baseUrl
}

/**
 * Retourne les sésatheques courramment utilisées (seules celle de prod sont connues sur le client
 * mais l’appel de stclient ajoutera celle dont on a besoin si nécessaire)
 * @private
 */
function getBibliConnues () {
  return {
    // on essaie de conserver le même nb de lettre suivant local/dev/prod
    // pour qu'une réécriture dans des js ne modifie pas le mapping
    sesabibli: 'https://bibliotheque.sesamath.net/',
    sesabidev: 'https://bibliotheque.sesamath.dev/',
    bibliloca: 'https://bibliotheque.sesamath.local/',
    sesacommun: 'https://commun.sesamath.net/',
    sesacomdev: 'https://commun.sesamath.dev/',
    communloca: 'https://commun.sesamath.local/'
  }
}

/**
 * Retourne un client sur la bibli
 * Cf https://bibliotheque.sesamath.dev/doc/modules/sesatheque-client/module-sesatheque-client.html
 * @private
 * @param {string} sesathequeBaseId Le baseID de la bibliothèque (à priori sesabibli, sinon sesabidev)
 * @param {function} next appelée avec (error, client)
 */
const getClientBibli = (function () {
  /**
   * Le client sur la sesatheque que l’on utilise (qui peut aller chercher des ressources sur
   * toutes les sésathèques déclarées à l’init, celles de getBibliConnues)
   * @private
   */
  let bibliClient

  // une closure pour garder nos variables privées (en attendant que j3p utilise des modules js)
  return function getClientBibli (next) {
    if (bibliClient) return next(null, bibliClient)
    const baseUrl = getBibliBaseUrl()
    loadJs(baseUrl + 'client.js')
      .then(function () {
        // on devrait avoir un stclient en global
        if (typeof window.stclient === 'object' && typeof window.stclient.default === 'function') {
          window.stclient = window.stclient.default
        }
        if (typeof window.stclient !== 'function') return next(new Error('Le chargement du client de la Sesathèque a échoué'))
        // Il faut passer d’objet en tableau
        const sesathequesObj = getBibliConnues()
        const sesatheques = []
        let sesatheque
        for (const prop in sesathequesObj) {
          if (hasProp(sesathequesObj, prop)) {
            sesatheque = {
              baseId: prop,
              baseUrl: sesathequesObj[prop]
            }
            sesatheques.push(sesatheque)
          }
        }
        bibliClient = window.stclient(sesatheques, window.location.origin)
        next(null, bibliClient)
      })
      .catch(next)
  }
})()

/**
 * Récupère une ressource sur la bibliothèque et la passe à next
 * @param {string}   id      sous la forme em/42 par ex
 * @param {string}   [sesathequeBaseId] L’id de la sésatheque
 * @param {function} next    sera appelé avec (error, ressource)
 */
export function j3pGetRessourceBibli (id, sesathequeBaseId, next) {
  getClientBibli(function (error, client) {
    if (error) return j3pShowError(error, { message: 'Impossible de récupérer un client pour appeler une ressource sur la bibliothèque' })
    // on passe un rid, sous la forme baseId/oid
    client.getRessource(sesathequeBaseId + '/' + id, next)
  })
}

/**
 * Affiche l’exo puis appelle next (avec une erreur ou sans argument)
 * On lui passera la callback traite_resultat pour gérer les retours du flash
 * @param {Ressource} ressource La ressource de la bibli
 *                              (objet https://bibliotheque.sesamath.net/vendors/sesamath/Ressource.js)
 * @param {object}   options L'objet qui sera passé au display de la sésathèque
 * @param {string}   [options.sesathequeBaseId]
 * @param {function} next       appelé à la fin de l’affichage (tout a été mis dans le dom mais le rendu peut ne pas être terminé),
 *                              avec une erreur ou rien
 */
export function j3pAfficheBibli (ressource, options, next) {
  // pour ne pas oublier de traiter l’erreur
  if (next.length !== 1) {
    console.error('La callback d’affichage doit prendre un paramètre, l’erreur éventuelle')
  }
  const baseId = options.sesathequeBaseId
  // va charger le module display et lui passer les infos, en fixant qq valeurs imposées
  if (!options) options = {}
  options.base = getBibliBaseUrl(baseId)

  // pour rendre display et ses modules bavard en console
  options.verbose = true
  if (!options.container && options.containerId) {
    options.container = j3pElement(options.containerId)
    delete options.containerId
  }
  if (!options.container) return j3pShowError('Impossible de charger une ressource de la bibliothèque sans préciser de conteneur')

  if (!options.errorsContainer && options.errorsContainerId) {
    options.errorsContainer = document.getElementById(options.errorsContainerId)
    delete options.errorsContainerId
  }

  // à priori pas la peine d’aller chercher un 2e display si on en a déjà un
  // mais ça demande que les 2 biblis aient les mêmes plugins installés
  // cf rev 8492 pour une version où on renomme les stdisplay pour leur éviter de se crêper le chignon
  // depuis 2025-05-06 c'est le preloader stdisplay qui gère ses ≠ versions
  if (typeof window.stdisplay === 'function') {
    window.stdisplay(ressource, options, next)
  } else {
    loadJs(options.base + 'display.js')
      .then(() => {
        if (typeof window.stdisplay !== 'function') {
          return next(Error('Le chargement du display de la Sesathèque a échoué'))
        }
        window.stdisplay(ressource, options, next)
      }).catch(next)
  }
}

/**
 * Retourne la valeurs du paramètre d’url demandé s’il existe, ou tous si pas précisé (décodés)
 *
 * Le caractère "+" est conservé (alors qu’il devrait être interprété comme espace)
 *
 * Attention, les tableaux ne sont pas interprétés, avec ?foo[bar][2]=b%20az&… dans l’url
 *   j3pGetUrlParams("foo[bar][2]") retourne "b az"
 *   j3pGetUrlParams("foo[bar]") retourne undefined
 *
 * @param {string} [name] Le paramètre voulu (sinon tous)
 * @returns {string|object} La valeur du paramètre (undefined s’il n’existait pas, chaine vide s’il existe sans valeur),
 *                         ou objet avec tous les paramètres (les valeurs de chaque propriété sont toutes des strings)
 */
export function j3pGetUrlParams (name) {
  const queryString = location.search.substring(1)
  const pattern = /([^&=]+)(=[^&]*)?/g
  // graphe=[1,"modele",[{pe:">=0",nn:"1",conclusion:"Suite"}]];[2,"modele",[{pe:">=0",nn:"fin",conclusion:"FIN"}]];
  let result, cle, valeur
  let retour = name ? undefined : {}
  if (queryString) {
    while (result = pattern.exec(queryString)) { // eslint-disable-line no-cond-assign
      cle = decodeURIComponent(result[1]) // Il faudrait être tordu pour passer des noms non ascii mais on sait jamais
      valeur = result[2] ? decodeURIComponent(result[2].substr(1)) : ''
      if (name) {
        if (cle === name) {
          retour = valeur
          break
        }
      } else {
        retour[cle] = valeur
      }
    }
  }

  return retour
}

/**
 * fonction permettant de tester si un nb &lt;10^6 est premier ou non
 * Attention, ça renvoie false pour 1 et tous les nombres négatifs ou non entiers
 * @param {string} num Le nombre à tester
 * @return {boolean|undefined} true s’il est premier, undefined si > 10^6, false si < 2 ou non entier
 */
export function j3pEstPremier (num) {
  const nb = (typeof num !== 'number') ? Number(num) : num
  if (Number.isNaN(nb)) {
    console.error(Error('nombre invalide ' + num))
    return false
  }
  if (nb < 2) return false
  if (nb > 1000000) {
    console.error(Error('Le nombre ' + nb + ' est trop grand pour savoir s’il est premier avec cette fonction'))
    return // on renvoie undefined
  }
  const liste = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997, 1009]
  for (let i = 0; i < liste.length; i++) {
    if (nb === liste[i]) return true
    if (nb % liste[i] === 0) return false
    if (nb < liste[i]) return true
  }
  // on laisse undefined (si jamais on passait là ce serait encore le plus juste, mais
  // ça ne devrait jamais arriver avec 1009 en dernier dans le tableau et la
  // limite à 1e6 mise avant)
}

/**
 * Affiche un message d’erreur à l’écran (dans le conteneur .j3pErrors mis par le loader, en attendant d’en avoir un par instance)
 * Attention, à priori c’est dans MepMG, donc si un autre bout de code le vide
 * l’affichage de l’erreur va disparaître au passage
 * Cliquer sur l’erreur la fait disparaître (ça peut être au dessus de l’énoncé)
 * @param {Error|string} error
 * @param {Object} [options]
 * @param {string} [options.message] Si fourni, c’est ce qui sera affiché à l’écran (error.message sinon)
 * @param {boolean} [options.mustNotify=false] Si true l’erreur sera aussi notifiée à bugsnag
 * @param {Object} [options.notifyData] Si c’est un objet on l’enverra dans la notif bugsnag (remplace alors mustNotify=true)
 * @param {number} [options.vanishAfter] Pour que le message disparaisse tout seul, passer ici un délai en secondes (sinon il faut cliquer sur la croix)
 */
export function j3pShowError (error, { message, mustNotify, notifyData, vanishAfter } = {}) {
  if (!error) return console.error(Error('j3pShowError appelé sans erreur à afficher'))
  console.error(error)
  // pour d’éventuelles erreurs contenant une erreur d’origine
  if (error.reason) console.error('à cause de', error.reason)
  if (!message) message = error.message || error
  // on prend le premier trouvé (pour le moment y’en a qu’un)
  const errors = document.querySelector('.j3pErrors')
  if (errors) {
    let p = j3pAddElt(errors, 'p', message)
    const destroyer = () => {
      // on peut être appelé par la croix ou le setTimeout (en cas de vanishAfter)
      if (p) {
        j3pDetruit(p)
        p = null
      }
    }
    const croix = j3pAddElt(p, 'span', '', { className: 'croix' })
    croix.addEventListener('click', destroyer)
    if (typeof vanishAfter === 'number' && vanishAfter > 1 && vanishAfter < 1000) {
      setTimeout(destroyer, vanishAfter * 1000)
    }
    // si le navigateur le gère, on scroll pour que l’erreur soit visible à l’écran
    if (typeof errors.scrollIntoView === 'function') errors.scrollIntoView()
  } else {
    // eslint-disable-next-line no-alert
    alert(message) // (là on peut rien faire d’autre)
  }
  if (notifyData || mustNotify) {
    if (error instanceof UserError) return
    if (typeof error === 'string') error = Error(error) // on veut la trace
    notify(error, notifyData)
  }
}

/**
 * Ajoute une classe css à un ou des éléments désignés par selector
 * Pour l’ajouter à un elt déjà référencé, utilisez plutôt elt.classList.add(className)
 * @param {string} selector sélecteur css (préfixe #Mepact toujours ajouté) ou élément
 * @param {string} className
 * @param {HTMLElement} [parent=document] Si fourni on se limite aux enfants de cet élément
 * @return {boolean} false si y’a eu un pb, true sinon
 */
export function j3pAddClass (selector, className, parent) {
  try {
    if (typeof selector !== 'string') return console.error(Error('selecteur invalide (doit être une string)'))
    // scope #Mepact obligatoire pour ne pas modifier de style hors des contenus j3p, sauf si y’a un parent
    if (!parent) {
      parent = document
      if (!/#Mepact/.test(selector)) selector = '#Mepact ' + selector
    }
    parent.querySelectorAll(selector).forEach((elt) => {
      elt.classList.add(className)
    })
    return true
  } catch (error) {
    console.error(error)
    return false
  }
}

/**
 * Retire une classe css à un ou des éléments désignés par selector
 * Pour la retirer à un elt déjà référencé, utiliser elt.classList.remove(className)
 * @param {string} selector sélecteur css (préfixe #Mepact toujours ajouté)
 * @param {string} className
 * @param {HTMLElement} [parent=document] Si fourni on se limite aux enfants de cet élément
 * @return {boolean} false si y’a eu un pb, true sinon
 */
export function j3pRemoveClass (selector, className, parent) {
  if (!className || typeof className !== 'string') {
    console.error('paramètre className incorrect (' + (className ? typeof className : 'vide') + ')')
    return false
  }
  try {
    if (!/#Mepact/.test(selector)) selector = '#Mepact ' + selector
    ;
    (parent || document).querySelectorAll(selector).forEach(function (elt) {
      elt.classList.remove(className)
    })
    return true
  } catch (error) {
    console.error(error)
    return false
  }
}

/**
 * Ajoute un élément html dans le conteneur et retourne cet élément
 * (la syntaxe avec props en 2e arg et content en 3e fonctionne aussi)
 * À remplacer par addElement de sesajs/dom
 * @deprecated
 * @param {string|HTMLElement} container
 * @param {string} tag
 * @param {string|HTMLElement|NodeList} [content] Contenu à mettre dans l’élément
 * @param {object} [props] propriétés éventuelles à coller à l’elt créé (id, name, …)
 *        Attention, pour les styles il faut passer le nom des propriétés en camelCase (pas de -), par ex `style: { fontWeight: 'bold'}`
 *        cf https://developer.mozilla.org/fr/docs/Web/CSS/CSS_Properties_Reference
 *        Les propriétés top|left|right|bottom peuvent être passées sans unité, px sera alors ajouté
 * @return {HTMLElement}
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pAddElt (container, tag, content, props) {
  try {
    container = j3pEnsureHtmlElement(container)
    if (typeof tag !== 'string') return console.error(Error('paramètre tag invalide'), tag)
    const elt = document.createElement(tag)

    if (typeof content === 'object') {
      if (typeof props === 'string') {
        // content et props sont inversés, on gère les deux pour être compatible avec la syntaxe de sesajstools:addElement
        const contentTmp = props
        props = content
        content = contentTmp
      } else if (!props) {
        // pas de content et props en 3e param
        props = content
        content = ''
      }
      // else les deux sont object (content est HTMLElement ou NodeList) et pas commutables
    }

    // affectation des props
    if (props) j3pSetProps(elt, props)

    // Il faut le faire avant le test de content si on veut afficher un 0 comme '0'
    if (typeof content === 'number') content = String(content)
    if (content) {
      // @todo voir si pour les types autres que number|string|Array|NodeList on laisse comme ça => ça plante dans le appendChild final
      // sinon pour blinder davantage il faudrait faire un
      // if (/object HTML/.test(String(content)) => ok
      if (typeof content === 'string') {
        j3pAddContent(elt, content)
      } else {
        if (typeof content.forEach === 'function') {
          content.forEach(function (node) {
            elt.appendChild(node)
          })
        } else {
          elt.appendChild(content) // on suppose un élément seul
        }
      }
    }
    container.appendChild(elt)
    return elt
  } catch (error) {
    console.error(error)
    console.error('j3pAddElt plante avec le conteneur', container, 'le tag', tag, 'le contenu', content, 'et les propriétés', props)
    // on veut pas planter, on retourne le conteneur,
    // c’est encore le "moins pire" (si l’appelant utilise le résultat pour ajouter qqchose dedans)
    return container
  }
}

/**
 * Ajoute (ou remplace) du contenu dans un élément.
 * Remplace les \n par des tags br insérés avec du appendChild, sauf si y’a d’autres tags (dans ce cas ça fait du innerHTML, avec un warning car ça peut détruire des listeners ou des pointeurs vers les éléments existants)
 * @param {HTMLElement|string} container
 * @param {string} content
 * @param {Object} [options]
 * @param {boolean} [options.replace] passer true pour remplacer (sinon ajoute)
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pAddContent (container, content, options) {
  container = j3pEnsureHtmlElement(container)
  if (typeof content === 'number') content = String(content)
  if (typeof content !== 'string') return console.error(Error('j3pAddContent veut une string en contenu'))
  if (options && options.replace) j3pEmpty(container)
  // on remplace les <br> par des \n pour qu’ils soient gérés proprements
  if (content.includes('<')) {
    const contentWithoutBr = content.replace(/<br *\/?>/ig, '\n')
    if (contentWithoutBr.includes('<')) {
      if (contentWithoutBr.includes('$')) {
        // faut mettre les paires de $ entre des spans, sinon mathlive peut virer les espaces autours des tags html au même niveau que son expression
        content = content.replace(/(\$[^$]+?\$)/g, '<span>$1</span>')
      }
      // y’a d’autres tags, on utilise innerHTML mais on râle si y’a déjà du contenu
      if (container.innerHTML) {
        console.warn('Attention, en passant du html à j3pAddContent cela va faire du innerHTML +=, ce qui va détruire et recréer tous les childNodes, donc d’éventuelles références dessus ne pointeront plus vers des éléments du dom, et les listeners éventuels ne fonctionneront plus')
        container.innerHTML += content
      } else {
        container.innerHTML = content
      }
      return
    }
    // y’avait que des <br>, on laisse poursuivre
    content = contentWithoutBr
  }
  if (content.includes('\n')) {
    content.split('\n').forEach(function (chunk, i) {
      if (i) container.appendChild(document.createElement('br'))
      container.appendChild(_getTextNode(chunk))
    })
  } else {
    container.appendChild(_getTextNode(content))
  }
}

/**
 * Affecte les props à l’élément (en gérant les spécificités des HTMLElement, avec qq vérifs
 * Les valeurs undefined sont ignorées, les valeurs null suppriment la propriété.
 * @param elt
 * @param props
 */
export function j3pSetProps (elt, props) {
  if (!props) return
  if (typeof props !== 'object') return console.error(Error('props invalides'))
  if (typeof elt === 'string') elt = j3pElement(elt)
  if (!j3pIsHtmlElement(elt, true)) return
  Object.entries(props).forEach(([prop, value]) => {
    if (value === undefined) return
    // style peut pas être affecté directement (lecture seule), il faut affecter ses propriétés
    if (prop === 'style') {
      const clone = _clonePropsCleanStyle({ style: value })
      if (clone.style) {
        Object.entries(clone.style).forEach(([prop, value]) => {
          if ([null, undefined].includes(value)) return // on ignore
          if (['string', 'number'].includes(typeof value)) elt.style[prop] = value
          else console.warn(`type ${typeof value} incorrect pour la propriété de style ${prop} sur l’élément`, elt)
        })
      }
    } else if (prop === 'id') {
      // on passe toujours par j3pGetNewId pour ne jamais mettre dans le dom un id qui existe déjà
      // si ça n’existe pas ce sera utilisé tel quel, sinon il y aura un suffixe numérique et ça va râler en console
      elt.id = j3pGetNewId(value, true)
    } else if (prop === 'class') {
      console.warn('La propriété correspondante à l’attribut class d’un élément est className et non class (rectifié)')
      if (props.class && typeof props.class !== 'string') elt.className = props.class
      else console.error(Error('className invalide'))
    } else if (/^[oO]n/.test(prop)) {
      if (typeof value === 'function') {
        console.warn('Il faudrait utiliser addEventListener plutôt que ' + prop)
        elt[prop] = value
      } else if (typeof value === 'string') {
        console.warn(Error('Il faudrait utiliser addEventListener plutôt que ' + prop + ' et lui passer une fonction (et pas une string)'))
        elt[prop] = value
      } else {
        console.error(Error('Propriété ' + prop + ' invalide (' + typeof value + ')'))
      }
    } else if (prop === 'attrs') {
      for (const [attr, val] of Object.entries(value)) {
        elt.setAttribute(attr, val)
      }
    } else if (['name', 'title'].includes(prop)) {
      // pour ceux-là il faut passer par un setAttribute, sinon ça ne fait rien du tout (au moins sous chrome sur un div)
      // …et y’en a qui cherchent la calculatrice d’après le name de son div.
      elt.setAttribute(prop, value)
    } else {
      elt[prop] = value
    }
  })
}

/**
 * Ajoute du texte à container (avec createTextNode, gére les &lt; & co mais aucun tag ni \n qui sera affiché tel quel à l’écran)
 * @param {HTMLElement|string} container
 * @param {string} textContent
 * @return {Text} le textNode
 * @throws {Error} si le conteneur n’existe pas
 */
export function j3pAddTxt (container, textContent) {
  // Il faut réaffecter au cas où container était une string
  container = j3pEnsureHtmlElement(container)
  const textNode = _getTextNode(textContent)
  container.appendChild(textNode)
  return textNode
}

/**
 * Retourne la propriété cssProp calculée pour cet élément
 * @param {HTMLElement} elt
 * @param {string} cssProp en camelCase ou hyphen-case peu importe, cette fct fait la conversion
 * @return {string} La valeur de cssProp calculée pour cet elt, chaîne vide si elle n’existe pas ou n’a jamais été définie et n’a pas de valeur par défaut
 */
export function j3pGetCssProp (elt, cssProp) {
  if (!isDomElement(elt) || !cssProp || typeof cssProp !== 'string') {
    console.error(Error('Argument invalide'), elt, cssProp)
    return ''
  }
  // on insère un - devant chaque majuscule (et on passe en minuscule,
  // même si c’est pas la peine car le navigateur devrait le faire d’après la spec
  // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue
  cssProp = cssProp.replace(/([A-Z])/g, '-$1').toLowerCase()
  return getComputedStyle(elt).getPropertyValue(cssProp)
}

/**
 * Vide container de tous ses éléments (comme pour j3pDetruit, inutile de tester l’existence avant,
 * peut être appelé avec un id qui n’existe pas dans le dom, et dans ce cas ça ne fait rien)
 * @param {HTMLElement|string} container
 */
export function j3pEmpty (container) {
  if (!container) return // on fait rien si on nous appelle avec null|undefined
  // vu qu’il y a un return plus bas si l’élément n’existe pas, on teste d’abord les derniers arguments
  if (arguments.length > 1) {
    let i = 1
    while (i < arguments.length) {
      i++
      j3pEmpty(arguments[i])
    }
  }
  if (typeof container === 'string') container = document.getElementById(container)
  if (!container) return // on ne dit rien si l’id n’existe pas
  while (container.lastChild) container.removeChild(container.lastChild)
}

/**
 * Clone un objet (il n’y aura pas les méthodes du prototype de l’objet source,
 * mais s’il y a des propriétés qui sont des fonctions elles seront conservées,
 * sans clonage sur leurs éventuelles propriétés, idem pour d’éventuelles fcts d’un tableau)
 * @param {Object} obj
 * @param {boolean} [noDeep=false] passer true pour ne cloner que le premier niveau (shallow copy, pas de deep clone)
 * @return {Object} ou obj inchangé si c'était pas un object
 */
export function j3pClone (obj, noDeep) {
  // on passe par une fonction imbriquée pour avoir une trace de l’appel initial en cas de boucle infinie
  // (ça arrive si une valeur d’une propriété de l’objet est l’objet lui-même)
  const getClone = obj => {
    // si c’est null ou pas object on ne fait rien
    if (!obj || typeof obj !== 'object') return obj
    if (Array.isArray(obj)) return noDeep ? [...obj] : obj.map(getClone)
    if (!isPlainObject(obj)) return obj
    // c’est un objet "standard" (ni regexp ni date ni…)
    const clone = {}
    for (const [prop, value] of Object.entries(obj)) {
      clone[prop] = noDeep ? value : getClone(value)
    }
    return clone
  }
  try {
    return getClone(obj)
  } catch (error) {
    console.error(Error('Objet trop profond (probablement une référence circulaire)'), error)
    return obj
  }
}

/**
 * Retourne un nouvel objet avec les mêmes propriétés que defaultsValues, donc chacune vaut celle de givenValues si elle existe et est du même type que defaultsValues, sinon celle de defaultsValues
 * Si c’est un tableau, fera la même chose sur chaque élément (donc dans le retour il y en aura autant que dans defaultsValues)
 * Descend dans les sous-propriétés.
 * @param {Object} defaultsValues
 * @param {Object} givenValues
 * @return {Object}
 */
export function j3pComplete (defaultsValues, givenValues) {
  if (typeof defaultsValues !== 'object') return console.error(Error('paramètres invalides'))
  let values
  if (Array.isArray(defaultsValues)) {
    if (!Array.isArray(givenValues)) return defaultsValues
    values = []
    defaultsValues.forEach(function (dv, i) {
      values.push(typeof givenValues[i] === typeof dv ? givenValues[i] : dv)
    })
  } else {
    values = {}
    Object.keys(defaultsValues).forEach(function (p) {
      // on traite object à part, pour du récursif (avec un Array ça fonctionnera aussi)
      if (typeof defaultsValues[p] === 'object') {
        values[p] = j3pComplete(defaultsValues[p], givenValues[p])
      } else {
        values[p] = typeof givenValues[p] === typeof defaultsValues[p] ? givenValues[p] : defaultsValues[p]
      }
    })
  }
  return values
}

// /////////////////////////////////////////////////////////////////////////
// Fin des fonctions globales j3p*
// /////////////////////////////////////////////////////////////////////////

/**
 * Clone props et le renvoie avec une propriété style en objet (convertie si c'était une string, supprimée si null)
 * @param {object} props les props à nettoyer, si undefined ou null renverra un objet vide
 * @param {object} [options]
 * @param {boolean} [options.addEmpty=false] Passer true pour ajouter un objet vide sur la propriété style s’il n’y avait rien (ou rien de valide)
 * @param {boolean} [options.noClone=false] Passer true pour ne pas cloner props (et modifier éventuellement props.style)
 * @return {object} un clone de props, avec éventuellement une propriété style en object (suivant options)
 * @private
 */
export function _clonePropsCleanStyle (props, { addEmpty, noClone } = {}) {
  if (!props || typeof props !== 'object') throw Error('Paramètre invalide (devrait être un objet)')
  if (!noClone) props = j3pClone(props)
  if (hasProp(props, 'style')) {
    const style = props.style
    if (['', null, undefined].includes(style)) {
      delete props.style
    } else if (['object', 'string'].includes(typeof style)) {
      if (typeof style === 'object' && (style.couleur || style.taillepolice)) _convertStyleExoticProps(style)
      props.style = getCleanStyle(style)
    } else {
      console.error(Error('style invalide (' + typeof style))
      delete props.style
    }
  }
  if (addEmpty && !props.style) props.style = {}
  return props
}

/**
 * Remplace dans style les éventuelles valeurs exotiques utilisées dans j3p (couleur et taillepolice)
 * @param {object} style
 * @private
 */
export function _convertStyleExoticProps (style) {
  if (!style || typeof style !== 'object') return console.error(Error('style n’est pas un objet'))
  if (!Object.keys(style).length) return style // objet vide
  // on traduit des noms de propriétés spécifiques à j3p
  if (hasProp(style, 'couleur')) {
    if (!style.color) style.color = style.couleur
    delete style.couleur
  }
  if (hasProp(style, 'taillepolice')) {
    if (!style.fontSize) style.fontSize = getCssDimension(style.taillepolice)
    delete style.taillepolice
  }
  return style
}

/**
 * Surveille un élément du DOM et le remet à l’identique s’il change.
 * Fonctionne sur une branche du DOM, même si elle contient du svg par ex.
 * ATTENTION, ça ne conserve pas des propriétés ajoutées en js sur elt ou ses enfants
 * @param {HTMLElement|string} elt L’élément ou son id
 */
export function j3pFreezeElt (elt) {
  // le Object.freeze ne marche pas sur les éléments du DOM
  // on passe par https://developer.mozilla.org/fr/docs/Web/API/MutationObserver
  // Object.freeze pose effectivement problème avec les inputmq, notamment en cas d’asynchrone (comme avec les tableaux de signes ou de variations, mais pas seulement)
  // => on utilise MutationObserver pour tout le monde, sauf les inputs "standard" pour lesquels on simplifie avec Object.freeze
  try {
    if (typeof elt === 'string') elt = j3pElement(elt)
    if (!j3pIsHtmlElement(elt, true)) return
    if (elt.frozen) return console.warn('L’élément est déjà gelé', elt)
    // console.trace('j3pFreezeElt', elt)
    // si navigateur vraiment trop vieux, on laisse tomber
    if (typeof MutationObserver !== 'function') return console.warn('navigateur trop ancien pour geler un élément du DOM')
    const parent = elt.parentNode
    if (!parent) return console.warn('élément sans parentNode, impossible à geler', elt)

    // avant 2025-10-29 on restaurait le outerHTML du node,
    // mais ça ne fonctionne pas sur des <div> que l'on modifiait via les devTools
    // (qui ne modifient pas le node mais le remplacent par un nouveau)
    // et notre mutationObserver ne restaurerait alors plus le bon node.
    // => on passe par un clone du node que l'on conserve hors DOM,
    // et que l'on remettra dans le dom à chaque modif (après nouveau clonage pour garder le snapshot intact).

    const snapshot = elt.cloneNode(true)
    // pour les select il faut mémoriser l'état courant qui n'est pas dans le clone
    const state = {}
    if (elt.tagName === 'SELECT') {
      if (elt.multiple) {
        state.selected = Array.from(elt.options)
          .map((opt, i) => opt.selected ? i : -1)
          .filter(index => index !== -1)
      } else {
        state.selectedIndex = elt.selectedIndex
      }
    }
    if ('value' in elt) {
      state.value = elt.value
    }

    const restore = () => {
      // Remplace la branche entière par le snapshot (cloné à chaque fois)
      const newNode = snapshot.cloneNode(true)
      if ('selectedIndex' in state) {
        newNode.selectedIndex = state.selectedIndex
      } else if (state.selected) {
        // on remet tout à false au cas où la sélection d'un item n'aurait pas retiré l'attribut (seulement la propriété)
        for (const opt of newNode.options) opt.selected = false
        // avant de passer à true ce qui doit l'être
        for (const i of state.selected) {
          newNode.options[i].selected = true
        }
      }
      if ('value' in state) {
        newNode.value = state.value
      }
      parent.replaceChild(newNode, elt)
      // installe l’observateur sur le nouveau nœud
      installObserver(newNode)
      if (!elt.frozen) {
        // elt est sorti du dom mais n'avait pas de frozen, c'est donc la ref passée au 1er appel
        // on la marque aussi frozen maintenant qu'on est sûr que l'élément actuellement dans le dom est bien gelé.
        // (si l'appelant a conservé une ref à cet elt il pourra tester s'il est gelé)
        elt.frozen = true
      }
      elt = newNode // pour le prochain `parent.replaceChild(newNode, elt)`
    }
    const installObserver = (node) => {
      const observer = new MutationObserver(() => {
        // on déconnecte celui-ci et on restaure (ça en recréera un autre)
        observer.disconnect()
        restore()
      })
      observer.observe(node, { attributes: true, characterData: true, childList: true, subtree: true })
      node.frozen = true
    }
    // on restaure une 1re fois, pour supprimer tous les listeners existants sur ce bout de DOM
    restore()
  } catch (error) {
    console.error(error)
  }
}

/**
 * Vérifie que value est entre min et max et le retourne en string
 * @param {number|string} value
 * @param {number} [max=1]
 * @param {number} [min=0]
 * @return {string}
 * @private
 */
export function _cssGetFloatAsString (value, max, min) {
  if (typeof max !== 'number') max = 1
  if (typeof min !== 'number') min = 0
  const val = typeof value === 'number' ? value : Number(value)
  if (val >= min && val <= max) return String(val)
  console.error(Error('valeur incorrecte pour un nombre attendu entre ' + min + ' et ' + max + ' : ' + value))
  return ''
}

/**
 * Retourne value en number (pouvant être utilisé dans une opération) si value est number ou une string du genre 12px
 * Râle en console sinon et retourne 0
 * @param {string|number} value
 * @return {number} value nettoyé (px éventuel viré), 0 si y’a eu un pb
 * @private
 */
export function _cssToNb (value) {
  if (typeof value === 'number' && isFinite(value)) return value
  if (typeof value === 'string') {
    const chunks = /^(-?[0-9.]+)(px)?;?$/.exec(value)
    if (chunks) {
      return Number(chunks[1])
    }
  }
  console.error(Error('impossible de convertir ' + value + ' en number'))
  // on retourne un number quand même, pour pas planter la suite
  return 0
}

/**
 * Retourne un élément Text (sorti de document.createTextNode) prêt à être inséré avec appendChild
 * Les entités html &(lt|gt|amp|nbsp|dollar); sont gérées, &#xx; aussi (même s’il vaudrait mieux l’écrire directement avec du \uXXXX),
 * mais s’il y a du code html il sera affiché tel quel.
 * Pour les caractères spéciaux voir {@link https://html.spec.whatwg.org/multipage/named-characters.html}
 * @param {string} text
 * @return {Text}
 * @private
 */
export function _getTextNode (text) {
  if (typeof text !== 'string') {
    if (typeof text === 'number') {
      text = String(text)
    } else {
      console.warn(Error('texte invalide, remplacé par une chaîne vide'), text)
      text = ''
    }
  }
  if (text.includes('&')) {
    text = text
      .replace(/&lt;/g, '<')
      .replace(/&le;/g, '≤')
      .replace(/&gt;/g, '>')
      .replace(/&ge;/g, '≥')
      .replace(/&amp;/g, '&')
      .replace(/&dollar;/g, '$')
      // ce code fonctionne avec chrome et firefox (ça coupe pas sur cet espace)
      .replace(/&nbsp;/g, '\u00a0')
      // on remplace tous les &#xx; par leur entité utf8
      .replace(/&#([0-9]+);/g, (str, nb) => String.fromCharCode(Number(nb)))
  }
  return document.createTextNode(text)
}