/**
* Ce fichier définit les tags html <mathgraph-player> et <mathgraph-editor>, permettant d'insérer des figures avec par ex
* `<mathgraph-player" fig="le code base 64 de la figure" />`
* ou
* `<mathgraph-editor fig="le code base 64 de la figure pour l'ouvrir dans l'éditeur" />`
* @see https://developer.mozilla.org/fr/docs/Web/Web_Components/Using_custom_elements
* @see https://javascript.info/custom-elements
* @see https://web.dev/custom-elements-best-practices/
* @fileOverview
*/
// ATTENTION, ce fichier ne passe pas par vite, il est juste compressé avec Terser par compressMathgraphElements.js (appelé par postBuild.js)
// Il faut donc faire attention à ne pas utiliser de code js trop moderne ici
// (tous les navigateurs qui gèrent customElements doivent au moins gérer ES2017).
// on est chargé comme script indépendant, d'où la bonne vieille IIFE
(function mathgraphElements () {
if (typeof window === 'undefined') throw Error('La définition de ce composant mathgraph ne peut être appelée que dans un navigateur')
const customElements = window.customElements
if (typeof customElements === 'undefined') throw Error('Ce navigateur ne permet pas d’utiliser les composants html5 personnalisés')
/**
* Alias de document.createElement
* @private
* @param {string} tag
* @returns {HTMLElement}
*/
const ce = (tag) => document.createElement(tag)
/**
* Les options de mtgOptions booléens true par défaut
* @private
* @type {string[]}
*/
const optsTrue = ['displayMeasures', 'isInteractive', 'pointsAuto', 'codeBase64', 'open', 'save', 'newFig', 'options']
const optsFalse = ['randomOnInit', 'stylePointCroix', 'onlyPoints', 'dys', 'construction', 'hideCommands']
const optsString = ['language']
const optsNumber = ['level', 'zoomFactor']
const optsFunction = ['functionOnSave', 'callBackAfterReady']
const optToAttr = {}
const attrToOpt = {}
for (const opt of optsTrue.concat(optsFalse, optsString, optsNumber, optsFunction)) {
const attr = opt.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase())
optToAttr[opt] = attr
attrToOpt[attr] = opt
}
const o2a = opt => optToAttr[opt]
const attrsTrue = optsTrue.map(o2a)
const attrsFalse = optsFalse.map(o2a)
const attrsString = optsString.map(o2a)
const attrsNumber = optsNumber.map(o2a)
const attrsFunction = optsFunction.map(o2a)
const jsAdded = {}
// let mtgNum = 0
function addJs (url, { timeout = 30_000 } = {}) {
if (!jsAdded[url]) {
jsAdded[url] = new Promise((resolve, reject) => {
function onLoad () {
clearTimeout(timerId)
elt.removeEventListener('load', onLoad)
resolve()
}
/**
* @private
* @type {HTMLElement}
*/
const body = window.document.getElementsByTagName('body')[0]
const elt = ce('script')
elt.async = true
body.appendChild(elt)
elt.addEventListener('load', onLoad)
const timerId = setTimeout(() => reject(Error(`Timeout, ${url} n’a pas été chargé en ${Math.round(timeout / 1000)}s`)), timeout)
elt.src = url
})
}
return jsAdded[url]
}
// Par défaut un customElement est de type inline.
// On veut qu'il soit en block par défaut, mais laisser à l'utilisateur la possibilité de le mettre en inline ou inline-block
// sans casser le positionnement de nos input (d'où l'ajout d'un div entre notre customElement et le svg)
// On pourrait hériter de HTMLElement et le forcer en block dans connectedCallback,
// mais ça interdirait à l'utilisateur d'utiliser une figure en inline.
/**
* La classe du custom element mathgraph-player
* @class
*/
class MathgraphPlayer extends HTMLElement {
// la plupart des exemples de customElements utilisent un shadowRoot attaché à l'élément dans son constructeur
// ici on en a pas besoin (on ne veut pas isoler les éléments internes de notre élément du reste du dom)
// le constructeur est donc inutile.
/**
* Retourne la liste des attributs pour lesquels toute modif devra appeler attributeChangedCallback
* @type {string[]}
*/
static get observedAttributes () {
// les noms d'attributs doivent être lowercase sans underscore (tiret autorisé)
// cf https://html-validate.org/rules/attr-pattern.html
return ['fig', 'python-code-id', 'javascript-code-id', 'hide-commands']
}
/**
* Appelé quand notre tag est mis dans le dom.
* Attention, son rendu n'est pas fini, à ce stade il n'a aucun child, son innerHTML est null même si y'en avait
*/
connectedCallback (/* retry = false */) {
// on doit être sûr que le container filé à mtgLoad soit de type block et position: 'relative'
// console.log('connectedCallback')
this.mtgContainer = ce('div')
// pour empêcher une modif via du css on met ça dans l'attribut style
// pour le modifier l'utilisateur devra le faire en js après injection dans la page, là il fait exprès et c'est à ses risques et périls
this.mtgContainer.setAttribute('style', 'position: relative;')
this.appendChild(this.mtgContainer)
// if (!this.id) this.id = `mtg${mtgNum++}` // à réactiver pour le debug (si on décommente un console.log)
// console.log( 'connected', this.id)
// il faut un setTimeout de 0 pour que le display soit lancé quand l'élément ET SON CONTENU (même vide) est vraiment dans le dom
// sinon mtgLoad met dedans un svg et ne le retrouve pas quand il get son id plus tard avec addDoc
// mais il ne faut rien lancer s'il n'y a pas encore d'attribut (les ajouter va relancer un display)
if (this instanceof MathgraphEditor || MathgraphPlayer.observedAttributes.some(attr => this.getAttribute(attr))) {
setTimeout(this.display.bind(this), 0)
}
}
/**
* Appelé lorsque notre tag est viré du dom
*/
disconnectedCallback () {
// console.trace('disconnected', this.id)
this.remove()
}
attributeChangedCallback (/* name, oldValue, newValue /* */) {
// console.log('attributeChanged', name, oldValue, 'devenu', newValue)
// ATTENTION :
// 1) avec un custom-element déjà dans le html au chargement
// - on est appelé au 1er chargement juste avant connectedCallback (mais isConnected est déjà true),
// il ne faut pas appeler display deux fois (sinon le premier plante parce que le 2e
// lui a vidé son container quand il veut mettre qqchose dedans)
// - this.isConnected est true, mais connectedCallback n'a pas encore rendu la main
// => on affecte mtgContainer dans connectedCallback et on ne fait rien ici si cet id n'existe pas
// 2) avec un custom-element mis en js, on peut être appelé juste après connectedCallback
// => il ne doit pas lancer de display dans ce cas
if (!this.isConnected || !this.mtgContainer) return
setTimeout(this.display.bind(this), 0)
}
/**
* Supprime tous les childs de notre élément (efface donc la figure si y'en avait une, et passe notre propriété mtgContainer à null)
*/
remove () {
// console.log('remove sur', this.id)
for (const child of this.childNodes) this.removeChild(child)
this.mtgContainer = null
this.app = null
}
/**
* Affiche la figure
* @param {MtgOptions} mtgOptionsSup les options passées par le display de l'appli (jamais utilisé pour le lecteur)
* @returns {Promise<void>}
*/
async display (mtgOptionsSup) {
// console.log('display', mtgOptionsSup)
try {
if (!this.mtgContainer) throw Error('L’élément n’a pas été correctement initialisé, impossible d’afficher la figure')
// on pourrait boucler sur les attributs observés
// for (const attr of this.constructor.observedAttributes) { }
// mais il faudrait ensuite des if pour gérer le type
// Pour le player y'en a que 4, on gère manuellement
const fig = (this.getAttribute('fig') ?? '').trim()
const pythonCodeId = this.getAttribute('python-code-id')
const javascriptCodeId = this.getAttribute('javascript-code-id')
// hideCommands plus loin si y'a du code
// depuis la version 7.3.5 MtgApp accepte une string vide comme figure (et démarre alors avec un repère orthonormal)
// on ne fait rien si y'a rien à faire (on nous passera p'tet des attributs plus tard)
if (!fig && !pythonCodeId && !javascriptCodeId && !(this instanceof MathgraphEditor)) return
const svgId = this.getAttribute('svgid') // éventuellement null, pas gênant
if (typeof window.mtgLoad !== 'function') {
// attention à mettre le même test que dans src/mtgLoad.preload.js
const mtgPublicPath = window.mtgPublicPath || (
/(local(host)?|\.sesamath.dev|dev.mathgraph32.org)$/.test(window.location.hostname)
? 'https://dev.mathgraph32.org/js/mtgLoad/' // dev
: 'https://www.mathgraph32.org/js/mtgLoad/' // prod
)
const mtgUrl = `${mtgPublicPath}mtgLoad.min.js`
await addJs(this.getAttribute('mtgUrl') || window.mtgUrl || mtgUrl)
if (typeof window.mtgLoad !== 'function') throw Error('Le chargement de Mathgraph a échoué (pas récupéré de fonction mtgLoad)')
}
// on construit svgOptions d'après les attribut de notre tag <mathgraph-xxx>
const svgOptions = { svgId }
const widthAttr = this.getAttribute('width')
if (widthAttr) {
const width = Number(widthAttr)
if (Number.isFinite(width) && width > 0) svgOptions.width = width
else (console.error(Error(`attribut width ${widthAttr} invalide`)))
}
const heightAttr = this.getAttribute('height')
if (heightAttr) {
const height = Number(heightAttr)
if (Number.isFinite(height) && height > 0) svgOptions.height = height
else (console.error(Error(`attribut height ${heightAttr} invalide`)))
}
// idem avec mtgOptions
const mtgOptions = {
isEditable: false
}
// on affecte les attributs récupérés
if (fig) mtgOptions.fig = fig
if (pythonCodeId || javascriptCodeId) {
if (pythonCodeId) mtgOptions.pythonCodeId = pythonCodeId
else mtgOptions.javascriptCodeId = javascriptCodeId
// avec du code fourni, on regarde le dernier attribut surveillé
mtgOptions.hideCommands = this.getAttribute('hide-commands') === 'true'
}
// et les éventuelles options de l'éditeur
if (mtgOptionsSup) {
Object.assign(mtgOptions, mtgOptionsSup)
}
// avant d'afficher il faut attendre que le précédent affichage ait terminé, sinon ça peut se crêper le chignon
if (this.app) await this.app.abort()
// on vire les erreurs précédentes éventuelles
if (this.errorContainer) while (this.errorContainer.lastChild) this.errorContainer.removeChild(this.errorContainer.lastChild)
// et on affiche
this.app = await window.mtgLoad(this.mtgContainer, svgOptions, mtgOptions)
} catch (error) {
console.error(error)
// on ajoute quand même un message sur la page (dans notre élément)
if (!this.errorContainer) this.errorContainer = ce('div')
// on laisse ce appendChild hors du if précédent au cas où qqun aurait vidé le contenu de notre élément
// (si errorContainer est déjà dans l'élément ça ne fait que le remettre en dernier child)
this.appendChild(this.errorContainer)
const p = ce('p')
p.style.color = 'red'
p.appendChild(document.createTextNode('Il y a eu une erreur au chargement de cette figure Mathgraph :'))
p.appendChild(ce('br'))
p.appendChild(document.createTextNode(error.message))
this.errorContainer.appendChild(p)
}
}
}
/**
* La classe du custom element mathgraph-editor
* @class
*/
class MathgraphEditor extends MathgraphPlayer {
// il a bcp plus d'attributs
static get observedAttributes () {
return [...attrsTrue, ...attrsFalse, attrsString, ...attrsNumber, ...attrsFunction]
}
// display est la seule méthode différente pour MathgraphEditor
/**
* Affiche l’éditeur avec la figure
* @returns {Promise<MtgApp>}
*/
display () {
// on ajoute simplement les options supplémentaires de l'éditeur
const mtgOptions = {
isEditable: true
}
for (const attr of attrsTrue) {
if (this.getAttribute(attr) === 'false') {
const opt = attrToOpt[attr] ?? attr
mtgOptions[opt] = false
}
}
// les booléens false par défaut (on ignore local, loadCoreOnly et loadCoreWithMathJax qui n'ont pas de sens dans ce contexte)
for (const attr of attrsFalse) {
if (this.getAttribute(attr) === 'true') {
const opt = attrToOpt[attr] ?? attr
mtgOptions[opt] = true
}
}
// les string et number
for (const attr of attrsString.concat(attrsNumber)) {
const value = this.getAttribute(attr)
if (value) {
const opt = attrToOpt[attr] ?? attr
mtgOptions[opt] = attrsString.includes(opt) ? value : Number(value)
}
}
// les fonctions, qui du coup doivent être globales…
for (const attr of attrsFunction) {
const functionName = this.getAttribute(attr)
if (typeof window[functionName] === 'function') {
const opt = attrToOpt[attr] ?? attr
mtgOptions[opt] = window[functionName]
}
}
super.display(mtgOptions)
}
}
// On définit l'élément (le tiret dans le nom est obligatoire)
if (!customElements.get('mathgraph-player')) {
customElements.define('mathgraph-player', MathgraphPlayer)
customElements.define('mathgraph-editor', MathgraphEditor)
}
})()