legacy/outils/bulleAide/BulleAide.js

import { getDelayMs } from 'sesajs/date'

import { j3pAddElt, j3pDetruit, j3pElement, j3pGetPos, j3pIsHtmlElement } from 'src/legacy/core/functions'
import { j3pAffiche } from 'src/lib/mathquill/functions'
import { legacyStyles } from 'src/legacy/core/StylesJ3p'
import { getCssDimension } from 'sesajs/css'

import './bulleAide.css'

/**
 * Crée une bulle d’aide
 * Cf r11620 pour une version avec un trigger plus grand (la bulle s’affichait lorsque le curseur approchait du trigger sans être dessus)
 * @module legacy/outils/bulleAide/BulleAide
 */

// iife pour déclarer ce que je veux tranquillement
// cf https://developer.mozilla.org/fr/docs/Glossaire/IIFE
// fonctions privées, qui pourront être transformées en listeners avec du bind ou appelée avec call
// (pour fixer leur this sur l’instance bulleAide courante)

/**
 * Appelé à chaque show, dans le cas où la bulle se positionne sur le trigger,
 * pour éviter que la bulle ne déborde à droite
 * @private
 */
function followTrigger () {
  const pos = j3pGetPos(this.bulleTrigger)
  this.bulleDiv.style.left = (pos.x - j3pElement('MepMG').scrollLeft) + 'px'
  this.bulleDiv.style.top = (pos.y - j3pElement('MepMG').scrollTop) + 'px'
  const total = document.body.offsetWidth
  const reste = total - pos.x
  if (reste < this.bulleDiv.offsetWidth) {
    // la bulle déborde à droite, on décale vers la gauche
    const moitie = this.bulleDiv.offsetWidth / 2
    if (moitie < pos.x && moitie < reste) {
      // centré sur le trigger, ça rentre
      this.bulleDiv.style.left = (pos.x - moitie) + 'px'
      if (this.gg) this.bulleDiv.style.left = (pos.x - 200) + 'px'
    } else if (this.bulleDiv.offsetWidth < total) {
      // on cale à droite sans rétrécir le div
      this.bulleDiv.style.left = (pos.x - this.bulleDiv.offsetWidth + reste) + 'px'
      if (this.gg) this.bulleDiv.style.left = (pos.x - 200) + 'px'
    } else {
      if (this.gg) this.bulleDiv.style.left = (pos.x - 200) + 'px'
      // on prend toute la place possible
      this.bulleDiv.style.left = '0'
      this.bulleDiv.style.width = total + 'px'
    }
  } else {
    if (this.gg) {
      this.bulleDiv.style.left = (pos.x - 200) + 'px'
    }
  }
}

/**
 * Appelé à chaque show, dans le cas où on est dans un parent filé au constructeur
 * @private
 */
function replace () {
  const pos = j3pGetPos(this.bulleDiv)
  // on vérifie pas de minimum, fallait pas nous filer un conteneur qui peut se retrouver trop à droite…
  this.bulleDiv.style.maxWidth = (document.body.offsetWidth - pos.x) + 'px'
}

/**
 * La dernière bulle ouverte (pour la refermer si on en ouvre une autre, cette var est globale à l’outil, donc commune à toutes les bulles)
 * @private
 * @type {BulleAide}
 */
let lastOpened

/**
 * Crée l’objet bulle et ses éléments dans le dom
 * @param {HTMLElement|string} container Le conteneur du "?" que l’on va ajouter pour servir de déclencheur (ou le déclencheur si options.containerAsTrigger)
 * @param {string} content Le contenu de la bulle d’aide (qui sera passé à j3pAffiche pour interpréter les caractères spéciaux façon j3p)
 * @param {Object} [options]
 * @param {string} [options.backgroundColor]
 * @param {string} [options.color]
   * @param {boolean} [options.containerAsTrigger] Passer true pour que le container passé soit utilisé comme élément déclencheur (et dans ce cas on ajoute pas le (?) dedans)
 * @param {HTMLElement|string} [options.parent] Éventuel parent dans lequel mettre la bulle d’aide
 * @param {number} [options.left=0] Pour positionner la bulle d’aide par rapport à son parent
 * @param {number} [options.top=0]
 * @param {boolean} [options.gg=false]
 * @constructor
 */
