// qq fcts utiles pour manipuler le dom
import { ce } from './kernel'
/**
* Créer un élément de type tag, l'ajoute dans ct et le retourne
* @param {HTMLElement} ct
* @param {string} tag
* @param {string} [textContent] Du texte éventuel à ajouter dans l'élément
* @return {HTMLElement}
*/
export function addElt (ct, tag, textContent = '') {
if (!ct) return null
const elt = ce(tag)
ct.appendChild(elt)
if (textContent) elt.appendChild(ctn(textContent))
return elt
}
/**
* Ajoute une image dans ct
* @param {HTMLElement} ct
* @param {string} imgName Le nom de l'image (dans src/images, si y'a pas d'extension on ajoute .png)
* @param {object} options
* @param {string} [options.alt]
* @param {string} [options.width]
* @param {string} [options.height]
* @returns {HTMLImageElement}
*/
export function addImg (ct, imgName, { alt = '', width = 22, height = 22 } = {}) {
const img = ce('img')
img.setAttribute('alt', alt)
if (width) img.setAttribute('width', `${width}px`)
if (height) img.setAttribute('height', `${height}px`)
ct.appendChild(img)
// on ajoute l'image avec un import dynamique, mais il faut préciser l'extension dans l'import (vite l'exige)
let suffix = 'png'
if (imgName.includes('.')) {
// on pourrait avoir deux points dans le nom, on ne fait donc pas de split mais une regexp
const [, f, s] = /(.*)\.([^.]*)$/.exec(imgName)
if (!['png', 'gif'].includes(s)) throw Error(`Suffixe ${s} non géré (png|gif seulement)`)
imgName = f
suffix = s
}
const p = suffix === 'gif'
? import(`src/images/${imgName}.gif`)
: import(`src/images/${imgName}.png`)
p.then(({ default: src }) => {
img.setAttribute('src', src)
})
.catch(error => console.error(error))
return img
}
/**
* Crée un TextNode et le retourne (sans l'ajouter dans le dom)
* @type {function}
* @param {string} text
* @return {Text}
*/
export const ctn = (text) => document.createTextNode(text)
/**
* Crée un TextNode et l'ajoute dans ct
* @param {HTMLElement} ct
* @param {string} text
*/
export function addText (ct, text) {
if (!ct) return console.error(Error('conteneur manquant'))
ct.appendChild(ctn(text))
}
/**
* Vide elt
* @param {HTMLElement} elt
*/
export function empty (elt) {
while (elt?.lastChild) elt.removeChild(elt.lastChild)
}
/**
* Retourne true si l'élément est en display none ou qu'il a un parent en display none (mais ça peut renvoyer false et être en visibility hidden)
* @param {Element|ParentNode} elt
* @returns {boolean}
* /
export function hasDisplayNone (elt) {
if (!elt) {
console.error(Error('isDisplayed appelé sans élément'))
return false
}
if (elt?.style?.display === 'none') return true
while (elt?.parentNode) {
if (elt.parentNode?.style?.display === 'none') return true
elt = elt.parentNode
}
return false
} /* */
/**
* Alias de getElementById (qui warn en console si ça n'existe pas,
* sauf avec lax=true où c'est silencieux)
* @param {string} id
* @param {boolean} [lax=false] Passer true pour rester silencieux si #{id} n'existe pas
* @returns {HTMLElement}
*/
export function ge (id, lax = false) {
const elt = document.getElementById(id)
if (!elt && !lax) console.warn(`Aucun élément #${id} dans le dom`)
return elt
}
/**
* Retourne un id qui n'existe pas dans le DOM (prefix + num)
* @param {string} prefix
* @returns {string}
*/
export function getNewId (prefix = 'mtg') {
let i = 1
while (ge(`${prefix}${i}`, true)) i++
return `${prefix}${i}`
}
/**
* Lance le chargement d'un script et résoud la promesse quand c'est fait (ou la rejette en cas de timeout)
* @param {string} url
* @param {object} [opts]
* @param {string} [opts.type=text/javascript] le type à mettre sur le tag script
* @param {object} [opts.timeout=30_000] timeout en ms
* @returns {Promise<void>}
*/
export function loadScript (url, opts) {
const type = opts?.type ?? 'text/javascript'
return new Promise((resolve, reject) => {
const script = ce('script', { type })
document.body.appendChild(script)
const timeout = opts?.timeout ?? 30_000
// on devrait virer le listener si on rejette, mais il sera visiblement jamais appelé,
// et s'il l'était ça changerait rien, la promesse sera déjà rejetée
const timerId = setTimeout(() => reject(Error(`Timeout : impossible de charger ${url} (après ${Math.round(timeout / 1000)}s d’attente)`)), timeout)
script.addEventListener('load', () => {
clearTimeout(timerId)
resolve()
}, { once: true })
script.src = url
})
}
/**
* Affecte les attributs attrs à l'élément elt
* @param {HTMLElement|SVGElement|string} elt
* @param {Object} attrs Les propriétés sont les noms des attributs (et les valeurs devraient être des strings)
*/
export function setAttrs (elt, attrs) {
const e = typeof elt === 'string' ? ge(elt) : elt
if (!e) return console.error(Error(`Impossible d’affecter des attributs à un élément inexistant ${typeof elt === 'string' ? `(#${elt})` : ''}`))
for (const [attr, value] of Object.entries(attrs)) {
try {
e.setAttribute(attr, value)
} catch (error) {
console.error(`Impossible d'affecter l’attribut ${attr} avec ${value} sur`, elt, error)
}
}
}
/**
* Affecte chaque propriété de style à l'élément
* @param {HTMLElement|SVGElement|string} elt
* @param {Object} style Les propriétés devraient être des propriété de style existantes pour l'élément et les valeurs des strings
*/
export function setStyle (elt, style) {
const e = typeof elt === 'string' ? ge(elt) : elt
if (!e) return console.error(Error(`Impossible d’affecter le style d’un élément inexistant ${typeof elt === 'string' ? `(#${elt})` : ''}`))
for (const [attr, value] of Object.entries(style)) {
try {
e.style[attr] = value
} catch (error) {
console.error(`Pb pour affecter ${value} dans style.${attr}`, error)
}
}
}
/**
* Copie text dans le presse papier (ne plante pas si ça marche pas, la promesse sera résolue avec false dans ce cas)
* @param {string} text
* @return {Promise<boolean>} true si ça a été copié, false sinon
*/
export async function copyToClipBoard (text) {
try {
if (navigator.clipboard) {
// Cf https://developer.mozilla.org/fr/docs/Web/API/Clipboard#disponibilit%C3%A9_du_presse-papiers
// il faudrait normalement utiliser l'api Permission
// https://developer.mozilla.org/fr/docs/Web/API/Permissions_API
// pour y accéder, mais tous les navigateurs ne l'implémentent pas de la même manière
// https://developer.mozilla.org/fr/docs/Web/API/Clipboard#disponibilit%C3%A9_du_presse-papiers
await navigator.clipboard.writeText(text)
return true // si writeText plante c'est parti dans le catch
}
return false
} catch (error) {
console.error(error)
return false
}
}
/**
* Copie le contenu de elt dans le presse papier (ne plante pas si ça marche pas, la promesse sera résolue avec false dans ce cas)
* @param {HTMLElement} elt
* @return {Promise<boolean>} true si ça a été copié, false sinon
*/
export async function copyContentToClipBoard (elt) {
try {
const text = elt.textContent
// on tente d'abord la méthode moderne
if (await copyToClipBoard(text)) return true
// on tente le deprecated execCommand
if (typeof document.execCommand === 'function') {
elt.select()
return document.execCommand('copy')
}
// ça veut pas :-/
return false
} catch (error) {
console.error(error)
return false
}
}
/**
* Impose le style du conteneur du svg, pour un calcul correct des positionnements
* (input & boites de dialogue)
* @param {HTMLElement} container
* @param {number} height
* @param {number} width
*/
export function fixSvgContainer ({ container, height, width, svg }) {
container.style.boxSizing = 'border-box'
// on lui impose un positionnement relatif, sauf s'il est déjà en absolu
if (container.style.position !== 'absolute') container.style.position = 'relative'
container.style.margin = '0'
container.style.padding = '0'
container.style.border = 'none'
// on lui colle la taille imposée
container.style.width = width + 'px'
container.style.height = height + 'px'
// et on passe au svg
svg.setAttribute('width', width)
svg.setAttribute('height', height)
// Ajouté par Yves version 6.6.1 pour même fonctionnement dans le player que dans l'application
svg.setAttribute('shape-rendering', 'geometricPrecision')
// il faut aussi le forcer à coller à container (qui sert pour le positionnement des inputs et boites de dialogue)
svg.setAttribute('style', `position:absolute;top:0;left:0;margin:0;padding:0;border:none;background-color:#ffffff;width:${width}px;height:${height}px;`)
}