/**
 * Composant du lecteur - Classe de base pour tous les objets de l'interface utilisateur
 *
 * @file component.js
 */
import window from 'global/window' ;
import evented de './mixins/evented' ;
import stateful from './mixins/stateful' ;
import * as Dom from './utils/dom.js' ;
import * as Fn from './utils/fn.js' ;
import * as Guid from './utils/guid.js' ;
import {toTitleCase, toLowerCase} from './utils/string-cases.js' ;
import mergeOptions from './utils/merge-options.js' ;
import computedStyle de './utils/computed-style' ;
import Map from './utils/map.js' ;
import Set from './utils/set.js' ;
import keycode from 'keycode' ;

/**
 * Classe de base pour tous les composants de l'interface utilisateur.
 * Les composants sont des objets de l'interface utilisateur qui représentent à la fois un objet javascript et un élément
 * dans le DOM. Ils peuvent être des enfants d'autres composants et peuvent avoir
 * les enfants eux-mêmes.
 *
 * Les composants peuvent également utiliser des méthodes de {@link EventTarget}
 */
classe Composant {

  /**
   * Un rappel qui est appelé lorsqu'un composant est prêt. N'a pas de
   * et toute valeur de rappel seront ignorées.
   *
   * @callback Composant~ReadyCallback
   * @this Component
   */

  /**
   * Crée une instance de cette classe.
   *
   * @param {Player} player
   *        Le `Player` auquel cette classe doit être attachée.
   *
   * @param {Objet} [options]
   *        Le magasin clé/valeur des options des composants.
   *
   * @param {Objet[]} [options.children]
   *        Un tableau d'objets enfants avec lesquels initialiser ce composant. Les objets enfants ont
   *        une propriété de nom qui sera utilisée si plus d'un composant du même type doit être
   *        ajouté.
   *
   * @param {string} [options.className]
   *         Une classe ou une liste de classes séparées par des espaces pour ajouter le composant
   *
   * @param {Component~ReadyCallback} [ready]
   *        Fonction qui est appelée lorsque le `Composant` est prêt.
   */
  constructor(player, options, ready) {

    // Le composant peut être le joueur lui-même et nous ne pouvons pas passer `this` à super
    if (!player && this.play) {
      this.player_ = player = this ; // eslint-disable-line
    } else {
      this.player_ = player ;
    }

    this.isDisposed_ = false ;

    // Maintient la référence au composant parent via la méthode `addChild`
    this.parentComponent_ = null ;

    // Faire une copie de prototype.options_ pour se prémunir contre l'écrasement des valeurs par défaut
    this.options_ = mergeOptions({}, this.options_) ;

    // Mise à jour des options avec les options fournies
    options = this.options_ = mergeOptions(this.options_, options) ;

    // Obtenir l'identifiant à partir des options ou de l'élément d'options s'il en est fourni un
    this.id_ = options.id || (options.el && options.el.id) ;

    // S'il n'y a pas d'identifiant dans les options, en générer un
    if (!this.id_) {
      // Ne pas exiger la fonction d'identification du joueur dans le cas de joueurs fictifs
      const id = player && player.id && player.id() || 'no_player' ;

      this.id_ = `${id}_component_${Guid.newGUID()}` ;
    }

    this.name_ = options.name || null ;

    // Créer un élément s'il n'a pas été fourni dans les options
    if (options.el) {
      this.el_ = options.el ;
    } else if (options.createEl !== false) {
      this.el_ = this.createEl() ;
    }

    if (options.className && this.el_) {
      options.className.split(' ').forEach(c => this.addClass(c)) ;
    }

    // si evented est autre chose que false, nous voulons mixer dans evented
    if (options.evented !== false) {
      // Faire de cet objet un objet événementiel et utiliser `el_`, s'il est disponible, comme bus événementiel
      evented(this, {eventBusKey : this.el_ ? 'el_' : null}) ;

      this.handleLanguagechange = this.handleLanguagechange.bind(this) ;
      this.on(this.player_, 'languagechange', this.handleLanguagechange) ;
    }
    stateful(this, this.constructor.defaultState) ;

    this.children_ = [] ;
    this.childIndex_ = {} ;
    this.childNameIndex_ = {} ;

    this.setTimeoutIds_ = new Set() ;
    this.setIntervalIds_ = new Set() ;
    this.rafIds_ = new Set() ;
    this.namedRafs_ = new Map() ;
    this.clearingTimersOnDispose_ = false ;

    // Ajouter tout composant enfant dans les options
    if (options.initChildren !== false) {
      this.initChildren() ;
    }

    // Il ne faut pas déclencher ready ici, sinon il sera lancé avant que init ne soit réellement exécuté
    // terminé pour tous les enfants qui exécutent ce constructeur
    this.ready(ready) ;

    if (options.reportTouchActivity !== false) {
      this.enableTouchActivity() ;
    }

  }

  /**
   * Se débarrasse du `Composant` et de tous ses composants enfants.
   *
   * @fires Composant#dispose
   *
   * @param {Objet} options
   * @param {Element} options.originalEl élément avec lequel remplacer l'élément du lecteur
   */
  dispose(options = {}) {

    // S'en sortir si le composant a déjà été éliminé.
    if (this.isDisposed_) {
      retour ;
    }

    if (this.readyQueue_) {
      this.readyQueue_.length = 0 ;
    }

    /**
     * Déclenché lorsqu'un `Composant` est disposé.
     *
     * @event Composant#dispose
     * @type {EventTarget~Event}
     *
     * @property {boolean} [bubbles=false]
     *           est fixé à false pour que l'événement dispose ne se produise pas
     *           bulle d'air
     */
    this.trigger({type : 'dispose', bubbles : false}) ;

    this.isDisposed_ = true ;

    // Se débarrasser de tous les enfants.
    if (this.children_) {
      for (let i = this.children_.length - 1 ; i >= 0 ; i--) {
        if (this.children_[i].dispose) {
          this.children_[i].dispose() ;
        }
      }
    }

    // Supprimer les références des enfants
    this.children_ = null ;
    this.childIndex_ = null ;
    this.childNameIndex_ = null ;

    this.parentComponent_ = null ;

    if (this.el_) {
      // Retirer l'élément du DOM
      if (this.el_.parentNode) {
        if (options.restoreEl) {
          this.el_.parentNode.replaceChild(options.restoreEl, this.el_) ;
        } else {
          this.el_.parentNode.removeChild(this.el_) ;
        }
      }

      this.el_ = null ;
    }

    // supprimer la référence au lecteur après avoir éliminé l'élément
    ce.joueur_ = null ;
  }

  /**
   * Déterminer si ce composant a été éliminé ou non.
   *
   * @return {boolean}
   *         Si le composant a été éliminé, la valeur sera `true`. Sinon, `false`.
   */
  isDisposed() {
    return Boolean(this.isDisposed_) ;
  }

  /**
   * Retourne le {@link Player} auquel le `Component` s'est attaché.
   *
   * @return {Player}
   *         Le lecteur auquel ce `Component` est attaché.
   */
  player() {
    return this.player_ ;
  }

  /**
   * Fusion en profondeur d'objets d'options avec de nouvelles options.
   * > Remarque : Lorsque `obj` et `options` contiennent des propriétés dont les valeurs sont des objets.
   *         Les deux propriétés sont fusionnées en utilisant {@link module:mergeOptions}
   *
   * @param {Objet} obj
   *        L'objet qui contient les nouvelles options.
   *
   * @return {Object}
   *         Un nouvel objet de `this.options_` et `obj` fusionnés ensemble.
   */
  options(obj) {
    if (!obj) {
      return this.options_ ;
    }

    this.options_ = mergeOptions(this.options_, obj) ;
    return this.options_ ;
  }

  /**
   * Obtenir l'élément DOM `Component`s
   *
   * @return {Element}
   *         L'élément DOM de ce `Component`.
   */
  el() {
    return this.el_ ;
  }

  /**
   * Créez l'élément DOM `Component`.
   *
   * @param {string} [tagName]
   *        Type de nœud DOM de l'élément, par exemple "div"
   *
   * @param {Objet} [propriétés]
   *        Un objet de propriétés à définir.
   *
   * @param {Objet} [attributs]
   *        Un objet d'attributs à définir.
   *
   * @return {Element}
   *         L'élément qui est créé.
   */
  createEl(tagName, properties, attributes) {
    return Dom.createEl(tagName, properties, attributes) ;
  }

  /**
   * Localiser une chaîne de caractères à partir de la chaîne en anglais.
   *
   * Si des jetons sont fournis, il essaiera d'effectuer un simple remplacement de jeton sur la chaîne fournie.
   * Les mots-clés recherchés ressemblent à `{1}`, l'index étant indexé à 1 dans le tableau des mots-clés.
   *
   * Si une `valeur par défaut` est fournie, elle sera utilisée à la place de `string`,
   * si une valeur n'est pas trouvée dans les fichiers de langue fournis.
   * Cette option est utile si vous souhaitez disposer d'une clé descriptive pour le remplacement des jetons
   * mais avoir une chaîne localisée succincte et ne pas exiger que `en.json` soit inclus.
   *
   * Actuellement, il est utilisé pour la synchronisation de la barre de progression.
   * ``js
   * {
   *   "Barre de progression : currentTime={1} duration={2}" : "{1} de {2}"
   * }
   * ```
   * Il est ensuite utilisé comme suit :
   * ``js
   * this.localize('progress bar timing : currentTime={1} duration{2}',
   *               [this.player_.currentTime(), this.player_.duration()],
   *               '{1} de {2}') ;
   * ```
   *
   * Ce qui donne quelque chose comme : `01:23 of 24:56`.
   *
   *
   * @param {string} string
   *        La chaîne à localiser et la clé à rechercher dans les fichiers de langue.
   * @param {string[]} [tokens]
   *        Si l'élément en cours a des remplacements de jetons, fournir les jetons ici.
   * @param {string} [defaultValue]
   *        La valeur par défaut est `string`. Peut être une valeur par défaut à utiliser pour le remplacement du jeton
   *        si la clé de recherche doit être séparée.
   *
   * @return {string}
   *         La chaîne localisée ou, si aucune localisation n'existe, la chaîne anglaise.
   */
  localize(string, tokens, defaultValue = string) {

    const code = this.player_.language && this.player_.language() ;
    const languages = this.player_.languages && this.player_.languages() ;
    const language = languages && languages[code] ;
    const primaryCode = code && code.split('-')[0] ;
    const primaryLang = languages && languages[primaryCode] ;

    let localizedString = defaultValue ;

    if (language && language[string]) {
      localizedString = language[string] ;
    } else if (primaryLang && primaryLang[string]) {
      localizedString = primaryLang[string] ;
    }

    if (tokens) {
      localizedString = localizedString.replace(/\{(\d+)\}/g, function(match, index) {
        const value = tokens[index - 1] ;
        let ret = valeur ;

        if (typeof value === 'undefined') {
          ret = match ;
        }

        retour ret ;
      }) ;
    }

    return localizedString ;
  }

  /**
   * Gère le changement de langue pour le lecteur dans les composants. Doive être remplacée par des sous-composants.
   *
   * @abstract
   */
  handleLanguagechange() {}

  /**
   * Retourne l'élément DOM `Component`. C'est ici que les enfants sont insérés.
   * Ce sera généralement le même que l'élément renvoyé dans {@link Component#el}.
   *
   * @return {Element}
   *         L'élément de contenu de ce `Component`.
   */
  contentEl() {
    return this.contentEl_ || this.el_ ;
  }

  /**
   * Obtenir l'ID de ce composant
   *
   * @return {string}
   *         L'identifiant de ce `Composant`
   */
  id() {
    return this.id_ ;
  }

  /**
   * Obtenir le nom du composant. Le nom est utilisé pour référencer le `Composant`
   * et est défini lors de l'enregistrement.
   *
   * @return {string}
   *         Le nom de ce `Composant`.
   */
  name() {
    return this.name_ ;
  }

  /**
   * Obtenir un tableau de tous les composants enfants
   *
   * @return {Array}
   *         Les enfants
   */
  children() {
    return this.children_ ;
  }

  /**
   * Retourne l'enfant `Composant` avec l'`id` donné.
   *
   * @param {string} id
   *        L'identifiant du `Composant` enfant à obtenir.
   *
   * @return {Component|undefined}
   *         L'enfant `Component` avec l'`id` donné ou non défini.
   */
  getChildById(id) {
    return this.childIndex_[id] ;
  }

  /**
   * Retourne l'enfant `Composant` avec le `nom` donné.
   *
   * @param {string} name
   *        Le nom du `Composant` enfant à récupérer.
   *
   * @return {Component|undefined}
   *         L'enfant `Composant` avec le `nom` donné ou non défini.
   */
  getChild(name) {
    if (!name) {
      retour ;
    }

    return this.childNameIndex_[name] ;
  }

  /**
   * Retourne le descendant `Composant` qui suit l'élément donné
   * descendant `names`. Par exemple, ['foo', 'bar', 'baz'] serait
   * essayer d'obtenir 'foo' sur le composant actuel, 'bar' sur le composant 'foo'
   * et "baz" sur le composant "bar" et renvoie une valeur non définie
   * si l'un d'entre eux n'existe pas.
   *
   * @param {...string[]|...string} names
   *        Le nom du `Composant` enfant à récupérer.
   *
   * @return {Component|undefined}
   *         Le descendant `Composant` suivant le descendant donné
   *         `names` ou non défini.
   */
  getDescendant(...names) {
    // aplatir l'argument du tableau dans le tableau principal
    names = names.reduce((acc, n) => acc.concat(n), []) ;

    let currentChild = this ;

    for (let i = 0 ; i < names.length ; i++) {
      currentChild = currentChild.getChild(names[i]) ;

      if (!currentChild || !currentChild.getChild) {
        retour ;
      }
    }

    return currentChild ;
  }

  /**
   * Ajoute un `Composant` enfant à l'intérieur du `Composant` actuel.
   *
   *
   * @param {string|Composant} child
   *        Le nom ou l'instance d'un enfant à ajouter.
   *
   * @param {Objet} [options={}]
   *        Le magasin clé/valeur des options qui seront transmises aux enfants de
   *        l'enfant.
   *
   * @param {number} [index=this.children_.length]
   *        L'index dans lequel il faut tenter d'ajouter un enfant.
   *
   * @return {Component}
   *         Le `Composant` qui est ajouté en tant qu'enfant. Lors de l'utilisation d'une chaîne de caractères, l'option
   *         le composant sera créé par ce processus.
   */
  addChild(child, options = {}, index = this.children_.length) {
    let component ;
    let componentName ;

    // Si l'enfant est une chaîne, créer un composant avec des options
    if (typeof child === 'string') {
      componentName = toTitleCase(child) ;

      const componentClassName = options.componentClass || componentName ;

      // Définir le nom par le biais des options
      options.name = componentName ;

      // Créer un nouvel objet & pour cet ensemble de contrôles
      // S'il n'y a pas de .player_, il s'agit d'un joueur
      const ComponentClass = Component.getComponent(componentClassName) ;

      if (!ComponentClass) {
        throw new Error(`Le composant ${componentClassName} n'existe pas`) ;
      }

      // les données stockées directement sur l'objet videojs peuvent être
      // identifié à tort comme un composant à conserver
      // rétrocompatibilité avec la version 4.x. vérifier que le fichier
      // la classe de composant peut être instanciée.
      if (typeof ComponentClass !== 'function') {
        retourner null ;
      }

      component = new ComponentClass(this.player_ || this, options) ;

    // l'enfant est une instance de composant
    } else {
      composant = enfant ;
    }

    if (component.parentComponent_) {
      component.parentComponent_.removeChild(component) ;
    }
    this.children_.splice(index, 0, component) ;
    component.parentComponent_ = this ;

    if (typeof component.id === 'function') {
      this.childIndex_[component.id()] = component ;
    }

    // Si aucun nom n'a été utilisé pour créer le composant, vérifiez si nous pouvons utiliser l'attribut
    // nom de la fonction du composant
    componentName = componentName || (component.name && toTitleCase(component.name())) ;

    if (componentName) {
      this.childNameIndex_[componentName] = component ;
      this.childNameIndex_[toLowerCase(componentName)] = component ;
    }

    // Ajouter l'élément de l'objet de l'interface utilisateur au conteneur div (box)
    // Il n'est pas nécessaire d'avoir un élément
    if (typeof component.el === 'function' && component.el()) {
      // Si l'insertion se fait avant un composant, l'insertion se fait avant l'élément de ce composant
      let refNode = null ;

      if (this.children_[index + 1]) {
        // La plupart des enfants sont des composants, mais la technologie vidéo est un élément HTML
        if (this.children_[index + 1].el_) {
          refNode = this.children_[index + 1].el_ ;
        } else if (Dom.isEl(this.children_[index + 1])) {
          refNode = this.children_[index + 1] ;
        }
      }

      this.contentEl().insertBefore(component.el(), refNode) ;
    }

    // Retourner pour qu'il puisse être stocké sur l'objet parent si désiré.
    retourner le composant ;
  }

  /**
   * Supprime un `Composant` enfant de la liste des enfants de ce `Composant`. Supprime également
   * l'élément `Composant` enfant de cet élément `Composant`.
   *
   * @param {Composant} component
   *        Le `Composant` enfant à supprimer.
   */
  removeChild(component) {
    if (typeof component === 'string') {
      component = this.getChild(component) ;
    }

    if (!component || !this.children_) {
      retour ;
    }

    let childFound = false ;

    for (let i = this.children_.length - 1 ; i >= 0 ; i--) {
      if (this.children_[i] === component) {
        childFound = true ;
        this.children_.splice(i, 1) ;
        pause ;
      }
    }

    if (!childFound) {
      retour ;
    }

    component.parentComponent_ = null ;

    this.childIndex_[component.id()] = null ;
    this.childNameIndex_[toTitleCase(component.name())] = null ;
    this.childNameIndex_[toLowerCase(component.name())] = null ;

    const compEl = component.el() ;

    if (compEl && compEl.parentNode === this.contentEl()) {
      this.contentEl().removeChild(component.el()) ;
    }
  }

  /**
   * Ajouter et initialiser un composant enfant par défaut en fonction des options.
   */
  initChildren() {
    const children = this.options_.children ;

    if (children) {
      // `this` est `parent`
      const parentOptions = this.options_ ;

      const handleAdd = (child) => {
        const name = child.name ;
        let opts = child.opts ;

        // Permettre aux options des enfants d'être définies au niveau des options du parent
        // par exemple, videojs(id, { controlBar : false }) ;
        // au lieu de videojs(id, { children : { controlBar : false }) ;
        if (parentOptions[name] !== undefined) {
          opts = parentOptions[name] ;
        }

        // Permet de désactiver les composants par défaut
        // par exemple, options['children']['posterImage'] = false
        if (opts === false) {
          retour ;
        }

        // Permettre aux options d'être transmises sous la forme d'un simple booléen s'il n'y a pas de configuration
        // est nécessaire.
        if (opts === true) {
          opts = {} ;
        }

        // Nous voulons également transmettre les options du joueur d'origine
        // à chaque composant afin qu'ils n'aient pas besoin d'en faire autant
        // se replonger dans le lecteur pour des options ultérieures.
        opts.playerOptions = this.options_.playerOptions ;

        // Créer et ajouter le composant enfant.
        // Ajouter une référence directe à l'enfant par son nom sur l'instance parent.
        // Si deux composants identiques sont utilisés, des noms différents doivent être fournis
        // pour chaque
        const newChild = this.addChild(name, opts) ;

        if (newChild) {
          this[name] = newChild ;
        }
      };

      // Permet de passer un tableau de détails sur les enfants dans les options
      let workingChildren ;
      const Tech = Component.getComponent('Tech') ;

      if (Array.isArray(children)) {
        workingChildren = children ;
      } else {
        workingChildren = Object.keys(children) ;
      }

      enfants travailleurs
      // les enfants qui sont dans this.options_ mais aussi dans workingChildren seraient
      // nous donnent des enfants supplémentaires dont nous ne voulons pas. Nous voulons donc les filtrer.
        .concat(Object.keys(this.options_)
          .filter(function(child) {
            return !workingChildren.some(function(wchild) {
              if (typeof wchild === 'string') {
                return child === wchild ;
              }
              return child === wchild.name ;
            }) ;
          }))
        .map((child) => {
          laisser le nom ;
          let opts ;

          if (typeof child === 'string') {
            nom = enfant ;
            opts = children[name] || this.options_[name] || {} ;
          } else {
            nom = nom de l'enfant ;
            opts = child ;
          }

          return {name, opts} ;
        })
        .filter((child) => {
        // nous devons nous assurer que child.name n'est pas dans le techOrder puisque
        // les techniciens sont enregistrés comme composants mais ne sont pas compatibles
        // Voir https://github.com/videojs/video.js/issues/2772
          const c = Component.getComponent(child.opts.componentClass ||
                                       toTitleCase(child.name)) ;

          return c && !Tech.isTech(c) ;
        })
        .forEach(handleAdd) ;
    }
  }

  /**
   * Construit le nom de la classe DOM par défaut. Doive être remplacée par des sous-composants.
   *
   * @return {string}
   *         Le nom de la classe DOM pour cet objet.
   *
   * @abstract
   */
  buildCSSClass() {
    // Les classes enfantines peuvent inclure une fonction qui le fait :
    // return 'NOM DE CLASSE' + this._super() ;
    retourner '' ;
  }

  /**
   * Lier un écouteur à l'état de préparation du composant.
   * Différent des auditeurs d'événements en ce sens que si l'événement "ready" s'est déjà produit
   * il déclenchera la fonction immédiatement.
   *
   * @return {Component}
   *         Se renvoie lui-même ; la méthode peut être enchaînée.
   */
  ready(fn, sync = false) {
    if (!fn) {
      retour ;
    }

    if (!this.isReady_) {
      this.readyQueue_ = this.readyQueue_ || [] ;
      this.readyQueue_.push(fn) ;
      retour ;
    }

    if (sync) {
      fn.call(this) ;
    } else {
      // Appeler la fonction de manière asynchrone par défaut pour des raisons de cohérence
      this.setTimeout(fn, 1) ;
    }
  }

  /**
   * Déclenche tous les récepteurs prêts pour ce `Composant`.
   *
   * @fires Composant#ready
   */
  triggerReady() {
    this.isReady_ = true ;

    // S'assurer que l'état prêt est déclenché de manière asynchrone
    this.setTimeout(function() {
      const readyQueue = this.readyQueue_ ;

      // Réinitialisation de la file d'attente
      this.readyQueue_ = [] ;

      if (readyQueue && readyQueue.length > 0) {
        readyQueue.forEach(function(fn) {
          fn.call(this) ;
        }, this) ;
      }

      // Permet également d'utiliser des récepteurs d'événements
      /**
       * Déclenché lorsqu'un `Composant` est prêt.
       *
       * @event Composant#ready
       * @type {EventTarget~Event}
       */
      this.trigger('ready') ;
    }, 1) ;
  }

  /**
   * Trouver un seul élément DOM correspondant à un `sélecteur`. Cela peut se faire à l'intérieur de la rubrique "Composants"
   * `contentEl()` ou un autre contexte personnalisé.
   *
   * @param {string} selector
   *        Un sélecteur CSS valide, qui sera passé à `querySelector`.
   *
   * @param {Element|string} [context=this.contentEl()]
   *        Un élément du DOM à l'intérieur duquel la requête doit être effectuée. Peut également être une chaîne de sélection dans
   *        auquel cas le premier élément correspondant sera utilisé comme contexte. Si
   *        manquant `this.contentEl()` est utilisé. Si `this.contentEl()` renvoie
   *        rien, il se rabat sur `document`.
   *
   * @return {Element|null}
   *         l'élément dom qui a été trouvé, ou null
   *
   * @voir [Information sur les sélecteurs CSS] (https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $(selector, context) {
    return Dom.$(selector, context || this.contentEl()) ;
  }

  /**
   * Trouve tous les éléments du DOM correspondant à un `sélecteur`. Cela peut se faire à l'intérieur de la rubrique "Composants"
   * `contentEl()` ou un autre contexte personnalisé.
   *
   * @param {string} selector
   *        Un sélecteur CSS valide, qui sera passé à `querySelectorAll`.
   *
   * @param {Element|string} [context=this.contentEl()]
   *        Un élément du DOM à l'intérieur duquel la requête doit être effectuée. Peut également être une chaîne de sélection dans
   *        auquel cas le premier élément correspondant sera utilisé comme contexte. Si
   *        manquant `this.contentEl()` est utilisé. Si `this.contentEl()` renvoie
   *        rien, il se rabat sur `document`.
   *
   * @return {NodeList}
   *         une liste des éléments dom trouvés
   *
   * @voir [Information sur les sélecteurs CSS] (https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $$(selector, context) {
    return Dom.$$(selector, context || this.contentEl()) ;
  }

  /**
   * Vérifie si l'élément d'un composant possède un nom de classe CSS.
   *
   * @param {string} classToCheck
   *        Nom de la classe CSS à vérifier.
   *
   * @return {boolean}
   *         - True si le `Composant` possède la classe.
   *         - Faux si le `Composant` n'a pas la classe`
   */
  hasClass(classToCheck) {
    return Dom.hasClass(this.el_, classToCheck) ;
  }

  /**
   * Ajouter un nom de classe CSS à l'élément `Component`.
   *
   * @param {string} classToAdd
   *        Nom de la classe CSS à ajouter
   */
  addClass(classToAdd) {
    Dom.addClass(this.el_, classToAdd) ;
  }

  /**
   * Supprime le nom d'une classe CSS de l'élément `Component`.
   *
   * @param {string} classToRemove
   *        Nom de la classe CSS à supprimer
   */
  removeClass(classToRemove) {
    Dom.removeClass(this.el_, classToRemove) ;
  }

  /**
   * Ajouter ou supprimer un nom de classe CSS de l'élément du composant.
   * - `classToToggle` est ajouté lorsque {@link Component#hasClass} renvoie false.
   * - `classToToggle` est supprimé lorsque {@link Component#hasClass} renvoie true.
   *
   * @param {string} classToToggle
   *         La classe à ajouter ou à supprimer en fonction de (@link Component#hasClass}
   *
   * @param {boolean|Dom~predicate} [predicate]
   *         Une fonction {@link Dom~predicate} ou un booléen
   */
  toggleClass(classToToggle, predicate) {
    Dom.toggleClass(this.el_, classToToggle, predicate) ;
  }

  /**
   * Afficher l'élément `Component` s'il est caché en supprimant l'élément
   * le nom de la classe "vjs-hidden" en est extrait.
   */
  show() {
    this.removeClass('vjs-hidden') ;
  }

  /**
   * Cachez l'élément `Component` s'il est actuellement affiché en ajoutant la balise
   * 'vjs-hidden` pour le nom de la classe.
   */
  hide() {
    this.addClass('vjs-hidden') ;
  }

  /**
   * Verrouiller un élément `Component` dans son état visible en ajoutant l'option 'vjs-lock-showing'
   * à son nom de classe. Utilisé pendant les fondus entrant et sortant.
   *
   * @private
   */
  lockShowing() {
    this.addClass('vjs-lock-showing') ;
  }

  /**
   * Déverrouiller un élément `Component` de son état visible en supprimant l'option 'vjs-lock-showing'
   * de la classe. Utilisé pendant les fondus entrant et sortant.
   *
   * @private
   */
  unlockShowing() {
    this.removeClass('vjs-lock-showing') ;
  }

  /**
   * Obtenir la valeur d'un attribut de l'élément `Component`.
   *
   * @param {string} attribut
   *        Nom de l'attribut à partir duquel la valeur doit être obtenue.
   *
   * @return {string|null}
   *         - La valeur de l'attribut demandé.
   *         - Peut être une chaîne vide sur certains navigateurs si l'attribut n'existe pas
   *           ou n'a pas de valeur
   *         - La plupart des navigateurs renvoient la valeur null si l'attribut n'existe pas ou s'il n'a pas été créé
   *           aucune valeur.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
   */
  getAttribute(attribute) {
    return Dom.getAttribute(this.el_, attribute) ;
  }

  /**
   * Fixer la valeur d'un attribut sur l'élément `Component`
   *
   * @param {string} attribut
   *        Nom de l'attribut à définir.
   *
   * @param {string} value
   *        Valeur à attribuer à l'attribut.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
   */
  setAttribute(attribute, value) {
    Dom.setAttribute(this.el_, attribute, value) ;
  }

  /**
   * Supprime un attribut de l'élément `Component`.
   *
   * @param {string} attribut
   *        Nom de l'attribut à supprimer.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
   */
  removeAttribute(attribute) {
    Dom.removeAttribute(this.el_, attribute) ;
  }

  /**
   * Permet d'obtenir ou de définir la largeur du composant en fonction des styles CSS.
   * Voir {@link Component#dimension} pour plus d'informations.
   *
   * @param {number|string} [num]
   *        La largeur que vous souhaitez définir après avoir fixé '%', 'px' ou rien du tout.
   *
   * @param {boolean} [skipListeners]
   *        Sauter le déclenchement de l'événement de redimensionnement des composants
   *
   * @return {number|string}
   *         La largeur lors de l'obtention, zéro s'il n'y a pas de largeur. Il peut s'agir d'une chaîne
   *           postposé avec "%" ou "px".
   */
  width(num, skipListeners) {
    return this.dimension('width', num, skipListeners) ;
  }

  /**
   * Permet d'obtenir ou de définir la hauteur du composant en fonction des styles CSS.
   * Voir {@link Component#dimension} pour plus d'informations.
   *
   * @param {number|string} [num]
   *        La hauteur que vous souhaitez définir après avoir fixé '%', 'px' ou rien du tout.
   *
   * @param {boolean} [skipListeners]
   *        Sauter le déclenchement de l'événement de redimensionnement des composants
   *
   * @return {number|string}
   *         La largeur lors de l'obtention, zéro s'il n'y a pas de largeur. Il peut s'agir d'une chaîne
   *         postposé avec "%" ou "px".
   */
  height(num, skipListeners) {
    return this.dimension('height', num, skipListeners) ;
  }

  /**
   * Définit simultanément la largeur et la hauteur de l'élément `Component`.
   *
   * @param {nombre|chaîne} largeur
   *         Largeur de l'élément `Component`.
   *
   * @param {nombre|chaîne} hauteur
   *         Hauteur de l'élément `Component`.
   */
  dimensions(width, height) {
    // Sauter les auditeurs de redimensionnement des composants en fonction de la largeur pour des raisons d'optimisation
    this.width(width, true) ;
    this.height(height) ;
  }

  /**
   * Permet d'obtenir ou de définir la largeur ou la hauteur de l'élément `Component`. Voici le code partagé
   * pour les {@link Component#width} et {@link Component#height}.
   *
   * Ce qu'il faut savoir :
   * - Si la largeur ou la hauteur est un nombre, il renvoie le nombre postfixé par "px".
   * - Si la largeur/hauteur est un pourcentage, cette option renvoie le pourcentage postfixé par "%"
   * - Les éléments cachés ont une largeur de 0 avec `window.getComputedStyle`. Cette fonction
   *   prend par défaut le `style.width` du `Composant` et retombe sur `window.getComputedStyle`.
   *   Voir [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
   *   pour plus d'informations
   * - Si vous voulez le style calculé du composant, utilisez {@link Component#currentWidth}
   *   et {@link {Component#currentHeight}
   *
   * @fires Component#componentresize
   *
   * @param {string} widthOrHeight
   8 "largeur" ou "hauteur
   *
   * @param {number|string} [num]
   8 Nouvelle dimension
   *
   * @param {boolean} [skipListeners]
   *         Sauter le déclenchement de l'événement componentresize
   *
   * @return {number}
   *         La dimension lorsqu'elle est obtenue ou 0 si elle n'est pas définie
   */
  dimension(widthOrHeight, num, skipListeners) {
    if (num !== undefined) {
      // Fixe à zéro si null ou littéralement NaN (NaN !== NaN)
      if (num === null || num !== num) {
        num = 0 ;
      }

      // Vérifier si l'on utilise la largeur/hauteur css (% ou px) et l'ajuster
      if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
        this.el_.style[widthOrHeight] = num ;
      else if (num === 'auto') {
        this.el_.style[widthOrHeight] = '' ;
      } else {
        this.el_.style[widthOrHeight] = num + 'px' ;
      }

      // skipListeners nous permet d'éviter de déclencher l'événement de redimensionnement lorsque l'on définit à la fois la largeur et la hauteur
      if (!skipListeners) {
        /**
         * Déclenché lorsqu'un composant est redimensionné.
         *
         * @event Component#componentresize (Redimensionnement du composant)
         * @type {EventTarget~Event}
         */
        this.trigger('componentresize') ;
      }

      retour ;
    }

    // La valeur n'est pas définie, elle est donc récupérée
    // S'assurer que l'élément existe
    if (!this.el_) {
      retour 0 ;
    }

    // Obtenir la valeur de la dimension à partir du style
    const val = this.el_.style[widthOrHeight] ;
    const pxIndex = val.indexOf('px') ;

    if (pxIndex !== -1) {
      // Retourne la valeur du pixel sans 'px'
      return parseInt(val.slice(0, pxIndex), 10) ;
    }

    // Pas de px donc utilisation de % ou aucun style n'a été défini, donc retour à offsetWidth/height
    // Si le composant a un affichage:none, l'offset renverra 0
    // TODO : gérer le style display:none et no dimension en utilisant px
    return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10) ;
  }

  /**
   * Obtenir la largeur ou la hauteur calculée de l'élément du composant.
   *
   * Utilise `window.getComputedStyle`.
   *
   * @param {string} widthOrHeight
   *        Une chaîne contenant "width" ou "height". Celui que vous voulez obtenir.
   *
   * @return {number}
   *         La dimension demandée ou 0 si rien n'a été défini
   *         pour cette dimension.
   */
  currentDimension(widthOrHeight) {
    let computedWidthOrHeight = 0 ;

    if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
      lancer une nouvelle erreur ('currentDimension n'accepte que la valeur de la largeur ou de la hauteur') ;
    }

    computedWidthOrHeight = computedStyle(this.el_, widthOrHeight) ;

    // supprimer "px" de la variable et l'analyser comme un entier
    computedWidthOrHeight = parseFloat(computedWidthOrHeight) ;

    // si la valeur calculée est toujours 0, il est possible que le navigateur mente
    // et nous voulons vérifier les valeurs de décalage.
    // Ce code s'exécute également lorsque getComputedStyle n'existe pas.
    if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
      const rule = `offset${toTitleCase(widthOrHeight)}` ;

      computedWidthOrHeight = this.el_[rule] ;
    }

    return computedWidthOrHeight ;
  }

  /**
   * Un objet qui contient les valeurs de largeur et de hauteur du `Composant`
   * style calculé. Utilise `window.getComputedStyle`.
   *
   * @typedef {Objet} Composant~DimensionObjet
   *
   * @property {number} width
   *           La largeur du style calculé du `Composant`.
   *
   * @property {number} height
   *           La hauteur du style calculé du `Composant`.
   */

  /**
   * Obtention d'un objet contenant les valeurs calculées de la largeur et de la hauteur de l'image
   * l'élément du composant.
   *
   * Utilise `window.getComputedStyle`.
   *
   * @return {Component~DimensionObject}
   *         Les dimensions calculées de l'élément du composant.
   */
  currentDimensions() {
    retour {
      width : this.currentDimension('width'),
      height : this.currentDimension('height')
    };
  }

  /**
   * Obtenir la largeur calculée de l'élément du composant.
   *
   * Utilise `window.getComputedStyle`.
   *
   * @return {number}
   *         La largeur calculée de l'élément du composant.
   */
  currentWidth() {
    return this.currentDimension('width') ;
  }

  /**
   * Obtenir la hauteur calculée de l'élément du composant.
   *
   * Utilise `window.getComputedStyle`.
   *
   * @return {number}
   *         La hauteur calculée de l'élément du composant.
   */
  currentHeight() {
    return this.currentDimension('height') ;
  }

  /**
   * Mettre l'accent sur ce composant
   */
  focus() {
    this.el_.focus() ;
  }

  /**
   * Retirer le focus de ce composant
   */
  blur() {
    this.el_.blur() ;
  }

  /**
   * Lorsque ce composant reçoit un événement `keydown` qu'il ne traite pas,
   *  il transmet l'événement au lecteur pour qu'il le traite.
   *
   * @param {EventTarget~Event} event
   *        L'événement `keydown` qui a provoqué l'appel de cette fonction.
   */
  handleKeyDown(event) {
    if (this.player_) {

      // Nous n'arrêtons la propagation qu'ici car nous voulons que les événements non gérés tombent
      // vers le navigateur. Exclure l'onglet pour la capture de l'attention.
      if (!keycode.isEventKey(event, 'Tab')) {
        event.stopPropagation() ;
      }
      this.player_.handleKeyDown(event) ;
    }
  }

  /**
   * De nombreux composants avaient une méthode `handleKeyPress`, qui était mal utilisée
   * nommé parce qu'il a écouté un événement `keydown`. Le nom de cette méthode est désormais
   * est délégué à `handleKeyDown`. Cela signifie que toute personne appelant `handleKeyPress`
   * ne verront pas leurs appels de méthode cesser de fonctionner.
   *
   * @param {EventTarget~Event} event
   *        L'événement qui a provoqué l'appel de cette fonction.
   */
  handleKeyPress(event) {
    this.handleKeyDown(event) ;
  }

  /**
   * Émettre un événement "tap" lorsque la prise en charge d'un événement tactile est détectée. Il est utilisé pour
   * permettent de basculer les commandes en tapant sur la vidéo. Ils sont activés
   * parce que chaque sous-composant aurait des frais généraux supplémentaires dans le cas contraire.
   *
   * @private
   * composant @fires#tap
   * @listens Composant#touchstart
   * @listens Composant#touchmove
   * @listens Composant#touchleave
   * @listens Component#touchcancel
   * @listens Component#touchend

   */
  emitTapEvents() {
    // Suivre l'heure de début afin de pouvoir déterminer la durée du contact
    let touchStart = 0 ;
    let firstTouch = null ;

    // Mouvement maximal autorisé lors d'un événement tactile pour être considéré comme un tapotement
    // D'autres librairies populaires utilisent entre 2 (hammer.js) et 15,
    // donc 10 semble être un chiffre rond et agréable.
    const tapMovementThreshold = 10 ;

    // La longueur maximale d'une touche peut être considérée comme une prise
    const touchTimeThreshold = 200 ;

    laisser couldBeTap ;

    this.on('touchstart', function(event) {
      // S'il y a plus d'un doigt, ne considérez pas cela comme un clic
      if (event.touches.length === 1) {
        // Copie de la pageX/pageY de l'objet
        firstTouch = {
          pageX : event.touches[0].pageX,
          pageY : event.touches[0].pageY
        };
        // Enregistrer l'heure de début afin de pouvoir détecter un tapotement par rapport à un "touch and hold" (toucher et maintenir)
        touchStart = window.performance.now() ;
        // Réinitialiser le suivi de couldBeTap
        couldBeTap = true ;
      }
    }) ;

    this.on('touchmove', function(event) {
      // S'il y a plus d'un doigt, ne considérez pas cela comme un clic
      if (event.touches.length > 1) {
        couldBeTap = false ;
      } else if (firstTouch) {
        // Certains appareils refusent les mouvements tactiles, sauf s'il s'agit d'une légère pression.
        // Ainsi, si nous ne nous déplaçons que sur une petite distance, il peut toujours s'agir d'un robinet
        const xdiff = event.touches[0].pageX - firstTouch.pageX ;
        const ydiff = event.touches[0].pageY - firstTouch.pageY ;
        const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff) ;

        if (touchDistance > tapMovementThreshold) {
          couldBeTap = false ;
        }
      }
    }) ;

    const noTap = function() {
      couldBeTap = false ;
    };

    // TODO : Écouter la cible originale. http://youtu.be/DujfpXOKUp8?t=13m8s
    this.on('touchleave', noTap) ;
    this.on('touchcancel', noTap) ;

    // Lorsque le contact se termine, mesurez la durée du contact et déclenchez l'action appropriée
    // événement
    this.on('touchend', function(event) {
      firstTouch = null ;
      // Ne procéder que si l'événement touchmove/leave/cancel ne s'est pas produit
      if (couldBeTap === true) {
        // Mesure la durée du contact
        const touchTime = window.performance.now() - touchStart ;

        // S'assurer que le toucher était inférieur au seuil pour être considéré comme un tapotement
        if (touchTime < touchTimeThreshold) {
          // Ne laissez pas le navigateur transformer ceci en un clic
          event.preventDefault() ;
          /**
           * Déclenché lorsqu'un `Composant` est touché.
           *
           * @event Composant#tap
           * @type {EventTarget~Event}
           */
          this.trigger('tap') ;
          // Il peut être utile de copier l'objet de l'événement touchend et de modifier l'objet de l'événement touchend
          // type de tapotement, si les autres propriétés de l'événement ne sont pas exactes après
          // Events.fixEvent s'exécute (par exemple event.target)
        }
      }
    }) ;
  }

  /**
   * Cette fonction signale l'activité de l'utilisateur lorsque des événements tactiles se produisent. Cela peut se traduire par
   * désactivé par tout sous-composant qui souhaite que les événements tactiles agissent d'une autre manière.
   *
   * Signaler l'activité tactile de l'utilisateur lorsque des événements tactiles se produisent. L'activité de l'utilisateur est utilisée pour
   * déterminent quand les contrôles doivent être affichés/masqués. C'est simple quand il s'agit de la souris
   * car tout événement de souris devrait afficher les contrôles. Nous capturons donc la souris
   * qui remontent jusqu'au joueur et signalent l'activité lorsque cela se produit.
   * Avec les événements tactiles, ce n'est pas aussi simple que de faire basculer le lecteur de `touchstart` et de `touchend`
   * des contrôles. Les événements tactiles ne peuvent donc pas non plus nous aider au niveau du joueur.
   *
   * L'activité de l'utilisateur est vérifiée de manière asynchrone. Ce qui pourrait donc se produire, c'est un événement de robinetterie
   * sur la vidéo désactive les commandes. Ensuite, l'événement `touchend` fait boule de neige jusqu'à
   * le joueur. Qui, s'il signalait l'activité de l'utilisateur, ferait basculer les contrôles à droite
   * retourner à la page d'accueil. Nous ne voulons pas non plus empêcher complètement les événements tactiles de bouillonner.
   * En outre, un événement `touchmove` et toute autre chose qu'un tap, ne devrait pas tourner
   * les commandes sont remises en place.
   *
   * @listens Composant#touchstart
   * @listens Composant#touchmove
   * @listens Component#touchend
   * @listens Component#touchcancel
   */
  enableTouchActivity() {
    // Ne pas poursuivre si le lecteur racine ne prend pas en charge le signalement de l'activité de l'utilisateur
    if (!this.player() || !this.player().reportUserActivity) {
      retour ;
    }

    // listener pour signaler que l'utilisateur est actif
    const report = Fn.bind(this.player(), this.player().reportUserActivity) ;

    let touchHolding ;

    this.on('touchstart', function() {
      rapport() ;
      // Tant qu'ils touchent l'appareil ou qu'ils ont la souris enfoncée,
      // nous les considérons comme actifs même s'ils ne bougent pas le doigt ou la souris.
      // Nous voulons donc continuer à mettre à jour qu'ils sont actifs
      this.clearInterval(touchHolding) ;
      // rapport au même intervalle que activityCheck
      touchHolding = this.setInterval(report, 250) ;
    }) ;

    const touchEnd = function(event) {
      rapport() ;
      // arrêter l'intervalle qui maintient l'activité si la touche est maintenue
      this.clearInterval(touchHolding) ;
    };

    this.on('touchmove', report) ;
    this.on('touchend', touchEnd) ;
    this.on('touchcancel', touchEnd) ;
  }

  /**
   * Un callback qui n'a pas de paramètres et qui est lié au contexte du `Composant`.
   *
   * @callback Component~GenericCallback
   * @this Component
   */

  /**
   * Crée une fonction qui s'exécute après un délai de `x` millisecondes. Cette fonction est une
   * enveloppe autour de `window.setTimeout`. Il y a plusieurs raisons de l'utiliser
   * à la place :
   * 1. Il est effacé par {@link Component#clearTimeout} lorsque
   *    {@link Component#dispose} est appelé.
   * 2. La fonction callback sera transformée en {@link Component~GenericCallback}
   *
   * > Remarque : Vous ne pouvez pas utiliser `window.clearTimeout` sur l'identifiant renvoyé par cette fonction. Cette
   *         fera en sorte que l'auditeur de la mise au rebut ne soit pas nettoyé ! Veuillez utiliser
   *         {@link Component#clearTimeout} ou {@link Component#dispose} à la place.
   *
   * @param {Composant~GenericCallback} fn
   *        La fonction qui sera exécutée après `timeout`.
   *
   * @param {number} timeout
   *        Délai d'attente en millisecondes avant l'exécution de la fonction spécifiée.
   *
   * @return {number}
   *         Renvoie un identifiant de délai d'attente qui est utilisé pour identifier le délai d'attente. Il peut également
   *         est utilisé dans {@link Component#clearTimeout} pour effacer le délai d'attente que les
   *         a été fixé.
   *
   * @listens Component#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
   */
  setTimeout(fn, timeout) {
    // les déclarer comme variables afin qu'elles soient correctement disponibles dans la fonction timeout
    // eslint-disable-next-line
    var timeoutId, disposeFn ;

    fn = Fn.bind(this, fn) ;

    this.clearTimersOnDispose_() ;

    timeoutId = window.setTimeout(() => {
      if (this.setTimeoutIds_.has(timeoutId)) {
        this.setTimeoutIds_.delete(timeoutId) ;
      }
      fn() ;
    }, timeout) ;

    this.setTimeoutIds_.add(timeoutId) ;

    return timeoutId ;
  }

  /**
   * Efface un timeout créé via `window.setTimeout` ou
   * {@link Component#setTimeout}. Si vous définissez un délai d'attente via {@link Component#setTimeout}
   * utiliser cette fonction au lieu de `window.clearTimout`. Si vous ne vous débarrassez pas de votre
   * ne sera pas nettoyé avant {@link Component#dispose} !
   *
   * @param {number} timeoutId
   *        L'identifiant du délai d'attente à effacer. La valeur de retour de
   *        {@link Component#setTimeout} ou `window.setTimeout`.
   *
   * @return {number}
   *         Renvoie l'identifiant du délai d'attente qui a été supprimé.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
   */
  clearTimeout(timeoutId) {
    if (this.setTimeoutIds_.has(timeoutId)) {
      this.setTimeoutIds_.delete(timeoutId) ;
      window.clearTimeout(timeoutId) ;
    }

    return timeoutId ;
  }

  /**
   * Crée une fonction qui s'exécute toutes les `x` millisecondes. Cette fonction est une enveloppe
   * autour de `window.setInterval`. Il y a cependant quelques raisons d'utiliser celle-ci à la place.
   * 1. Il est effacé par {@link Component#clearInterval} lorsque
   *    {@link Component#dispose} est appelé.
   * 2. La fonction callback sera un {@link Component~GenericCallback}
   *
   * @param {Composant~GenericCallback} fn
   *        La fonction à exécuter toutes les `x` secondes.
   *
   * @param {number} interval
   *        Exécute la fonction spécifiée toutes les `x` millisecondes.
   *
   * @return {number}
   *         Renvoie un identifiant qui peut être utilisé pour identifier l'intervalle. Il peut également être utilisé dans
   *         {@link Component#clearInterval} pour effacer l'intervalle.
   *
   * @listens Component#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
   */
  setInterval(fn, interval) {
    fn = Fn.bind(this, fn) ;

    this.clearTimersOnDispose_() ;

    const intervalId = window.setInterval(fn, interval) ;

    this.setIntervalIds_.add(intervalId) ;

    return intervalId ;
  }

  /**
   * Efface un intervalle créé via `window.setInterval` ou
   * {@link Component#setInterval}. Si vous définissez une valeur entière via {@link Component#setInterval}
   * utiliser cette fonction au lieu de `window.clearInterval`. Si vous ne vous débarrassez pas de votre
   * ne sera pas nettoyé avant {@link Component#dispose} !
   *
   * @param {number} intervalId
   *        L'identifiant de l'intervalle à effacer. La valeur de retour de
   *        {@link Component#setInterval} ou `window.setInterval`.
   *
   * @return {number}
   *         Renvoie l'identifiant de l'intervalle qui a été effacé.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
   */
  clearInterval(intervalId) {
    if (this.setIntervalIds_.has(intervalId)) {
      this.setIntervalIds_.delete(intervalId) ;
      window.clearInterval(intervalId) ;
    }

    return intervalId ;
  }

  /**
   * Met en file d'attente un rappel à transmettre à requestAnimationFrame (rAF), mais
   * avec quelques bonus supplémentaires :
   *
   * - Les navigateurs qui ne prennent pas en charge rAF sont pris en charge par le système
   *   {@link Component#setTimeout}.
   *
   * - Le rappel est transformé en un {@link Component~GenericCallback} (c'est-à-dire
   *   lié au composant).
   *
   * - L'annulation automatique du rappel rAF est gérée si le composant
   *   est éliminé avant d'être appelé.
   *
   * @param {Composant~GenericCallback} fn
   *         Une fonction qui sera liée à ce composant et exécutée simplement
   *         avant le prochain repeint du navigateur.
   *
   * @return {number}
   *         Renvoie un ID rAF qui est utilisé pour identifier le délai d'attente. Il peut
   *         peut également être utilisé dans {@link Component#cancelAnimationFrame} pour annuler une animation
   *         le rappel de l'image d'animation.
   *
   * @listens Component#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
   */
  requestAnimationFrame(fn) {
    // Revenir à l'utilisation d'une minuterie.
    if (!this.supportsRaf_) {
      return this.setTimeout(fn, 1000 / 60) ;
    }

    this.clearTimersOnDispose_() ;

    // les déclarer en tant que variables afin qu'elles soient correctement disponibles dans la fonction rAF
    // eslint-disable-next-line
    var id ;
    fn = Fn.bind(this, fn) ;

    id = window.requestAnimationFrame(() => {
      if (this.rafIds_.has(id)) {
        this.rafIds_.delete(id) ;
      }
      fn() ;
    }) ;
    this.rafIds_.add(id) ;

    retourner l'id ;
  }

  /**
   * Demande une image d'animation, mais seulement une animation nommée
   * sera mise en file d'attente. Il n'y en aura jamais d'autre jusqu'à ce que
   * le précédent se termine.
   *
   * @param {string} name
   *        Le nom à donner à ce cadre d'animation
   *
   * @param {Composant~GenericCallback} fn
   *         Une fonction qui sera liée à ce composant et exécutée simplement
   *         avant le prochain repeint du navigateur.
   */
  requestNamedAnimationFrame(name, fn) {
    if (this.namedRafs_.has(name)) {
      retour ;
    }
    this.clearTimersOnDispose_() ;

    fn = Fn.bind(this, fn) ;

    const id = this.requestAnimationFrame(() => {
      fn() ;
      if (this.namedRafs_.has(name)) {
        this.namedRafs_.delete(name) ;
      }
    }) ;

    this.namedRafs_.set(name, id) ;

    nom de retour ;
  }

  /**
   * Annule une image d'animation nommée en cours, si elle existe.
   *
   * @param {string} name
   *        Le nom de la fenêtre d'animation à annuler.
   */
  cancelNamedAnimationFrame(name) {
    if (!this.namedRafs_.has(name)) {
      retour ;
    }

    this.cancelAnimationFrame(this.namedRafs_.get(name)) ;
    this.namedRafs_.delete(name) ;
  }

  /**
   * Annule un rappel en file d'attente passé à {@link Component#requestAnimationFrame}
   * (rAF).
   *
   * Si vous mettez en file d'attente un rappel rAF via {@link Component#requestAnimationFrame},
   * utiliser cette fonction au lieu de `window.cancelAnimationFrame`. Si ce n'est pas le cas,
   * votre auditeur dispose ne sera pas nettoyé avant {@link Component#dispose} !
   *
   * @param {number} id
   *        Le rAF ID à dégager. La valeur de retour de {@link Component#requestAnimationFrame}.
   *
   * @return {number}
   *         Renvoie l'identifiant rAF qui a été effacé.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
   */
  cancelAnimationFrame(id) {
    // Revenir à l'utilisation d'une minuterie.
    if (!this.supportsRaf_) {
      return this.clearTimeout(id) ;
    }

    if (this.rafIds_.has(id)) {
      this.rafIds_.delete(id) ;
      window.cancelAnimationFrame(id) ;
    }

    retourner l'id ;

  }

  /**
   * Une fonction pour configurer `requestAnimationFrame`, `setTimeout`,
   * et `setInterval`, qui s'efface lors de l'élimination.
   *
   * > Auparavant, chaque minuterie ajoutait et supprimait des récepteurs de mise au rebut de son propre chef.
   * Pour de meilleures performances, il a été décidé de les regrouper et d'utiliser `Set`s
   * pour suivre les identifiants de temporisation en suspens.
   *
   * @private
   */
  clearTimersOnDispose_() {
    if (this.clearingTimersOnDispose_) {
      retour ;
    }

    this.clearingTimersOnDispose_ = true ;
    this.one('dispose', () => {
      [
        ['namedRafs_', 'cancelNamedAnimationFrame'],
        ['rafIds_', 'cancelAnimationFrame'],
        ['setTimeoutIds_', 'clearTimeout'],
        ['setIntervalIds_', 'clearInterval']
      ].forEach(([idName, cancelName]) => {
        // pour un `Set` la clé sera en fait la valeur à nouveau
        // donc forEach((val, val) =>` mais pour les cartes nous voulons utiliser
        // la clé.
        this[idName].forEach((val, key) => this[cancelName](key)) ;
      }) ;

      this.clearingTimersOnDispose_ = false ;
    }) ;
  }

  /**
   * Enregistrer un `Composant` avec `videojs` en donnant le nom et le composant.
   *
   * > NOTE : {@link Tech}s ne doit pas être enregistré en tant que `Component`. {@link Tech}s
   *         doit être enregistré en utilisant {@link Tech.registerTech} ou
   *         {@link videojs:videojs.registerTech}.
   *
   * > NOTE : Cette fonction est également visible sur videojs sous la forme suivante
   *         {@link videojs:videojs.registerComponent}.
   *
   * @param {string} name
   *        Le nom du `Composant` à enregistrer.
   *
   * @param {Composant} Composant à enregistrer
   *        La classe `Component` à enregistrer.
   *
   * @return {Component}
   *         Le `Composant` qui a été enregistré.
   */
  static registerComponent(name, ComponentToRegister) {
    if (typeof name !== 'string' || !name) {
      throw new Error(`Nom de composant illégal, "${nom}" ; doit être une chaîne non vide.`) ;
    }

    const Tech = Component.getComponent('Tech') ;

    // Nous devons nous assurer que cette vérification n'est effectuée que si Tech a été enregistré.
    const isTech = Tech && Tech.isTech(ComponentToRegister) ;
    const isComp = Component === ComponentToRegister ||
      Component.prototype.isPrototypeOf(ComponentToRegister.prototype) ;

    if (isTech || !isComp) {
      laisser la raison ;

      if (isTech) {
        reason = 'techs must be registered using Tech.registerTech()' ;
      } else {
        reason = "must be a Component subclass" ;
      }

      lancer une nouvelle erreur (`Composant illégal, "${nom}" ; ${motif}.`) ;
    }

    nom = toTitleCase(nom) ;

    if (!Component.components_) {
      Component.components_ = {} ;
    }

    const Player = Component.getComponent('Player') ;

    if (name === 'Player' && Player && Player.players) {
      const players = Player.players ;
      const playerNames = Object.keys(players) ;

      // Si nous avons des joueurs qui ont été éliminés, leur nom sera toujours
      // dans Players.players. Nous devons donc faire une boucle et vérifier que la valeur
      // pour chaque élément n'est pas nul. Cela permet d'enregistrer le composant Player
      // après que tous les joueurs ont été éliminés ou avant qu'aucun n'ait été créé.
      si (joueurs &&
          playerNames.length > 0 &&
          playerNames.map((pname) => players[pname]).every(Boolean)) {
        lance une nouvelle erreur ('Impossible d'enregistrer le composant Player après la création du joueur.') ;
      }
    }

    Component.components_[name] = ComponentToRegister ;
    Component.components_[toLowerCase(name)] = ComponentToRegister ;

    return ComponentToRegister ;
  }

  /**
   * Récupère un `Composant` en se basant sur le nom avec lequel il a été enregistré.
   *
   * @param {string} name
   *        Le nom du composant à obtenir.
   *
   * @return {Component}
   *         Le `Composant` qui a été enregistré sous le nom donné.
   */
  static getComponent(name) {
    if (!name || !Component.components_) {
      retour ;
    }

    return Component.components_[name] ;
  }
}

/**
 * Si ce composant supporte ou non `requestAnimationFrame`.
 *
 * Elle est exposée principalement à des fins de test.
 *
 * @private
 * @type {Booléen}
 */
Component.prototype.supportsRaf_ = typeof window.requestAnimationFrame === 'function' &&
  typeof window.cancelAnimationFrame === 'function' ;

Component.registerComponent('Component', Component) ;

exporter le composant par défaut ;