legacy/outils/zoneStyleMathquill/ZoneStyleMathquill1.js

import $ from 'jquery'

import { j3pDetruit, j3pFreezeElt, j3pStyle } from 'src/legacy/core/functions'
import ZoneStyleMathquillBase from 'src/legacy/outils/zoneStyleMathquill/ZoneStyleMathquillBase'
import { j3pAffiche } from 'src/lib/mathquill/functions'
import { isHtmlElement } from 'sesajs/dom'
import { barreZone } from './functions'
import { blurAllTabs, removeTab, prevTabFocus } from './listeTabulations'
// rien
// et notre css
import './zoneStyleMathquill.scss'

// une fct qui fait rien
const dummy = () => undefined

// pour tester arc & angle http://localhost:8081/?rid=sesabibli/5c97d5764bb1527df9236b29 ou https://j3p.sesamath.net/?rid=sesabibli/5c97d5764bb1527df9236b29

class ZoneStyleMathquill1 extends ZoneStyleMathquillBase {
  /**
   * Crée une zone de saisie (inline)
   * - pour des nombres simples, en acceptant les espaces
   * - pour des réponses textes, avec chapeau angle ou arc
   * @param {HTMLElement|string} conteneur Le conteneur (ou son id)
   * @param {Object} parametres
   * @param {string} parametres.clavierR  une chaine avec tous les caracteres proposes au clavier virtuel
   *                                     si undefined copie de restric
   *                                     @todo modifier ici caractere affiché et rendu pour espace|angle|arc|/ ou je sais pas quoi d autre
   *                                     faudra que je laisse la possibilite d’un string simple quand mm
   * @param {string} [parametres.contenu] Contenu éventuel à mettre dans l’input au départ
   * @param {number} [parametres.limite=1000] nb de caracteres max
   * @param {function} [parametres.enter] Une callback à appeler sur la touche entrée
   * @param {Parcours} parametres.parcours L’instance du parcours courant
   * @param {string} parametres.restric  une chaine avec tous les caracteres autorisées au clavier
   *                                     si undefined ca vaut 0123456789.,+-* /² ()
   * @param {boolean} parametres.tabauto Passer true pour que le focus passe automatiquement à l’input suivant lors de la saisie
   * @param {string} parametres.id pour permettre à tabulation de choper prevtab
   * @constructor
   */
  constructor (conteneur, parametres) {
    // on doit toujours commencer par appeler le constructeur de la classe parente
    super(conteneur, { ...parametres, version: 1 })
    // ce qui suit est spécifique à zsm1
    if (isHtmlElement(parametres.forceParent)) {
      this.parent = parametres.forceParent
    }
    this.sansBord = Boolean(parametres.sansBord)
    this.isRest = false
    this.okMod = false
    this.chapo = 0
    this.hasAngle = this.restric.includes('^')
    this.hasArc = this.restric.includes('$')
    if (this.hasAngle || this.hasArc) {
      // maintenant on vire ^ et $
      this.restric = this.restric.replace(/[$^]/g, '') // dans les crochets y’a pas besoin d’échapper $, ni ^ s’il n’est pas au début
      // faut donc réaffecter ça aussi
      if (!parametres.clavier) this.clavierR = this.restric
    }
    this.tabauto = parametres.tabauto === true
    this.prevTab = parametres.prevTab
    this.onModif = parametres.onmodif
    this.modeText = parametres.modeText
    this.hasAutoKeyboard = parametres.hasAutoKeyboard
    this.afaire = parametres.afaire
    this.id = parametres.id

    // crea de la zone
    this.texta = this.conteneur
    // 0 pour simple  1 pour frac
    this.texta.poscurseur = 0
    // 0 si dans num , 1 si dans den (pour pos2
    this.texta.elemnum = []
    // on a besoin de mettre ces deux listeners en propriété de l’objet pour que disable() puisse les virer
    this._gereBlurListener = this.gereblur.bind(this)
    this._clickZoneListener = this.clickzone.bind(this)
    // celui-là c’est addBtn qui l’utilise
    this._onClickClavier = this.clickKlav.bind(this)
    // la suite est commune aux 3 zsm
    super.constuctorFinalize(parametres.contenu || '')
  }

