legacy/outils/zoneStyleMathquill/ZoneStyleMathquillBase.js

import { j3pAddElt, j3pAddTxt, j3pDetruit, j3pElement, j3pEmpty, j3pGetNewId, j3pIsHtmlElement } from 'src/legacy/core/functions'
import { colorKo, colorOk } from 'src/legacy/core/StylesJ3p'
import { addTab } from 'src/legacy/outils/zoneStyleMathquill/listeTabulations'
import { addTable, getCells } from 'src/legacy/themes/table'
import { getJ3pConteneur } from 'src/lib/core/domHelpers'

/**
 * Classe qui regroupe le code commun à tous les ZoneStyleMathquill (qui l’étendent)
 * Elle ne devrait jamais être instanciée directement, seulement étendue
 */
class ZoneStyleMathquillBase {
  /**
   *
   * @param {HTMLElement|string} conteneur
   * @param {Object} params
   * @param {string} [params.contenu] Du contenu à mettre dans la zone au départ
   * @param {boolean} [inverse=false] Passer true pour que le clavier s’affiche au dessus de la zone
   * @param {number} version Pour gérer quelques spécificités de zsm1|zsm2|zsm3, doit être 1|2|3
   */
  constructor (conteneur, params) {
    if (typeof conteneur === 'string') conteneur = j3pElement(conteneur)
    if (!j3pIsHtmlElement(conteneur)) throw Error('Conteneur invalide')
    const { clavier, enter, inverse, limite, restric, version, j3pCont } = params
    if (![1, 2, 3].includes(version)) throw Error(`Version ${version} invalide`)
    /**
     * La version de zsm, ZoneStyleMathquill(1|2|3)
     * @private
     */
    this._version = version
    /**
     * Le conteneur
     * @type {HTMLElement}
     */
    this.conteneur = conteneur
    /**
     * La zone j3p totale (class j3pContainer) dans laquelle se trouve notre conteneur
     * @type {HTMLElement}
     */
    try {
      this.parent = getJ3pConteneur(conteneur, true) || j3pElement('j3pContainer') || j3pCont
      if (!this.parent) {
        this.parent = document.body
      } else {
        if (this.parent.URL) this.parent = j3pElement('j3pContainer') || j3pCont
      }
      if (!this.parent) this.parent = document.body
    } catch (e) {
      this.parent = document.body
    }

    const restricDefault = version === 2
      ? '0123456789.,+-*/²()^'
      : '0123456789.,+-*/²()'
    /**
     * La liste des caractères acceptés par l’input
     * @type {string}
     */
    this.restric = restric ?? restricDefault
    /**
     * Liste des caractères à mettre dans le clavier
     * @type {string}
     */
    this.clavierR = clavier || this.restric
    // touche maj
    if (version === 2) {
      this._fautMaj = false
      this.isCapsLocked = false
    } else {
      // on regarde si y’a des minuscules et des majuscules, pour savoir si le bouton maj est nécessaire
      const aMin = /[a-z]/.test(this.restric)
      const aMaj = /[A-Z]/.test(this.restric)
      /**
       * La touche maj est nécéssaire
       * @type {boolean}
       * @private
       */
      this._fautMaj = aMin && aMaj
      /**
       * Flag pour indiquer si on est en majuscule
       * @type {boolean}
       */
      this.isCapsLocked = !aMin
    }
    /**
     * État courant du clavier
     * @private
     * @type {boolean}
     */
    this._isClavierVisible = false
    /**
     * Flag pour bloquer l’insertion de caractère tant que le traitement du clic précédent n’est pas terminé
     * @type {boolean}
     */
    this._isLocked = false
    /**
     * mis à true par disable() pour marquer la zone désactivée
     * @type {boolean}
     */
    this.disabled = false
    /**
     * Callback sur entrée
     * @type {function|null}
     */
    this.enter = typeof enter === 'function' ? enter : null
    /**
     *
     * @type {boolean}
     */
    this.invClav = (inverse === true)
    /**
     * Le nb de caractères max
     * @type {number}
     */
    this.limite = Number.isFinite(limite) ? limite : 1000
    this.historique = []
    // des propriétés que l’on utilise dans les méthodes de cette classe parente, qui devront être surchargées par les enfants
    this._onClickClavier = () => {
      throw Error('Chaque instance doit affecter ce listener')
    }
  }

  barreIfKo () {
    if (this.bon === false) this.barre()
  }

  corrige (bon) {
    this.conteneur.style.color = bon ? colorOk : colorKo
    this.bon = bon
  }

  isOk () {
    return this.bon
  }

