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 <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 = ' '
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