  barre () {
    return barreZone(this.conteneur)
  }

  blur () {
    if (this.disabled) return
    if (this._isLocked) return
    const fofo = this.isblur
    this.isblur = true
    this.okMod = false
    this.blurHelper()
    if (!fofo && typeof this.onModif === 'function') {
      this.onModif()
    }
  }

  buildAutoKeyboard () {
    const firstButtons = []
    if (this.hasAngle) {
      firstButtons.push({
        mes: 'ANG',
        txt: '  ',
        className: 'zsmAngleFondGris'
      })
    }
    if (this.hasArc) {
      firstButtons.push({
        mes: 'ARC',
        txt: '  ',
        className: 'zsmArcFondGris'
      })
    }
    super.buildAutoKeyboard(firstButtons)
  }

  buildKeyboard (useExisting) {
    const avirer = this.isCapsLocked ? /[a-z]/g : /[A-Z]/g
    const touches = this.clavierR.replace(avirer, '')
    const extraButtons = []
    if (this.yaangle) {
      extraButtons.push({ txt: '', mes: 'ANG', className: 'zsmAngleFondGris' })
    }
    if (this.hasArc) {
      extraButtons.push({ txt: '', mes: 'ARC', className: 'zsmArcFondGris' })
    }
    this.buildKeyboardHelper(touches, { useExisting, extraButtons })
    if (useExisting) this.place(true)
  }

  cacheKlavier () {
    const node = this.divClavier
    node.style.visibility = 'hidden'
    node.style.height = '0'
  }

  /**
   * Reconstruit le clavier en invertissant min/maj
   */
  capsLock () {
    this.isCapsLocked = !this.isCapsLocked
    this.buildKeyboard(true)
  }