  /**
   * Ajoute un bouton dans cells
   * @param {Array<HTMLTableDataCellElement[]>} cells
   * @param {number} lig
   * @param {number} col
   * @param {string} [txt]
   * @param {string} mes
   * @param {string} [className]
   */
  addBtn ({ cells, lig, col, txt, mes, className = 'zsmBtn' }) {
    this.historique.push('addBtn')
    if (!cells[lig]) return console.error(Error(`Il n’y a pas de ligne ${lig} dans les cellules fournies`), cells)
    if (!cells[lig][col + 1]) {
      if (!cells[lig][col]) return console.error(Error(`On ne peut pas mettre de touche dans la dernière colonne du tableau (${col})`))
      return console.error(Error(`Il n’y a pas de colonne ${col} dans les cellules fournies`), cells)
    }
    const cell = cells[lig][col]
    if (className) cell.classList.add(className)
    cell.style.width = '40px'
    cell.addEventListener('click', this._onClickClavier, false)
    if (txt) {
      // si y’a du html dans txt faut passer par innerHTML pour qu’il soit interprété
      if (/<[^>]+>/.test(txt)) cell.innerHTML = txt
      else j3pAddTxt(cell, txt)
    } // y’a des cas où on nous passe pas de txt (contenu géré en css)
    cell.mes = mes
    cells[lig][col + 1].style.width = '10px'
  }

  /**
   * Factorise du code des méthodes blur
   */
  blurHelper () {
    this.historique.push('blurHelper')
    if (typeof this?.majaffiche !== 'function') throw Error('Appel invalide')
    this.majaffiche('')
    const elt = this.textaCont
    if (!elt) return
    elt.className = 'zsmMq'
    if (!this.obligeclav) {
      const elt = this.textaIm
      if (elt) elt.style.display = 'none'
      this.divClavier.style.visibility = 'hidden'
      this._isClavierVisible = false
    }
    if (this.hasAutoKeyboard) {
      this.divClavierAuto.style.visibility = 'hidden'
    } else {
      // me rappelle plus pourquoi ca ?
      const elt = this.unTAbc[0][this.texta.poscurseur * 2]
      if (elt) elt.className = ''
    }
  }

  /**
   * Crée le clavier qui s’ouvre automatiquement dès que la zone a le focus
   */
  buildAutoKeyboard (firstButtons) {
    this.historique.push('buildAutoKeyboard')
    if (typeof this?.clavierR !== 'string') throw Error('Appel invalide')
    // on commence par remplacer le * par × (pour pas le compter deux fois)
    if (this.clavierR.includes('*')) {
      this.clavierR = this.clavierR.replace('*', this.clavierR.includes('×') ? '' : '×')
    }

    // il y a quelques subtilités entre zfsm2 et zfsm3, cette méthode est commune aux deux
    const allChars = this._version === 3
      ? '[]()×/²ùπ'
      : this._version === 2
        ? '[]()×/²^π'
        : '[]()°µ'
    // on construit le <table>, en comptant d’abord combien de boutons il faut
    let cpt = firstButtons?.length || 0
    for (const char of allChars) {
      if (this.clavierR.includes(char)) cpt++
    }
    this.divClavierAuto = j3pAddElt(this.parent, 'div', '', {
      className: 'zsmClavier',
      style: { visibility: 'hidden' }
    })

    // ensuite on formate et ajoute du comportement sur les cellules qui vont prendre les boutons
    const tabCla = addTable(this.divClavierAuto, { nbLignes: 1, nbColonnes: cpt * 2 + 1, className: 'noMargin' })
    const cells = getCells(tabCla)
    cells[0][0].style.width = '10px'

    // et on ajoute les boutons
    cpt = 0
    if (firstButtons?.length) {
      for (const { txt, mes, className } of firstButtons) {
        this.addBtn({ cells, lig: 0, col: cpt * 2 + 1, txt, mes, className })
        cpt++
      }
    }
    for (const char of allChars) {
      if (this.clavierR.includes(char)) {
        const cell = cells[0][cpt * 2 + 1]
        if (!cell) return console.error(Error(`pas de cell d’index ${cpt * 2 + 1} (bouton n°${cpt})`), cells)
        cell.mes = char
        let txt = char
        if (char === '²' && this._version === 2) {
          txt = '<i>x</i>²'
        } else if (char === 'ù') {
          txt = '√'
        }
        this.addBtn({ cells, lig: 0, col: cpt * 2 + 1, txt, mes: char })
        cpt++
      }
    }
  }

