/**
* @file mixins/evented.js
* @module evented
*/
import window from 'global/window' ;
import * as Dom from '../utils/dom' ;
import * as Events from '../utils/events' ;
import * as Fn from '../utils/fn' ;
import * as Obj from '../utils/obj' ;
import EventTarget from '../event-target' ;
import DomData from '../utils/dom-data' ;
import log from '../utils/log' ;
const objName = (obj) => {
if (typeof obj.name === 'function') {
return obj.name() ;
}
if (typeof obj.name === 'string') {
return obj.name ;
}
if (obj.name_) {
return obj.name_ ;
}
if (obj.constructor && obj.constructor.name) {
return obj.constructor.name ;
}
return typeof obj ;
};
/**
* Indique si un objet a été soumis ou non à l'application du mixage événementiel.
*
* @param {Object} object
* Un objet à tester.
*
* @return {boolean}
* Indique si l'objet semble ou non faire l'objet d'un événement.
*/
const isEvented = (objet) =>
instanceof EventTarget ||
!objet.eventBusEl_ &&
['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function') ;
/**
* Ajoute un callback à exécuter après l'application du mixin événementiel.
*
* @param {Object} object
* Un objet à ajouter
* @param {Fonction} callback
* Le rappel à exécuter.
*/
const addEventedCallback = (target, callback) => {
if (isEvented(target)) {
callback() ;
} else {
if (!target.eventedCallbacks) {
target.eventedCallbacks = [] ;
}
target.eventedCallbacks.push(callback) ;
}
};
/**
* Si une valeur est un type d'événement valide - chaîne ou tableau non vide.
*
* @private
* @param {string|Array} type
* La valeur du type à tester.
*
* @return {boolean}
* Indique si le type est un type d'événement valide ou non.
*/
const isValidEventType = (type) =>
// La regex vérifie ici que le `type` contient au moins un non
// caractère d'espacement.
(typeof type === 'string' && (/\S/).test(type)) ||
(Array.isArray(type) && !!type.length) ;
/**
* Valide une valeur pour déterminer s'il s'agit d'une cible d'événement valide. Lance si ce n'est pas le cas.
*
* @private
* @throws {Error}
* Si la cible ne semble pas être une cible d'événement valide.
*
* @param {Objet} target
* L'objet à tester.
*
* @param {Objet} obj
* L'objet événementiel que nous validons
*
* @param {string} fnName
* Le nom de la fonction mixin événementielle qui a appelé cette fonction.
*/
const validateTarget = (target, obj, fnName) => {
if (!target || (!target.nodeName && !isEvented(target))) {
throw new Error(`Invalid target for ${objName(obj)}#${fnName} ; must be a DOM node or evented object.`) ;
}
};
/**
* Valide une valeur pour déterminer s'il s'agit d'une cible d'événement valide. Lance si ce n'est pas le cas.
*
* @private
* @throws {Error}
* Si le type ne semble pas être un type d'événement valide.
*
* @param {string|Array} type
* Le type à tester.
*
* @param {Objet} obj
* L'objet événementiel que nous validons
*
* @param {string} fnName
* Le nom de la fonction mixin événementielle qui a appelé cette fonction.
*/
const validateEventType = (type, obj, fnName) => {
if (!isValidEventType(type)) {
throw new Error(`Type d'événement non valide pour ${objName(obj)}#${fnName} ; doit être une chaîne ou un tableau non vide.`) ;
}
};
/**
* Valide une valeur pour déterminer s'il s'agit d'un auditeur valide. Lance si ce n'est pas le cas.
*
* @private
* @throws {Error}
* Si l'auditeur n'est pas une fonction.
*
* @param {Fonction} listener
* L'auditeur à tester.
*
* @param {Objet} obj
* L'objet événementiel que nous validons
*
* @param {string} fnName
* Le nom de la fonction mixin événementielle qui a appelé cette fonction.
*/
const validateListener = (listener, obj, fnName) => {
if (typeof listener !== 'function') {
throw new Error(`Invalid listener for ${objName(obj)}#${fnName} ; must be a function.`) ;
}
};
/**
* Prend un tableau d'arguments donnés à `on()` ou `one()`, les valide, et
* les normalise en un objet.
*
* @private
* @param {Objet} self
* L'objet événementiel sur lequel `on()` ou `one()` a été appelé. Cette
* sera lié en tant que valeur `this` pour l'écouteur.
*
* @param {Array} args
* Un tableau d'arguments passés à `on()` ou `one()`.
*
* @param {string} fnName
* Le nom de la fonction mixin événementielle qui a appelé cette fonction.
*
* @return {Object}
* Un objet contenant des valeurs utiles pour les appels `on()` ou `one()`.
*/
const normalizeListenArgs = (self, args, fnName) => {
// Si le nombre d'arguments est inférieur à 3, la cible est toujours l'élément
// l'objet événementiel lui-même.
const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_ ;
laisser la cible ;
laisser le type ;
laisser l'auditeur ;
if (isTargetingSelf) {
target = self.eventBusEl_ ;
// Traiter les cas où nous avons 3 arguments, mais où nous écoutons toujours
// l'objet événementiel lui-même.
if (args.length >= 3) {
args.shift() ;
}
[type, listener] = args ;
} else {
[target, type, listener] = args ;
}
validateTarget(target, self, fnName) ;
validateEventType(type, self, fnName) ;
validateListener(listener, self, fnName) ;
listener = Fn.bind(self, listener) ;
return {isTargetingSelf, target, type, listener} ;
};
/**
* Ajoute l'auditeur au(x) type(s) d'événement(s) sur la cible, en normalisant pour
* le type de cible.
*
* @private
* @param {Element|Objet} target
* Un nœud DOM ou un objet événementiel.
*
* @param {string} method
* La méthode de liaison des événements à utiliser ("on" ou "one").
*
* @param {string|Array} type
* Un ou plusieurs types d'événements.
*
* @param {Fonction} listener
* Une fonction d'écoute.
*/
const listen = (target, method, type, listener) => {
validateTarget(target, target, method) ;
if (target.nodeName) {
Events[method](target, type, listener) ;
} else {
target[method](type, listener) ;
}
};
/**
* Contient des méthodes qui fournissent des capacités événementielles à un objet qui lui est transmis
* à {@link module:evented|evented}.
*
* @mixin EventedMixin
*/
const EventedMixin = {
/**
* Ajouter un auditeur à un (ou plusieurs) événement(s) sur cet objet ou sur un autre objet événementiel
* objet.
*
* @param {string|Array|Element|Object} targetOrType
* S'il s'agit d'une chaîne ou d'un tableau, il représente le(s) type(s) d'événement(s)
* qui déclenchera l'écoute.
*
* Un autre objet événementiel peut être transmis ici à la place, ce qui aura pour effet de
* font que l'auditeur écoute les événements sur _cet_ objet.
*
* Dans les deux cas, la valeur `this` de l'auditeur sera liée à
* cet objet.
*
* @param {string|Array|Function} typeOrListener
* Si le premier argument était une chaîne de caractères ou un tableau, il s'agirait de l'élément
* fonction d'écoute. Sinon, il s'agit d'une chaîne ou d'un tableau d'événements
* type(s).
*
* @param {Fonction} [listener]
* Si le premier argument était un autre objet événementiel, ce sera
* la fonction d'écoute.
*/
on(...args) {
const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'on') ;
listen(target, 'on', type, listener) ;
// Si cet objet est à l'écoute d'un autre objet événementiel.
if (!isTargetingSelf) {
// Si cet objet est éliminé, supprimer l'écouteur.
const removeListenerOnDispose = () => this.off(target, type, listener) ;
// Utiliser le même identifiant de fonction que l'écouteur afin de pouvoir le supprimer ultérieurement
// en utilisant l'ID de l'auditeur original.
removeListenerOnDispose.guid = listener.guid ;
// Ajouter un récepteur pour l'événement "dispose" de la cible. Cela permet de garantir
// que si la cible est éliminée AVANT cet objet, nous supprimons l'élément
// l'auditeur de suppression qui vient d'être ajouté. Sinon, nous créons une fuite de mémoire.
const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose) ;
// Utiliser le même identifiant de fonction que l'écouteur afin de pouvoir le supprimer ultérieurement
// en utilisant l'ID de l'auditeur original.
removeRemoverOnTargetDispose.guid = listener.guid ;
listen(this, 'on', 'dispose', removeListenerOnDispose) ;
listen(target, 'on', 'dispose', removeRemoverOnTargetDispose) ;
}
},
/**
* Ajouter un auditeur à un (ou plusieurs) événement(s) sur cet objet ou sur un autre objet événementiel
* objet. L'écouteur sera appelé une fois par événement, puis supprimé.
*
* @param {string|Array|Element|Object} targetOrType
* S'il s'agit d'une chaîne ou d'un tableau, il représente le(s) type(s) d'événement(s)
* qui déclenchera l'écoute.
*
* Un autre objet événementiel peut être transmis ici à la place, ce qui aura pour effet de
* font que l'auditeur écoute les événements sur _cet_ objet.
*
* Dans les deux cas, la valeur `this` de l'auditeur sera liée à
* cet objet.
*
* @param {string|Array|Function} typeOrListener
* Si le premier argument était une chaîne de caractères ou un tableau, il s'agirait de l'élément
* fonction d'écoute. Sinon, il s'agit d'une chaîne ou d'un tableau d'événements
* type(s).
*
* @param {Fonction} [listener]
* Si le premier argument était un autre objet événementiel, ce sera
* la fonction d'écoute.
*/
one(...args) {
const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'one') ;
// Ciblage de cet objet événementiel.
if (isTargetingSelf) {
listen(target, 'one', type, listener) ;
// Ciblage d'un autre objet événementiel.
} else {
// TODO : Cette enveloppe est incorrecte ! Il doit seulement
// supprimer l'enveloppe du type d'événement qui l'a appelé.
// Au lieu de cela, toutes les listes sont supprimées au premier déclenchement !
// voir https://github.com/videojs/video.js/issues/5962
const wrapper = (...largs) => {
this.off(target, type, wrapper) ;
listener.apply(null, largs) ;
};
// Utiliser le même identifiant de fonction que l'écouteur afin de pouvoir le supprimer ultérieurement
// en utilisant l'ID de l'auditeur original.
wrapper.guid = listener.guid ;
listen(target, 'one', type, wrapper) ;
}
},
/**
* Ajouter un auditeur à un (ou plusieurs) événement(s) sur cet objet ou sur un autre objet événementiel
* objet. L'écouteur ne sera appelé qu'une seule fois pour le premier événement déclenché
* puis enlevée.
*
* @param {string|Array|Element|Object} targetOrType
* S'il s'agit d'une chaîne ou d'un tableau, il représente le(s) type(s) d'événement(s)
* qui déclenchera l'écoute.
*
* Un autre objet événementiel peut être transmis ici à la place, ce qui aura pour effet de
* font que l'auditeur écoute les événements sur _cet_ objet.
*
* Dans les deux cas, la valeur `this` de l'auditeur sera liée à
* cet objet.
*
* @param {string|Array|Function} typeOrListener
* Si le premier argument était une chaîne de caractères ou un tableau, il s'agirait de l'élément
* fonction d'écoute. Sinon, il s'agit d'une chaîne ou d'un tableau d'événements
* type(s).
*
* @param {Fonction} [listener]
* Si le premier argument était un autre objet événementiel, ce sera
* la fonction d'écoute.
*/
any(...args) {
const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'any') ;
// Ciblage de cet objet événementiel.
if (isTargetingSelf) {
listen(target, 'any', type, listener) ;
// Ciblage d'un autre objet événementiel.
} else {
const wrapper = (...largs) => {
this.off(target, type, wrapper) ;
listener.apply(null, largs) ;
};
// Utiliser le même identifiant de fonction que l'écouteur afin de pouvoir le supprimer ultérieurement
// en utilisant l'ID de l'auditeur original.
wrapper.guid = listener.guid ;
listen(target, 'any', type, wrapper) ;
}
},
/**
* Supprime le(s) auditeur(s) d'un ou plusieurs événement(s) sur un objet événementiel.
*
* @param {string|Array|Element|Object} [targetOrType]
* S'il s'agit d'une chaîne ou d'un tableau, il représente le(s) type(s) d'événement.
*
* Un autre objet événementiel peut être transmis ici à la place, auquel cas
* Les trois arguments sont _nécessaires_.
*
* @param {string|Array|Function} [typeOrListener]
* Si le premier argument est une chaîne de caractères ou un tableau, il peut s'agir de la fonction
* fonction d'écoute. Sinon, il s'agit d'une chaîne ou d'un tableau d'événements
* type(s).
*
* @param {Fonction} [listener]
* Si le premier argument était un autre objet événementiel, ce sera
* la fonction d'écoute ; sinon, _tous_ les auditeurs liés à la fonction de
* seront supprimés.
*/
off(targetOrType, typeOrListener, listener) {
// Ciblage de cet objet événementiel.
if (!targetOrType || isValidEventType(targetOrType)) {
Events.off(this.eventBusEl_, targetOrType, typeOrListener) ;
// Ciblage d'un autre objet événementiel.
} else {
const target = targetOrType ;
const type = typeOrListener ;
// Échouer rapidement et de manière significative !
validateTarget(target, this, 'off') ;
validateEventType(type, this, 'off') ;
validateListener(listener, this, 'off') ;
// S'assurer qu'il y a au moins un guid, même si la fonction n'a pas été utilisée
listener = Fn.bind(this, listener) ;
// Supprime l'écouteur dispose sur cet objet événementiel, qui a été donné
// le même guide que l'auditeur d'événements dans on().
this.off('dispose', listener) ;
if (target.nodeName) {
Events.off(target, type, listener) ;
Events.off(target, 'dispose', listener) ;
} else if (isEvented(target)) {
target.off(type, listener) ;
target.off('dispose', listener) ;
}
}
},
/**
* Déclenche un événement sur cet objet événementiel, provoquant l'appel de ses auditeurs.
*
* @param {string|Object} event
* Un type d'événement ou un objet avec une propriété de type.
*
* @param {Objet} [hash]
* Un objet supplémentaire à transmettre aux auditeurs.
*
* @return {boolean}
* Si le comportement par défaut a été empêché ou non.
*/
trigger(event, hash) {
validateTarget(this.eventBusEl_, this, 'trigger') ;
const type = event && typeof event !== 'string' ? event.type : event ;
if (!isValidEventType(type)) {
const error = `Type d'événement non valide pour ${objName(this)}#trigger ; ` +
doit être une chaîne non vide ou un objet avec une clé de type qui a une valeur non vide" ;
if (event) {
(this.log || log).error(error) ;
} else {
lancer une nouvelle erreur (error) ;
}
}
return Events.trigger(this.eventBusEl_, event, hash) ;
}
};
/**
* Applique {@link module:evented~EventedMixin|EventedMixin} à un objet cible.
*
* @param {Objet} target
* L'objet auquel ajouter des méthodes d'événement.
*
* @param {Objet} [options={}]
* Options permettant de personnaliser le comportement du mixin.
*
* @param {string} [options.eventBusKey]
* Par défaut, ajoute un élément DOM `eventBusEl_` à l'objet cible,
* qui est utilisé comme bus d'événements. Si l'objet cible possède déjà un
* L'élément DOM qui doit être utilisé, transmet sa clé ici.
*
* @return {Object}
* L'objet cible.
*/
function evented(target, options = {}) {
const {eventBusKey} = options ;
// Définir ou créer l'eventBusEl_.
if (eventBusKey) {
if (!target[eventBusKey].nodeName) {
throw new Error(`La clé d'événement "${eventBusKey}" ne fait pas référence à un élément.`) ;
}
target.eventBusEl_ = target[eventBusKey] ;
} else {
target.eventBusEl_ = Dom.createEl('span', {className : 'vjs-event-bus'}) ;
}
Obj.assign(target, EventedMixin) ;
if (target.eventedCallbacks) {
target.eventedCallbacks.forEach((callback) => {
callback() ;
}) ;
}
// Lorsqu'un objet événementiel est éliminé, il supprime tous ses auditeurs.
target.on('dispose', () => {
target.off() ;
[target, target.el_, target.eventBusEl_].forEach(function(val) {
if (val && DomData.has(val)) {
DomData.delete(val) ;
}
}) ;
window.setTimeout(() => {
target.eventBusEl_ = null ;
}, 0) ;
}) ;
retourner la cible ;
}
export default evented ;
export {isEvented} ;
export {addEventedCallback} ;