  clavier (event) {
    if (this.isblur) return
    if (this.disabled ||
      event.code === 'Tab' ||
      event.key === 'Tab' ||
      event.code === 'Enter' ||
      event.code === 'NumpadEnter'
    ) {
      if (this.enter) {
        if (event.code === 'Enter' ||
          event.code === 'NumpadEnter') {
          event.preventDefault()
          event.stopPropagation()
          this.blur()
          this.enter()
        }
      }
      if (event.key === 'Tab' || event.code === 'Tab') {
        this.blur()
        prevTabFocus(this)
        event.preventDefault()
        event.stopPropagation()
      }
      return
    }
    // pourquoi on désactive la propagation ?
    event.preventDefault()
    event.stopPropagation()

    // pourquoi tester ça ici, ça correspond pas au event.code === 'Enter' déjà traité ?
    // je crois que ya des navigateurs qui utilisent pas event.code , mais je sais plus ca fait longtemps
    if (event.key === 'Enter') return
    /*
      var convpoursafari = [];
      convpoursafari[43]='+';
      convpoursafari[45]='-';
      convpoursafari[48]='0';
      convpoursafari[49]='1';
      convpoursafari[50]='2';
      convpoursafari[51]='3';
      convpoursafari[52]='4';
      convpoursafari[53]='5';
      convpoursafari[54]='6';
      convpoursafari[55]='7';
      convpoursafari[56]='8';
      convpoursafari[87]='9';
      convpoursafari[47]='/';
      convpoursafari[42]='*';
      convpoursafari[46]=',';
      convpoursafari[44]=',';
      if (event.key==undefined){event.key=convpoursafari[event.which];}
      */

    let muo = event.key
    if (muo === 'Digit0') { muo = '0' }
    if (muo === ' ') { muo = ' ' }
    if (muo === '.' && this.modeText !== true) { muo = ',' }
    let monres = this.restric
    if (this.hasAngle) monres += '^'
    if (this.hasArc) monres += '$'
    if (muo === 'Dead') muo = '^'
    if (muo === '^' && monres.includes('µ')) muo = 'µ'
    if ((monres.includes(muo))) {
      this.okMod = muo !== ''
      this.majaffiche(muo)
    }

    let u
    if (event.code === 'ArrowRight') {
      if (this.texta.poscurseur < this.texta.elemnum.length) {
        if ((this.texta.elemnum[this.texta.poscurseur].length > 1)) {
          u = this.texta.elemnum[this.texta.poscurseur]
          this.texta.elemnum[this.texta.poscurseur] = u.substring(0, 1)
          this.texta.elemnum.splice(this.texta.poscurseur + 1, 0, u.substring(1, u.length))
        }
        this.texta.poscurseur++
      }
      this.majaffiche('')
    }
    if (event.code === 'ArrowLeft') {
      if (this.texta.poscurseur > 0) {
        if ((this.texta.elemnum[this.texta.poscurseur - 1].length > 1)) {
          u = this.texta.elemnum[this.texta.poscurseur - 1]
          this.texta.elemnum[this.texta.poscurseur - 1] = u.substring(0, u.length - 1)
          this.texta.elemnum.splice(this.texta.poscurseur, 0, u.substring(u.length - 1, u.length))
          this.texta.poscurseur++
        }
        this.texta.poscurseur--
      }
      this.majaffiche('')
    }
    if (event.code === 'Backspace') {
      if (this.texta.poscurseur > 0) {
        if ((this.texta.elemnum[this.texta.poscurseur - 1].length > 1)) {
          this.texta.elemnum[this.texta.poscurseur - 1] = this.texta.elemnum[this.texta.poscurseur - 1].substring(0, (this.texta.elemnum[this.texta.poscurseur - 1].length - 1))
        } else {
          this.texta.elemnum.splice((this.texta.poscurseur - 1), 1)
          this.texta.poscurseur--
        }
        if (this.modifL) this.modifL()
      }
      this.okMod = true
      this.majaffiche('')
    }
    if (event.code === 'Delete') {
      if (this.texta.elemnum.length > this.texta.poscurseur) {
        if ((this.texta.elemnum[this.texta.poscurseur].length > 1)) {
          this.texta.elemnum[this.texta.poscurseur] = this.texta.elemnum[this.texta.poscurseur].substring(1, (this.texta.elemnum[this.texta.poscurseur].length))
        } else {
          this.texta.elemnum.splice(this.texta.poscurseur, 1)
        }
        if (this.modifL) this.modifL()
      }
      this.okMod = true
      this.majaffiche('')
    }

    if (this.afaire !== undefined) {
      this.afaire(this.reponsechap())
    }
  }

  clickKlav (event) {
    this._isLocked = true
    setTimeout(() => {
      this._isLocked = false
      this.majaffiche('')
    }, 20)
    if (event.currentTarget.mes === undefined) return
    const add = event.currentTarget.mes
    if (add === 'MAJ') return this.capsLock()
    if (add === 'ANG') {
      return this.clavier({
        key: '^',
        code: '^',
        stopPropagation: dummy,
        preventDefault: dummy
      })
    }
    if (add === 'ARC') {
      return this.clavier({
        key: '$',
        code: '$',
        stopPropagation: dummy,
        preventDefault: dummy
      })
    }
    const fakeEvent = { key: add, code: add, stopPropagation: dummy, preventDefault: dummy }
    this.clavier(fakeEvent)
  }

  clickc (event) {
    if (this.disabled) { return }
    // e.preventDefault()
    // e.stopPropagation()
    const { x, width } = event.currentTarget.getBoundingClientRect()
    if (event.clientX > x + width / 2) {
      this.texta.poscurseur = event.currentTarget.num + 1
    } else {
      this.texta.poscurseur = event.currentTarget.num
    }
    this.texta.poscurseurIn = false
    this.texta.poscurseurInRac = false
    this.unTAbc[0][(event.currentTarget.num + 1) * 2].focus()
  }

  clickzone () {
    if (this.disabled) return
    this.isblur = false
    this.textaCont.setAttribute('class', 'zsmMqFocused')
    this.majaffiche('')
  }