  /**
   * @typedef ButtonDef
   * @property {string} mes Une propriété ajouté au &lt;td> qui sera récupérée par les écouteurs
   * @property {string} txt Le texte affiché sur la touche du clavier
   * @property {string} [className] une classe css à mettre sur le bouton à la place du zsmBtn par défaut
   */
  /**
   * Factorise du code pour les méthodes buildKeyboard
   * @param {string} touches La liste des caractères à mettre dans le clavier
   * @param {ButtonDef[]} [extraButtons] Des boutons supplémentaires à ajouter sur la dernière ligne
   */
  buildKeyboardHelper (touches, { useExisting = false, extraButtons = [] } = {}) {
    this.historique.push('buildKeyboardHelper')
    if (useExisting) {
      j3pEmpty(this.divClavier)
    } else {
      this.divClavier = j3pAddElt(this.parent, 'div', '', { className: 'zsmClavier', style: { visibility: 'hidden' } })
    }
    // les boutons de la dernière ligne qu’il faudra ajouter (on le fait maintenant pour calculer la largeur du clavier)
    const contents = [
      ['→', 'ArrowRight'],
      ['←', 'ArrowLeft'],
      ['↓', 'ArrowDown'],
      ['↑', 'ArrowUp'],
      ['Del', 'Delete'],
      ['Sup', 'Backspace']
    ]
    // pourquoi zsm1 n’a pas droit à Up|Down ?
    if (this._version === 1) {
      contents.splice(3, 2)
    }
    if (this._fautMaj) contents.unshift(['⇑', 'MAJ'])
    const nbKeysOnLastLine = contents.length + (extraButtons?.length || 0)
    if (nbKeysOnLastLine > 10) {
      const max = 10 - contents.length
      console.error(Error(`Il y a trop de boutons supplémentaires (${extraButtons.length}), seuls ${max} seront conservés`))
      extraButtons.splice(max)
    }

    // calcul du nb de lignes & colonnes
    const nbTouches = touches.length
    const nbLignes = Math.ceil(nbTouches / 10) + 1 // +1 pour la ligne des flèches
    // on veut d’abord savoir combien de touches par ligne
    const borne = Math.max(nbTouches, nbKeysOnLastLine) // au cas où on aurait nbTouches < nbKeysOnLastLine, faut pas limiter le nb de colonne à nbTouches ligne suivante
    const nbColAvecTouche = Math.min(borne, 10)
    const nbColonnes = nbColAvecTouche * 2 + 1
    // on peut créer le tableau
    const tabCla = addTable(this.divClavier, { nbLignes, nbColonnes, className: 'noMargin' })
    const cells = getCells(tabCla)
    // on impose la largeur du premier <td> de chaque tr
    for (const ligne of cells) {
      ligne[0].style.width = '7px'
    }

    // reste à mettre les touches dedans, celles passées dans la liste
    for (const [i, touche] of touches.split('').entries()) {
      const lig = Math.floor(i / nbColAvecTouche)
      const col = i % nbColAvecTouche * 2 + 1
      let txt, mes
      // les 3 caractères spéciaux gérés
      if (touche === ' ') {
        mes = ' '
      } else if (touche === 'µ') {
        txt = '^'
      } else if (touche === 'ù') {
        txt = '√'
      }
      if (!txt) txt = touche
      if (!mes) mes = touche
      // on peut ajouter le bouton et passer au suivant
      this.addBtn({ cells, lig, col, txt, mes })
    }
    // les flèches
    const lig = nbLignes - 1 // la dernière
    let col = 1
    for (const [txt, mes] of contents) {
      this.addBtn({ cells, lig, col, txt, mes })
      col += 2
    }
    // et d’éventuels extras
    if (extraButtons) {
      for (const { txt, mes, className } of extraButtons) {
        this.addBtn({ cells, lig, col, txt, mes, className })
        col += 2
      }
    }
  }

  /**
   * Factorise le code en fin du constructeur pour zsm1 & 3
   */
  constuctorFinalize (contenu) {
    this.historique.push('constuctorFinalize')
    this.buildKeyboard()
    // pourquoi zsm3 n’y a pas droit ?
    if (this._version !== 3) this.majaffiche('')
    document.addEventListener('click', this._gereBlurListener, false)
    // si y’a du contenu à mettre dès la construction on le fait ici
    if (contenu && typeof contenu === 'string') {
      if (this._version !== 2) {
        for (const char of contenu) {
          // @todo expliquer le rôle de ces espaces fins insécables, il vaudrait mieux s’en passer
          //  C’est très casse-gueule d’utiliser un caractère qui s’affiche comme une espace dans la plupart des éditeurs mais n’est pas une espace,
          //  => c’est pour ça que ça génère une erreur eslint
          //  => c’est considéré comme une mauvaise pratique à vraiment éviter
          this.majaffiche(char === ' ' ? ' ' : char)
        }
      } else {
        let apush = ''
        for (const char of contenu) {
          // @todo expliquer le rôle de ces espaces fins insécables, il vaudrait mieux s’en passer
          //  C’est très casse-gueule d’utiliser un caractère qui s’affiche comme une espace dans la plupart des éditeurs mais n’est pas une espace,
          //  => c’est pour ça que ça génère une erreur eslint
          //  => c’est considéré comme une mauvaise pratique à vraiment éviter
          if (char === ' ' || char === ' ') {
            if (apush !== '') this.majaffiche(apush)
            apush = ''
          } else {
            apush += char
          }
        }
        if (apush !== '') this.majaffiche(apush)
      }
    }
    this.blur()
    addTab(this)
  }

