/**
* @file events.js. Un système d'événements (John Resig - Secrets of a JS Ninja http://jsninja.com/)
* (La version originale du livre n'était pas complètement utilisable, nous avons corrigé certaines choses et rendu Closure Compiler compatible)
* Cela devrait fonctionner de façon très similaire aux événements de jQuery, mais c'est basé sur la version du livre qui n'est pas aussi
* robuste que celle de jquery, il y a donc probablement des différences.
*
* fichier events.js
* @module events
*/
import DomData from './dom-data' ;
import * as Guid from './guid.js' ;
import log from './log.js' ;
import window from 'global/window' ;
import document from 'global/document' ;
/**
* Nettoyer le cache de l'auditeur et les répartiteurs
*
* @param {Element|Objet} elem
* Élément à nettoyer
*
* @param {string} type
* Type d'événement à nettoyer
*/
function _cleanUpEvents(elem, type) {
if (!DomData.has(elem)) {
retour ;
}
const data = DomData.get(elem) ;
// Supprimer les événements d'un type particulier s'il n'y en a plus
if (data.handlers[type].length === 0) {
supprimer data.handlers[type] ;
// data.handlers[type] = null ;
// La définition de null provoquait une erreur avec data.handlers
// Supprimer le méta-manipulateur de l'élément
if (elem.removeEventListener) {
elem.removeEventListener(type, data.dispatcher, false) ;
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, data.dispatcher) ;
}
}
// Supprimer l'objet "événements" s'il n'y a plus de types
if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
supprimer data.handlers ;
supprimer data.dispatcher ;
supprimer les données.désactivées ;
}
// Enfin, supprimer les données de l'élément s'il n'y a plus de données
if (Object.getOwnPropertyNames(data).length === 0) {
DomData.delete(elem) ;
}
}
/**
* Passe en revue un tableau de types d'événements et appelle la méthode requise pour chaque type.
*
* @param {Fonction} fn
* La méthode d'événement que nous voulons utiliser.
*
* @param {Element|Objet} elem
* Élément ou objet auquel lier les récepteurs
*
* @param {string} type
* Type d'événement à lier.
*
* @param {EventTarget~EventListener} callback
* Écouteur d'événements.
*/
function _handleMultipleEvents(fn, elem, types, callback) {
types.forEach(function(type) {
// Appeler la méthode d'événement pour chacun des types
fn(elem, type, callback) ;
}) ;
}
/**
* Correction d'un événement natif pour qu'il ait des valeurs de propriété standard
*
* @param {Objet} événement
* Objet de l'événement à fixer.
*
* @return {Object}
* Objet d'événement fixe.
*/
export function fixEvent(event) {
if (event.fixed_) {
retour événement ;
}
function returnTrue() {
retourner vrai ;
}
function returnFalse() {
retourner faux ;
}
// Tester si une réparation est nécessaire
// Utilisé pour vérifier si !event.stopPropagation au lieu de isPropagationStopped
// Mais les événements natifs renvoient true pour stopPropagation, mais n'ont pas de
// d'autres méthodes attendues comme isPropagationStopped. Il semble y avoir un problème
// avec le code Javascript Ninja. Nous nous contentons donc d'ignorer tous les événements.
if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
const old = event || window.event ;
événement = {} ;
// Clone l'ancien objet afin de pouvoir modifier les valeurs event = {} ;
// IE8 n'aime pas que l'on modifie les propriétés des événements natifs
// Firefox retourne false pour event.hasOwnProperty('type') et autres props
// ce qui rend la copie plus difficile.
// TODO : Il est probablement préférable de créer une liste blanche d'accessoires d'événements
for (const key in old) {
// Safari 6.0.3 vous avertit si vous essayez de copier les couches X/Y obsolètes
// Chrome vous avertit si vous essayez de copier des données obsolètes keyboardEvent.keyLocation
// et webkitMovementX/Y
// Lighthouse se plaint si Event.path est copié
if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' &&
key !== 'webkitMovementX' && key !== 'webkitMovementY' &&
key !== 'path') {
// Chrome 32+ émet un avertissement si vous essayez de copier une valeur de retour dépréciée, mais
// nous voulons toujours le faire si preventDefault n'est pas pris en charge (IE8).
if ( !(key === 'returnValue' && old.preventDefault)) {
event[key] = old[key] ;
}
}
}
// L'événement s'est produit sur cet élément
if (!event.target) {
event.target = event.srcElement || document ;
}
// Gérer l'autre élément auquel l'événement est lié
if (!event.relatedTarget) {
event.relatedTarget = event.fromElement === event.target ?
event.toElement :
event.fromElement ;
}
// Arrêter l'action du navigateur par défaut
event.preventDefault = function() {
if (old.preventDefault) {
old.preventDefault() ;
}
event.returnValue = false ;
old.returnValue = false ;
event.defaultPrevented = true ;
};
event.defaultPrevented = false ;
// Arrêter le bouillonnement de l'événement
event.stopPropagation = function() {
if (old.stopPropagation) {
old.stopPropagation() ;
}
event.cancelBubble = true ;
old.cancelBubble = true ;
event.isPropagationStopped = returnTrue ;
};
event.isPropagationStopped = returnFalse ;
// Empêcher l'événement de se multiplier et d'exécuter d'autres gestionnaires
event.stopImmediatePropagation = function() {
if (old.stopImmediatePropagation) {
old.stopImmediatePropagation() ;
}
event.isImmediatePropagationStopped = returnTrue ;
event.stopPropagation() ;
};
event.isImmediatePropagationStopped = returnFalse ;
// Gestion de la position de la souris
if (event.clientX !== null && event.clientX !== undefined) {
const doc = document.documentElement ;
const body = document.body ;
event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0) ;
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0) ;
}
// Gérer les pressions sur les touches
event.which = event.charCode || event.keyCode ;
// Fixer le bouton pour les clics de souris :
// 0 == gauche ; 1 == milieu ; 2 == droite
if (event.button !== null && event.button !== undefined) {
// Ce qui suit est désactivé parce qu'il ne satisfait pas à la norme videojs
// et ................................
/* eslint-disable */
event.button = (event.button & 1 ? 0 :
(event.button & 4 ? 1 :
(event.button & 2 ? 2 : 0))) ;
/* eslint-enable */
}
}
event.fixed_ = true ;
// Retourne l'instance fixée
retour événement ;
}
/**
* Si les écouteurs d'événements passifs sont pris en charge
*/
let _supportsPassive ;
const supportsPassive = function() {
if (typeof _supportsPassive !== 'boolean') {
_supportsPassive = false ;
essayez {
const opts = Object.defineProperty({}, 'passive', {
get() {
_supportsPassive = true ;
}
}) ;
window.addEventListener('test', null, opts) ;
window.removeEventListener('test', null, opts) ;
} catch (e) {
// ne pas tenir compte
}
}
return _supportsPassive ;
};
/**
* Événements tactiles que Chrome considère comme passifs
*/
const passiveEvents = [
'touchstart',
'touchmove' (toucher, déplacer)
] ;
/**
* Ajouter un récepteur d'événements à l'élément
* Il stocke la fonction de traitement dans un objet de cache distinct
* et ajoute un gestionnaire générique à l'événement de l'élément,
* ainsi qu'un identifiant unique (guid) pour l'élément.
*
* @param {Element|Objet} elem
* Élément ou objet auquel lier les récepteurs
*
* @param {string|string[]} type
* Type d'événement à lier.
*
* @param {EventTarget~EventListener} fn
* Écouteur d'événements.
*/
export function on(elem, type, fn) {
if (Array.isArray(type)) {
return _handleMultipleEvents(on, elem, type, fn) ;
}
if (!DomData.has(elem)) {
DomData.set(elem, {}) ;
}
const data = DomData.get(elem) ;
// Nous avons besoin d'un endroit pour stocker toutes les données de notre gestionnaire
if (!data.handlers) {
data.handlers = {} ;
}
if (!data.handlers[type]) {
data.handlers[type] = [] ;
}
if (!fn.guid) {
fn.guid = Guid.newGUID() ;
}
data.handlers[type].push(fn) ;
if (!data.dispatcher) {
data.disabled = false ;
data.dispatcher = function(event, hash) {
if (data.disabled) {
retour ;
}
event = fixEvent(event) ;
const handlers = data.handlers[event.type] ;
if (handlers) {
// Copier les gestionnaires afin que si des gestionnaires sont ajoutés/supprimés au cours du processus, cela ne perturbe pas tout.
const handlersCopy = handlers.slice(0) ;
for (let m = 0, n = handlersCopy.length ; m < n ; m++) {
if (event.isImmediatePropagationStopped()) {
pause ;
} else {
essayez {
handlersCopy[m].call(elem, event, hash) ;
} catch (e) {
log.error(e) ;
}
}
}
}
};
}
if (data.handlers[type].length === 1) {
if (elem.addEventListener) {
let options = false ;
si (supportsPassive() &&
passiveEvents.indexOf(type) > -1) {
options = {passive : true} ;
}
elem.addEventListener(type, data.dispatcher, options) ;
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, data.dispatcher) ;
}
}
}
/**
* Supprime les récepteurs d'événements d'un élément
*
* @param {Element|Objet} elem
* Objet dont il faut retirer les auditeurs.
*
* @param {string|string[]} [type]
* Type d'auditeur à supprimer. Ne pas inclure pour supprimer tous les événements de l'élément.
*
* @param {EventTarget~EventListener} [fn]
* Auditeur spécifique à supprimer. Ne pas inclure pour supprimer les auditeurs d'un événement
* type.
*/
export function off(elem, type, fn) {
// Ne pas ajouter un objet de cache via getElData si ce n'est pas nécessaire
if (!DomData.has(elem)) {
retour ;
}
const data = DomData.get(elem) ;
// S'il n'y a pas d'événements, il n'y a rien à délier
if (!data.handlers) {
retour ;
}
if (Array.isArray(type)) {
return _handleMultipleEvents(off, elem, type, fn) ;
}
// Fonction utilitaire
const removeType = function(el, t) {
data.handlers[t] = [] ;
_cleanUpEvents(el, t) ;
};
// Est-ce que nous supprimons tous les événements liés ?
if (type === undefined) {
for (const t in data.handlers) {
if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
removeType(elem, t) ;
}
}
retour ;
}
const handlers = data.handlers[type] ;
// S'il n'y a pas de gestionnaire, il n'y a rien à délier
if (!handlers) {
retour ;
}
// Si aucun écouteur n'a été fourni, supprimer tous les écouteurs pour le type
if (!fn) {
removeType(elem, type) ;
retour ;
}
// Nous ne supprimons qu'un seul gestionnaire
if (fn.guid) {
for (let n = 0 ; n < handlers.length ; n++) {
if (handlers[n].guid === fn.guid) {
handlers.splice(n--, 1) ;
}
}
}
_cleanUpEvents(elem, type) ;
}
/**
* Déclencher un événement pour un élément
*
* @param {Element|Objet} elem
* Élément permettant de déclencher un événement sur
*
* @param {EventTarget~Event|string} event
* Une chaîne de caractères (le type) ou un objet d'événement avec un attribut de type
*
* @param {Objet} [hash]
* hachage de données à transmettre avec l'événement
*
* @return {boolean|undefined}
* Retourne le contraire de `defaultPrevented` si la valeur par défaut est
* empêché. Sinon, renvoie `undefined`
*/
export function trigger(elem, event, hash) {
// Récupère les données de l'élément et une référence au parent (pour les bulles).
// Il n'est pas nécessaire d'ajouter un objet de données au cache pour chaque parent,
// donc vérification de hasElData d'abord.
const elemData = DomData.has(elem) ? DomData.get(elem) : {} ;
const parent = elem.parentNode || elem.ownerDocument ;
// type = event.type || event,
// handler ;
// Si un nom d'événement a été transmis sous forme de chaîne, crée un événement à partir de ce nom
if (typeof event === 'string') {
event = {type : event, target : elem} ;
} else if (!event.target) {
event.target = elem ;
}
// Normalise les propriétés de l'événement.
event = fixEvent(event) ;
// Si l'élément transmis possède un distributeur, il exécute les gestionnaires établis.
if (elemData.dispatcher) {
elemData.dispatcher.call(elem, event, hash) ;
}
// Sauf arrêt explicite ou si l'événement ne fait pas de bulles (par exemple, événements médiatiques)
// appelle récursivement cette fonction pour faire remonter l'événement dans le DOM.
if (parent && !event.isPropagationStopped() && event.bubbles === true) {
trigger.call(null, parent, event, hash) ;
// Si elle se trouve au sommet du DOM, elle déclenche l'action par défaut, à moins qu'elle ne soit désactivée.
} else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
if (!DomData.has(event.target)) {
DomData.set(event.target, {}) ;
}
const targetData = DomData.get(event.target) ;
// Vérifie si la cible a une action par défaut pour cet événement.
if (event.target[event.type]) {
// Désactive temporairement l'envoi d'événements sur la cible car nous avons déjà exécuté le gestionnaire.
targetData.disabled = true ;
// Exécute l'action par défaut.
if (typeof event.target[event.type] === 'function') {
event.target[event.type]() ;
}
// Réactive l'envoi d'événements.
targetData.disabled = false ;
}
}
// Informer le déclencheur si le défaut a été empêché en renvoyant false
return !event.defaultPrevented ;
}
/**
* Déclencher un écouteur une seule fois pour un événement.
*
* @param {Element|Objet} elem
* Élément ou objet à lier.
*
* @param {string|string[]} type
* Nom/type de l'événement
*
* @param {Event~EventListener} fn
* Fonction d'écoute d'événements
*/
export function one(elem, type, fn) {
if (Array.isArray(type)) {
return _handleMultipleEvents(one, elem, type, fn) ;
}
const func = function() {
off(elem, type, func) ;
fn.apply(this, arguments) ;
};
// copie le guid dans la nouvelle fonction afin qu'elle puisse être supprimée en utilisant l'ID de la fonction d'origine
func.guid = fn.guid = fn.guid || Guid.newGUID() ;
on(elem, type, func) ;
}
/**
* Déclencher un écouteur une seule fois puis le désactiver pour tous
* événements configurés
*
* @param {Element|Objet} elem
* Élément ou objet à lier.
*
* @param {string|string[]} type
* Nom/type de l'événement
*
* @param {Event~EventListener} fn
* Fonction d'écoute d'événements
*/
export function any(elem, type, fn) {
const func = function() {
off(elem, type, func) ;
fn.apply(this, arguments) ;
};
// copie le guid dans la nouvelle fonction afin qu'elle puisse être supprimée en utilisant l'ID de la fonction d'origine
func.guid = fn.guid = fn.guid || Guid.newGUID() ;
// plusieurs fois par jour, mais une seule fois par jour pour tout
on(elem, type, func) ;
}