function BulleAide (container, content, options) {
  if (typeof container === 'string') container = j3pElement(container)
  if (!j3pIsHtmlElement(container)) return console.error(Error('container manquant'))

  // les paramètres
  if (!options || typeof options !== 'object') options = {}

  // on garde des refs sur nos éléments bulleTrigger et bulleDiv pour les utiliser directement dans les listeners

  if (options.containerAsTrigger) {
    this.bulleTrigger = container
  } else {
    /**
       * Le span contenant le (?) qui sert de déclencheur à l’apparition de la bulle
       * @type {HTMLElement}
       */
    this.bulleTrigger = j3pAddElt(container, 'span', '', {
      style: {
        // cf bulleAide.css et https://developer.mozilla.org/fr/docs/Web/CSS/animation
        animation: 'bulleAideGlow 2s infinite',
        color: legacyStyles.colorCorrection
      }
    })
    // et on ajoute le contenu en gras dedans
    j3pAddElt(this.bulleTrigger, 'strong', '(?)')
  }
  /**
   * c’est quoi ce gg ???
   * @type {boolean}
   */
  this.gg = options.gg === true
  // le div de la bulle
  const divStyle = {
    padding: '0.5em',
    border: 'solid 2px',
    display: 'none',
    position: 'absolute',
    // en haut à droite, du parent, on décale un peu
    left: options.left ? getCssDimension(options.left) : '0',
    top: options.top ? getCssDimension(options.top) : '0',
    backgroundColor: options.backgroundColor || options.couleurfond || '#81BEF7',
    color: options.color || options.couleur || '#0404B4',
    borderRadius: '5px',
    verticalAlign: 'middle',
    // 50 pour les boutons, 80 pour les modales, 100 pour dialog, faut être au-desssus…
    zIndex: 110
  }
  // on regarde si on a fourni un parent
  let parent = options.parent
  if (typeof parent === 'string') parent = j3pElement(parent)
  if (parent) {
    this.replace = replace.bind(this)
  } else {
    // s’il n’est pas fourni on se positionnera dans body, juste sur le span
    parent = document.body
    this.replace = followTrigger.bind(this)
  }
  /**
     * Le div du contenu de la bulle
     * @memberOf BulleAide
     * @type {HTMLElement}
     */
  this.bulleDiv = j3pAddElt(parent, 'div', '', { style: divStyle })
  j3pAffiche(this.bulleDiv, '', content)
  this.visible = false

  // ces listeners sont attachés à l’instance, mais les fct restent génériques (déclarées une seule fois pour toutes les bulles)
  this.listeners = {
    show: this.show.bind(this),
    hide: this.hide.bind(this),
    toggle: this.toggle.bind(this)
  }

  // hide / show au survol
  this.bulleTrigger.addEventListener('mouseenter', this.listeners.show, false)
  this.bulleTrigger.addEventListener('mouseout', this.listeners.hide, false)
  // pour les tablettes faut du click
  this.bulleTrigger.addEventListener('click', this.listeners.toggle, false)
  // hide au mouseout du div, au cas où il recouvre le trigger
  this.bulleDiv.addEventListener('mouseout', this.listeners.hide, false)
} // BulleAide

BulleAide.prototype.show = function show () {
  if (!this.bulleDiv) return console.error(Error('Cette bulle a été détruite'))
  if (this.visible) return
  // si y’avait une bulle ouverte on la ferme (ça peut arriver sur tablette avec le click)
  if (lastOpened) lastOpened.hide()
  lastOpened = this
  // on affiche le div
  this.visible = true
  this.bulleDiv.style.display = 'block'
  // et on arrête l’animation sur le span
  this.bulleTrigger.style.animation = ''
  this.lastShowTs = Date.now() // timestamp, en ms
  this.replace() // followTrigger ou replace suivant le cas
}

BulleAide.prototype.hide = function hide () {
  if (!this.bulleDiv) return console.error(Error('Cette bulle a été détruite'))
  if (!this.visible) return
  if (getDelayMs(this.lastShowTs, 101) < 100) return // la bulle recouvre le trigger, on laisse tomber ce sera son mouseout qui cachera
  // on cache le div
  this.visible = false
  this.bulleDiv.style.display = 'none'
}

BulleAide.prototype.toggle = function toggle () {
  if (this.visible) this.hide()
  else this.show()
}

/**
 * Détruit la bulle (et son trigger "(?)" que l’on avait ajouté)
 */
BulleAide.prototype.disable = function disable () {
  if (lastOpened === this) lastOpened = null
  if (!this.bulleDiv) return console.error(Error('Cette bulle a déjà été détruite'))
  // this.mainDiv.removeEventListener('mousemove', this.listeners.onMouseMove, false)
  // détruire l’élément va détruire ses écouteurs, sauf si qqun a mis une ref dessus (avec un var bulleDiv = bulle.bulleDiv)
  // donc on nettoie quand même avant de détruire
  this.bulleDiv.removeEventListener('mouseout', this.listeners.hide, false)
  j3pDetruit(this.bulleDiv)
  this.bulleDiv = null
  // idem pour le trigger
  this.bulleTrigger.removeEventListener('mouseenter', this.listeners.show, false)
  this.bulleTrigger.removeEventListener('mouseout', this.listeners.hide, false)
  this.bulleTrigger.removeEventListener('click', this.listeners.toggle, false)
  j3pDetruit(this.bulleTrigger)
  this.bulleTrigger = null
  // et on surcharge aussi cette methode, au cas où on l’appellerait une 2e fois
  this.disable = function () {
    console.error(Error('Cette bulle d’aide a déjà été désactivée'))
  }
}

// et on exporte seulement ça
export default BulleAide