  /**
   * Factorise du code de majAffiche
   */
  majAfficheHelper () {
    this.historique.push('majAfficheHelper')
    if (this.hasAutoKeyboard) {
      // on construit le clavier auto au premier focus
      if (this.divClavierAuto === undefined) this.buildAutoKeyboard()
      this.divClavierAuto.style.visibility = 'visible'
      const div = this.divClavierAuto
      const { x, y, height } = this.conteneur.getBoundingClientRect()
      const { height: dcaH } = this.divClavierAuto.getBoundingClientRect()
      const { x: xM, y: yM } = this.parent.getBoundingClientRect()
      div.style.left = (x - xM) + 'px'
      if (!this.invClav) {
        div.style.top = (y - yM - dcaH - 5) + 'px'
      } else {
        div.style.top = (y - yM + height + 5) + 'px'
      }
      div.style.zIndex = '105'
    }

    this.textaIm.style.display = ''
  }

  /**
   * Factorise du code de majAffiche
   */
  majAfficheHelper2 () {
    this.historique.push('majAfficheHelper2')
    if (typeof this?._clickZoneListener !== 'function') throw Error('Appel invalide')
    const classClavier = (this.divClavier && this.divClavier.style.visibility !== 'hidden') ? 'zsmPictoClavierBarre' : 'zsmPictoClavier'
    if (this.textaTab !== undefined) j3pDetruit(this.textaTab)
    this.textaTab = addTable(this.texta, { nbLignes: 1, nbColonnes: 2, className: 'zoneS3BAse' })
    const perpascell = getCells(this.textaTab)
    this.textaCont = perpascell[0][0]
    this.textaCont.id = j3pGetNewId()
    this.textaIm = perpascell[0][1]
    this.textaIm.classList.add(classClavier)
    this.textaIm.innerHTML = '&nbsp;&nbsp;&nbsp;&nbsp;'
    this.eLemCoNtz = j3pAddElt(this.textaCont, 'div')

    this.unTAb = addTable(this.eLemCoNtz, {
      nbLignes: 1,
      nbColonnes: this.texta.elemnum.length * 2 + 1,
      className: 'zoneS3'
    })
    this.unTAbc = getCells(this.unTAb)
    this.tabRac = []
    this.textaCont.addEventListener('click', this._clickZoneListener, false)
  }

  /**
   * Toggle le clavier et le positionne si ça l’affiche
   * @param {boolean} [withoutToggle] passer true pour ne pas faire le toggle
   */
  place (withoutToggle) {
    this.historique.push('place')
    if (!withoutToggle) this.toggle()
    if (!this._isClavierVisible) return // inutile de positionner un truc invisible
    this._isLocked = true
    setTimeout(() => {
      this._isLocked = false
      this.majaffiche('')
    }, 20)
    const div = this.divClavier
    const { x, y, height } = this.conteneur.getBoundingClientRect()
    const { height: heightClavier } = this.divClavier.getBoundingClientRect()
    const { x: xM, y: yM } = this.parent.getBoundingClientRect()
    if (!this.invClav) {
      div.style.top = (y - yM + height + 5) + 'px'
    } else {
      div.style.top = (y - yM - heightClavier - 5) + 'px'
    }
    div.style.left = (x - xM) + 'px'
    div.style.zIndex = '105'
  }

  /**
   * Affiche ou masque le clavier
   */
  toggle () {
    this.historique.push('toggle')
    const { style } = this.divClavier
    if (style.visibility === 'hidden') {
      // Contenu caché, le montrer
      style.visibility = 'visible'
      style.height = 'auto' // Optionnel rétablir la hauteur
      this._isClavierVisible = true
    } else {
      // Contenu visible, le cacher
      style.visibility = 'hidden'
      style.height = '0'
      this._isClavierVisible = false
    }
  }
}

export default ZoneStyleMathquillBase