  corrige (bon) {
    super.corrige(bon)
    if (bon) {
      if (this.chapo === 1) {
        this.unTAb.classList.add('zsmAngleVert')
        this.unTAb.classList.remove('zsmAngle')
      } else if (this.chapo === 2) {
        this.unTAb.classList.add('zsmArcVert')
        this.unTAb.classList.remove('zsmArc')
      }
    } else {
      if (this.chapo === 1) {
        this.unTAb.classList.add('zsmAngleOrange')
        this.unTAb.classList.remove('zsmAngle')
      } else if (this.chapo === 2) {
        this.unTAb.classList.add('zsmArcOrange')
        this.unTAb.classList.remove('zsmArc')
      }
    }
  }

  disable () {
    if (this.disabled) { return }
    this.blur()
    removeTab(this)
    document.removeEventListener('click', this._gereBlurListener, false)
    this.disabled = true
    this.textaCont.setAttribute('class', '')
    this.textaCont.removeEventListener('click', this._clickZoneListener, false)
    j3pDetruit(this.textaIm)
    this.cacheKlavier()
    j3pDetruit(this.divClavier, this.divClavierAuto)
    for (let i = 0; i < this.texta.elemnum.length; i++) {
      j3pFreezeElt(this.unTAbc[0][i * 2 + 1])
    }
  }

  focus () {
    if (this.disabled) { return }
    blurAllTabs(this)
    this.isblur = false
    this.okMod = false
    setTimeout(() => {
      this.majaffiche('')
      this.textaIm.style.display = ''
      if (typeof this.onModif === 'function') {
        this.onModif()
      }
    }, 0)
  }

  gereblur (event) {
    if (!$(event.target).closest('#' + this.textaCont.id).length) {
      this.blur()
    }
  }

