import { j3pAddElt, j3pDetruit, j3pElement } from 'src/legacy/core/functions'
import 'src/legacy/outils/menuContextuel/menuContextuel.css'
import { getJ3pConteneur } from 'src/lib/core/domHelpers'
import { j3pAffiche } from 'src/lib/mathquill/functions'
import { isHtmlElement } from 'src/lib/utils/dom/main'
/**
* Une callback de menu, son this sera l’élément cliqué
* @callback MenuContextuelCallback
* @param {MenuContextuel} menu L’instance courante du menu
* @param {MenuContextuelChoice} choice Le choix sur lequel on a cliqué (tel qu’on l’a passé au constructeur)
* @param {Event} event L’événement click
*/
/**
* @typedef MenuContextuelChoice
* @type Object
* @property {number} i
* @property {HTMLElement} elt
* @property {string} [label]
* @property {MenuContextuelCallback} [callback]
*/
/**
* Menu contextuel qui apparaît au clic sur un élément
* @class MenuContextuel
* @constructor
*/
const dummyFn = () => {}
/**
*
* @param trigger
* @param choices
* @param {Object} props
* @param {Object} [props.style]
* @param {Array} props.infos à documenter
* @param {string} props.id id à mettre sur le div qui contiendra les éléments de menu
* @param {Object} options
* @param {boolean} [options.stayOpenOnClick=false] Passer true pour ne pas fermer le menu à chaque clic sur un de ses éléments
* @param {boolean} [options.pasToucheBackground=false] Passer true pour ne pas toucher au background du trigger
* @param {boolean} [options.colle=false] Passer true que le menu soit collé au trigger
* @param {boolean} [options.callBackOpen = () => {}] function appelée à l’ouverture du menu
* @param {boolean} [options.callBackClose = () => {}] function appelée à la fermeture du menu
* @param {HTMLElement} [options.j3pCont] Pour imposer le conteneur (sinon on utilise getJ3pConteneur() ou on prend .j3pContainer)
* @constructor
*/
function MenuContextuel (trigger, choices, props, options) {
if (typeof this !== 'object') throw Error('Constructeur qui doit être appelé avec new')
// retourne un listener prêt pour addEventListener, pour les clics sur les éléments de menu
function getListener (callback, index) {
return function (event) {
// on ferme le menu
if (!options.stayOpenOnClick) menu.hide()
// on applique notre this (l’élément) à la callback, et on lui passe le reste en argument
callback.call(this, menu, choices[index], event)
}
}
// retourne true si on est un elt du trigger ou du menu (pour que le window.onClick ne fasse rien)
function isMenuChild (elt) {
while (elt) {
if (elt === menu.container || elt === menu.trigger) return true
elt = elt.parentNode
}
return false
}
const menu = this
if (!props) props = {}
if (!props.style) props.style = {}
if (!options) options = {}
this.callBackOpen = options.callBackOpen || dummyFn
this.callBackClose = options.callBackClose || dummyFn
this.pasToucheBackground = Boolean(options.pasToucheBackground === true)
this.dep = (options.colle === true) ? 0 : 3
/**
* @todo À documenter et typer
* @type {*}
*/
this.infos = props.infos
/**
* L’élément sur lequel on va ajouter notre listener onClick
* @type {HTMLElement|string}
*/
this.trigger = (typeof trigger === 'string') ? j3pElement(trigger) : trigger
if (!isHtmlElement(trigger)) return console.error(Error('Pas de trigger (ou trigger invalide), impossible de créer un menu contextuel'))
this.triggerBgColorDefault = getComputedStyle(this.trigger).backgroundColor
this.mepact = getJ3pConteneur(trigger, true) || j3pElement('j3pContainer') || options.j3pCont
if (!this.mepact) throw Error('Pas trouvé de conteneur')
if (this.mepact.URL) this.mepact = j3pElement('j3pContainer') || options.j3pCont
const { height: trigH, x: trigX, y: trigY } = trigger.getBoundingClientRect()
const { x: mainX, y: mainY } = this.mepact.getBoundingClientRect()
// on crée un div en absolute positionné juste dessous
props.style.display = 'none'
props.style.position = 'absolute'
props.style.left = (trigX - mainX) + 'px'
props.style.top = (trigY - mainY + trigH + 3) + 'px'
this.container = j3pAddElt(this.mepact, 'div', '', props)
// on le fait juste après (et pas imposé dans les props) au cas où on nous aurait déjà passé des classes css
this.container.classList.add('menuContextuel')
// et on ajoute les éléments dedans
let elt
const labelProps = {
style: {
position: 'relative'
}
}
/**
* Liste des choix
* @type {MenuContextuelChoice[]}
*/
this.listeChoicesMenu = []
const objet = this
for (const [i, choice] of choices.entries()) {
// attention, dans ce forEach on a perdu this
// check
if (typeof choice !== 'object' || typeof choice.label !== 'string' || typeof choice.callback !== 'function') {
console.error(Error('choix invalide'), choice)
continue
}
// ok, on ajoute l’élément
if (/\$[^$]+\$/.test(choice.label)) {
// en 2 temps pour passer j3pAffiche sur le label
elt = j3pAddElt(menu.container, 'div', null, labelProps)
j3pAffiche(elt, '', choice.label)
} else {
elt = j3pAddElt(menu.container, 'div', choice.label, labelProps)
}
// ici on n’a pas besoin de référencer les listeners qu’on ajoute car on aura pas besoin de les retirer
// (à la destruction du menu js les vire avec leur élément support)
// Sinon il faudrait mettre les résultats de getListener dans une variable.
elt.addEventListener('click', getListener(choice.callback, i))
objet.listeChoicesMenu.push({ i, elt })
}
// un booléen pour désactiver les callback
this.isActive = true
// nos listeners
this.onClickMenu = this.toggle.bind(this)
// clic sur le déclencheur
this.trigger.addEventListener('click', this.onClickMenu)
// un écouteur sur window pour refermer le menu au clic en dehors
this.onClickWindow = function (event) {
// this est ici l’élément sur lequel on a mis l’écouteur
// event.target l’élément sur lequel on a cliqué
if (isMenuChild(event.target)) return
// on a cliqué sur autre chose qu’un item du menu => on ferme le menu
menu.hide()
}
window.addEventListener('click', this.onClickWindow)
// et un écouteur sur le resize pour repositionner notre div en absolu
this.onResize = this.replace.bind(this)
window.addEventListener('resize', this.onResize)
}
MenuContextuel.prototype.show = function () {
if (!this.container) return console.error(Error('Ce menu a été détruit'))
// on replace le menu au cas où le trigger aurait changé de place
this.replace()
this.container.style.display = 'block'
if (!this.pasToucheBackground) this.trigger.style.backgroundColor = '#ddf' // cf css .menuContextuel div:hover
this.callBackOpen()
}
MenuContextuel.prototype.hide = function () {
if (!this.container) return console.error(Error('Ce menu a été détruit'))
this.container.style.display = 'none'
if (!this.pasToucheBackground) this.trigger.style.backgroundColor = this.triggerBgColorDefault
this.callBackClose()
}
/**
* Affiche ou masque le menu
*/
MenuContextuel.prototype.toggle = function () {
if (!this.isActive) return
this.isHidden() ? this.show() : this.hide()
}
/**
* Détruit le menu (qui ne peut plus être utilisé ensuite),
* Il faut supprimer les variables qui pointent dessus pour que le js puisse le supprimer en RAM.
*/
MenuContextuel.prototype.destroy = function () {
if (!this.container) return console.error(Error('Ce menu a été détruit'))
this.trigger.removeEventListener('click', this.onClickMenu)
window.removeEventListener('click', this.onClickWindow)
window.removeEventListener('resize', this.onResize)
j3pDetruit(this.container)
this.container = null
}
MenuContextuel.prototype.isHidden = function () {
if (!this.container) return console.error(Error('Ce menu a été détruit'))
return this.container.style.display === 'none'
}
MenuContextuel.prototype.setActive = function (isActive) {
if (!this.container) return console.error(Error('Ce menu a été détruit'))
this.isActive = Boolean(isActive)
}
/**
* À appeler si le trigger qu’on avait passé au constructeur a changé de place
*/
MenuContextuel.prototype.replace = function () {
if (!this.container) return console.error(Error('Ce menu a été détruit'))
const triggerPos = this.trigger.getBoundingClientRect()
const { x: mainX, y: mainY } = this.mepact.getBoundingClientRect()
// on crée un div en absolute positionné juste dessous
this.container.style.left = (triggerPos.x - mainX) + 'px'
this.container.style.top = (triggerPos.y - mainY + triggerPos.height + this.dep) + 'px'
}
export default MenuContextuel