  majaffiche (elem) {
    if (this.disabled) return
    if (elem === '^' && this.hasAngle) {
      // on passe à 0 si c’est 1 et fixe 1 sinon
      this.chapo = (this.chapo === 1) ? 0 : 1
    } else if (elem === '$') {
      this.chapo = (this.chapo === 2) ? 0 : 2
    } else if (elem !== '') {
      if (this.texta.elemnum.length > this.limite) {
        // on le passe sur fond rouge
        const eLemCoNt = this.textaCont
        // on le passe sur fond rouge
        eLemCoNt.style.backgroundColor = '#ff0000'
        // et on le remettra en blanc dans 100ms
        setTimeout(() => { eLemCoNt.style.backgroundColor = '#ffffff' }, 100)
        return
      }
      if (elem === 'µ') elem = '^'
      this.texta.elemnum.splice(this.texta.poscurseur, 0, elem)
      this.texta.poscurseur++
    }
    /// verif 2 chiffres cote a cote
    if (elem !== '') {
      this.bon = undefined
    }
    if (this.okMod && typeof this.onModif === 'function') {
      this.okMod = false
      this.onModif()
    }

    this.majAfficheHelper2()
    if (this.sansBord) this.textaCont.style.border = '0px solid black'
    const clickcListener = this.clickc.bind(this)
    const metCurseurListener = this.metcurseur.bind(this)
    const clavierListener = this.clavier.bind(this)
    const vireListener = this.vire.bind(this)
    let bufel, elti, eltCi, i
    for (i = 0; i < this.texta.elemnum.length; i++) {
      bufel = this.texta.elemnum[i]
      elti = this.unTAbc[0][2 * i]
      eltCi = this.unTAbc[0][2 * i + 1]
      if (bufel.includes('widehat')) {
        j3pStyle(eltCi, { fontSize: '85%' })
        eltCi.vAlign = 'top'
      }
      if (bufel === ' ' || bufel === ' ') {
        eltCi.innerHTML = '&nbsp;'
      } else if (bufel === '^') {
        eltCi.innerHTML = '^'
      } else {
        j3pAffiche(eltCi, null, '$' + this.texta.elemnum[i] + '$')
      }
      eltCi.num = i
      eltCi.num2 = 0
      elti.num = i
      elti.num2 = 0
      elti.tabIndex = 0
      eltCi.addEventListener('click', clickcListener, false)
      elti.addEventListener('focus', metCurseurListener, false)
      elti.addEventListener('keydown', clavierListener, false)
      // @todo expliquer pourquoi il faut ajouter un listener qui supprime la propagation de l’événement pour keypress et keyup
      elti.addEventListener('keypress', vireListener, false)
      elti.addEventListener('keyup', vireListener, false)
      eltCi.classList.add('noMarginPadding')
    }
    // en fait faut faire une fois de plus pour la dernière zone de saisie (tout à droite)
    // exactement la mm boucle que au dessus je la met à la fin
    elti = this.unTAbc[0][this.unTAbc[0].length - 1]
    elti.tabIndex = 0
    elti.addEventListener('focus', metCurseurListener, false)
    elti.addEventListener('keydown', clavierListener, false)
    elti.addEventListener('keypress', vireListener, false)
    elti.addEventListener('keyup', vireListener, false)
    elti.classList.add('noMarginPadding')
    if (i === 0) { this.unTAbc[0][0].innerHTML = '&nbsp;&nbsp;' }

    this.textaIm.addEventListener('mousedown', this.place.bind(this, false), false)
    this.textaIm.style.cursor = 'pointer'

    this.textaTab.style.width = (Math.min(this.textaCont.offsetWidth, 10) + this.textaIm.offsetWidth) + 'px'

    // mpol
    this.textaCont.classList.add('zsmMqFocused')
    this.textaIm.classList.add('zsmMq')

    if (this.chapo === 0) {
      this.unTAb.classList.remove('zsmAngle')
      this.unTAb.classList.remove('zsmArc')
    } // ça va virer toutes les autres
    if (this.chapo === 1) {
      const gg = this.bon ? 'zsmAngleVert' : 'zsmAngle'
      this.unTAb.classList.add(gg)
    }
    if (this.chapo === 2) {
      const gg = this.bon ? 'zsmArcVert' : 'zsmArc'
      this.unTAb.classList.add(gg)
    }

    this.majAfficheHelper()

    if (!this.isblur) {
      this.unTAbc[0][this.texta.poscurseur * 2].focus()
    }
    if (this.tabauto && elem !== '') {
      this.blur()
      prevTabFocus(this)
    }
    if (this.bon === false) this.corrige(false)
    if (this.modifL && elem !== '' && !this.isRest) this.modifL()
  } // majaffiche

  metcurseur (event) {
    // on est mis en listener avec du bind, donc notre this est bien l’this ZoneStyleMathquill1
    if (this.disabled) return
    // pour récupérer le this qu’on aurait eu sans le bind (l’élément sur lequel on a appelé addEventListener)
    // on utilise currentTarget (target est l’élément sur lequel on a déclenché l’événement, il peut être un enfant de currentTarget)
    // cf https://developer.mozilla.org/fr/docs/Web/API/EventTarget/addEventListener#la_valeur_de_this_%C3%A0_lint%C3%A9rieur_du_gestionnaire
    event.currentTarget.className = 'zsmMqFocusedCursor'
  }

  modif (items) {
    this.texta.elemnum = []
    this.texta.poscurseur = 0
    for (const item of items) {
      this.majaffiche(item.replace(' ', ' '))
    }
    this.majaffiche('')
  }

  reponse () {
    let rep = ''
    for (const item of this.texta.elemnum) {
      if (item === '{}^2') {
        rep += '^{2}'
      } else {
        if (item === '{}^3') {
          rep += '^{3}'
        } else {
          rep += item
        }
      }
    }

    return rep
  }

  reponsechap () {
    return [this.reponse(), this.chapo]
  }

  reset (items) {
    this.isRest = true
    this.texta.elemnum = [...items]
    this.okMod = true
    this.texta.poscurseur = 0
    this.majaffiche('')
    this.isRest = false
  }

  vire (event) {
    event.preventDefault()
    event.stopPropagation()
  }
}

export default ZoneStyleMathquill1