/**
 * 播放器組件 - 所有 UI 對象的基類
 *
 * @file component.js
 */
從“全局/窗口”導入窗口;
從'./mixins/evented'導入事件;
從“./mixins/stateful”導入有狀態;
import * as Dom from './utils/dom.js';
從 './utils/fn.js' 導入 * 作為 Fn;
從 './utils/guid.js' 導入 * 作為 Guid;
從 './utils/string-cases.js' 導入 {toTitleCase, toLowerCase};
從 './utils/merge-options.js' 導入 mergeOptions;
從'./utils/computed-style'導入計算樣式;
從“./utils/map.js”導入地圖;
從'./utils/set.js'導入集合;
從“鍵碼”導入鍵碼;

/**
 * 所有 UI 組件的基類。
 * 組件是代表 javascript 對象和元素的 UI 對象
 * 在 DOM 中。它們可以是其他組件的子組件,並且可以有
 * 孩子們自己。
 *
 * 組件也可以使用來自 {@link EventTarget} 的方法
 */
類組件{

  /**
   * 組件準備就緒時調用的回調。沒有任何
   * 參數和任何回調值將被忽略。
   *
   * @callback 組件~ReadyCallback
   * @this 組件
   */

  /**
   * 創建此類的一個實例。
   *
   * @param {Player} 播放器
   * 此類應附加到的 `Player`。
   *
   * @param {對象} [選項]
   * 組件選項的鍵/值存儲。
   *
   * @param {Object[]} [options.children]
   * 用於初始化此組件的子對像數組。兒童對像有
   * 如果需要多個相同類型的組件,將使用名稱屬性
   * 添加。
   *
   * @param {string} [options.className]
   * 類或空格分隔的類列表以添加組件
   *
   * @param {Component~ReadyCallback} [就緒]
   * `Component` 準備就緒時調用的函數。
   */
  構造函數(播放器,選項,準備就緒){

    // 組件可能是播放器本身,我們不能將 `this` 傳遞給 super
    如果(!播放器 && 這個。播放){
      this.player_ = player = this; // eslint-disable-line
    }其他{
      this.player_ = 玩家;
    }

    this.isDisposed_ = false;

    // 通過 `addChild` 方法保存對父組件的引用
    this.parentComponent_ = null;

    // 複製 prototype.options_ 以防止覆蓋默認值
    this.options_ = mergeOptions({}, this.options_);

    // 使用提供的選項更新選項
    options = this.options_ = mergeOptions(this.options_, options);

    // 如果提供了選項或選項元素,則從中獲取 ID
    this.id_ = options.id || (options.el && options.el.id);

    // 如果選項中沒有 ID,則生成一個
    如果(!this.id_){
      // 在模擬玩家的情況下不需要玩家 ID 函數
      const id = player && player.id && player.id() || 'no_player';

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

    this.name_ = options.name ||無效的;

    // 如果選項中沒有提供,則創建元素
    如果(選項.el){
      this.el_ = options.el;
    } 否則,如果(選項。創建!== 假){
      this.el_ = this.createEl();
    }

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

    // 如果 evented 是除 false 之外的任何值,我們想在 evented 中混合
    如果(選項。事件!==假){
      // 使它成為事件對象並使用 `el_`(如果可用)作為其事件總線
      事件(這,{eventBusKey:this.el_?'el_':null});

      this.handleLanguagechange = this.handleLanguagechange.bind(this);
      this.on(this.player_, 'languagechange', this.handleLanguagechange);
    }
    有狀態的(這,this.constructor.defaultState);

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

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

    // 在選項中添加任何子組件
    如果(選項。initChildren !== false){
      這個.initChildren();
    }

    // 不想在這裡觸發就緒,否則它會在 init 實際啟動之前觸發
    // 為運行此構造函數的所有子級完成
    this.ready(準備就緒);

    如果(選項。報告活動!== 假){
      this.enableTouchActivity();
    }

  }

  /**
   * 處理 `Component` 和所有子組件。
   *
   * @fires 組件#dispose
   *
   * @param {Object} 選項
   * @param {Element} options.originalEl 用來替換播放器元素的元素
   */
  處置(選項={}){

    // 如果組件已經被處置,則退出。
    如果(this.isDisposed_){
      返回;
    }

    如果(this.readyQueue_){
      this.readyQueue_.length = 0;
    }

    /**
     * 當 `Component` 被釋放時觸發。
     *
     * @event 組件#dispose
     * @type {EventTarget~Event}
     *
     * @property {boolean} [氣泡=假]
     * 設置為 false 這樣 dispose 事件就不會
     * 冒泡
     */
    this.trigger({type: 'dispose', bubbles: false});

    this.isDisposed_ = true;

    // 處置所有孩子。
    如果(this.children_){
      for (let i = this.children_.length - 1; i >= 0; i--) {
        如果(this.children_[i].dispose){
          this.children_[i].dispose();
        }
      }
    }

    // 刪除子引用
    this.children_ = null;
    this.childIndex_ = null;
    this.childNameIndex_ = null;

    this.parentComponent_ = null;

    如果(this.el_){
      // 從 DOM 中移除元素
      如果(this.el_.parentNode){
        如果(選項。restoreEl){
          this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
        }其他{
          this.el_.parentNode.removeChild(this.el_);
        }
      }

      this.el_ = null;
    }

    // 在處理元素後刪除對播放器的引用
    this.player_ = null;
  }

  /**
   * 判斷這個組件是否已經被釋放。
   *
   * @return {布爾值}
   * 如果組件已被處置,則為“true”。否則為“假”。
   */
  isDisposed() {
    返回布爾值(this.isDisposed_);
  }

  /**
   * 返回 `Component` 附加到的 {@link Player}。
   *
   * @return {玩家}
   * 這個 `Component` 附加到的播放器。
   */
  玩家(){
    返回這個.player_;
  }

  /**
   * 選項對象與新選項的深度合併。
   * > 注意:當 `obj` 和 `options` 都包含值為對象的屬性時。
   * 這兩個屬性得到合併使用 {@link 模塊:合併選項}
   *
   * @param {對象}對象
   * 包含新選項的對象。
   *
   * @return {對象}
   * `this.options_` 和 `obj` 合併在一起的新對象。
   */
  選項(對象){
    如果(!對象){
      返回 this.options_;
    }

    this.options_ = mergeOptions(this.options_, obj);
    返回 this.options_;
  }

  /**
   * 獲取 `Component` 的 DOM 元素
   *
   * @return {元素}
   * 此 `Component` 的 DOM 元素。
   */
  埃爾(){
    返回這個。el_;
  }

  /**
   * 創建 `Component` 的 DOM 元素。
   *
   * @param {string} [標籤名]
   * 元素的 DOM 節點類型。例如'div'
   *
   * @param {對象} [屬性]
   * 應設置的屬性對象。
   *
   * @param {對象} [屬性]
   * 應設置的屬性對象。
   *
   * @return {元素}
   * 被創建的元素。
   */
  createEl(tagName, properties, attributes) {
    返回 Dom.createEl(tagName, properties, attributes);
  }

  /**
   * 本地化給定字符串的英文字符串。
   *
   * 如果提供了令牌,它將嘗試對提供的字符串運行簡單的令牌替換。
   * 它查找的標記看起來像 `{1}`,索引為 1-indexed 到標記數組中。
   *
   * 如果提供了 `defaultValue`,它將在 `string` 上使用它,
   * 如果在提供的語言文件中找不到值。
   * 如果你想有一個用於令牌替換的描述性密鑰,這很有用
   * 但是有一個簡潔的本地化字符串,不需要包含 `en.json`。
   *
   * 目前用於進度條計時。
   *```js
   * {
   * "進度條計時:currentTime={1} duration={2}": "{1} of {2}"
   * }
   *```
   * 然後像這樣使用:
   *```js
   * this.localize('進度條計時:currentTime={1} duration{2}',
   * [this.player_.currentTime(), this.player_.duration()],
   * '{1} of {2}');
   *```
   *
   * 其中輸出如下內容:
   *
   *
   * @param {string} 字符串
   * 要本地化的字符串和要在語言文件中查找的鍵。
   * @param {string[]} [令牌]
   * 如果當前項目有代幣替換,請在此處提供代幣。
   * @param {string} [默認值]
   * 默認為“字符串”。可以是用於令牌替換的默認值
   * 如果查找鍵需要分開。
   *
   * @return {字符串}
   * 本地化字符串,如果沒有本地化,則為英文字符串。
   */
  本地化(字符串,標記,默認值=字符串){

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

    讓 localizedString = defaultValue;

    如果(語言&&語言[字符串]){
      localizedString = 語言[字符串];
    } else if (primaryLang && primaryLang[string]) {
      localizedString = primaryLang[字符串];
    }

    如果(代幣){
      localizedString = localizedString.replace(/\\{(\\d+)\\}/g, function(match, index) {
        const value = tokens[index - 1];
        讓 ret = 價值;

        如果(類型值==='未定義'){
          ret = 匹配;
        }

        返還;
      });
    }

    返回本地化字符串;
  }

  /**
   * 處理組件中播放器的語言更改。應該被子組件覆蓋。
   *
   * @抽象的
   */
  handleLanguagechange() {}

  /**
   * 返回 `Component` 的 DOM 元素。這是孩子們被插入的地方。
   * 這通常與 {@link Component#el} 中返回的元素相同。
   *
   * @return {元素}
   * 此 `Component` 的內容元素。
   */
  contentEl() {
    返回 this.contentEl_ ||這個.el_;
  }

  /**
   * 獲取這個 `Component` 的 ID
   *
   * @return {字符串}
   * 這個 `Component` 的 id
   */
  身份證(){
    返回這個.id_;
  }

  /**
   * 獲取 `Component` 的名稱。該名稱用於引用“組件”
   * 並在註冊期間設置。
   *
   * @return {字符串}
   * 此 `Component` 的名稱。
   */
  姓名() {
    返回此名稱_;
  }

  /**
   * 獲取所有子組件的數組
   *
   * @return {數組}
   * 這些孩子
   */
  孩子們() {
    返回這個。children_;
  }

  /**
   * 返回具有給定 `id` 的子 `Component`。
   *
   * @param {string} id
   * 要獲取的子 `Component` 的 id。
   *
   * @return {組件|未定義}
   * 具有給定 id 或未定義的子 Component 。
   */
  getChildById(id) {
    返回 this.childIndex_[id];
  }

  /**
   * 返回具有給定“名稱”的子“組件”。
   *
   * @param {string} 名稱
   * 要獲取的子組件的名稱。
   *
   * @return {組件|未定義}
   * 具有給定“名稱”或未定義的子“組件”。
   */
  得到孩子(名字){
    如果(!名稱){
      返回;
    }

    返回 this.childNameIndex_[name];
  }

  /**
   * 返回給定後的後代 `Component`
   * 後代 `names`。例如 ['foo', 'bar', 'baz'] 會
   * 嘗試在當前組件上獲取“foo”,在“foo”上獲取“bar”
   * component 和 'bar' 組件上的 'baz' 並返回 undefined
   * 如果其中任何一個不存在。
   *
   * @param {...string[]|...string} 名稱
   * 要獲取的子組件的名稱。
   *
   * @return {組件|未定義}
   * 給定後代之後的後代 `Component`
   * `names` 或未定義。
   */
  getDescendant(...名字){
    // 將數組參數展平到主數組中
    names = names.reduce((acc, n) => acc.concat(n), []);

    讓 currentChild = 這個;

    對於(讓我 = 0; 我的 < 名字。長度; 我 ++){
      currentChild = currentChild.getChild(名字[i]);

      如果(!currentChild || !currentChild.getChild){
        返回;
      }
    }

    返回當前子;
  }

  /**
   * 在當前 `Component` 中添加一個子 `Component`。
   *
   *
   * @param {string|Component} 孩子
   * 要添加的子項的名稱或實例。
   *
   * @param {對象} [選項={}]
   * 將傳遞給子項的選項的鍵/值存儲
   * 孩子。
   *
   * @param {number} [index=this.children_.length]
   * 嘗試將孩子添加到的索引。
   *
   * @return {組件}
   * 作為子組件添加的 `Component`。當使用字符串時
   * `Component` 將由此過程創建。
   */
  addChild(child, options = {}, index = this.children_.length) {
    讓組件;
    讓組件名稱;

    // 如果 child 是一個字符串,創建帶有選項的組件
    如果 (typeof child === 'string') {
      componentName = toTitleCase(child);

      const componentClassName = options.componentClass ||組件名稱;

      // 通過選項設置名稱
      options.name = 組件名稱;

      // 為此控件集創建一個新的對象和元素
      // 如果沒有.player_,這是一個玩家
      const ComponentClass = Component.getComponent(componentClassName);

      如果(!組件類){
        throw new Error(`組件 ${componentClassName} 不存在`);
      }

      // 直接存儲在 videojs 對像上的數據可能是
      // 錯誤識別為要保留的組件
      //與 4.x 向後兼容性。檢查以確保
      // 組件類可以被實例化。
      如果(類型組件類!== '功能') {
        返回空值;
      }

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

    // child 是一個組件實例
    }其他{
      組件=孩子;
    }

    如果(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;
    }

    // 如果名稱未用於創建組件,請檢查我們是否可以使用
    // 組件的名稱函數
    組件名 = 組件名 || (component.name && toTitleCase(component.name()));

    如果(組件名稱){
      this.childNameIndex_[componentName] = component;
      this.childNameIndex_[toLowerCase(componentName)] = component;
    }

    // 將 UI 對象的元素添加到容器 div (box)
    // 不需要有一個元素
    if (typeof component.el === 'function' && component.el()) {
      // 如果在組件之前插入,則在該組件的元素之前插入
      讓 refNode = null;

      如果 (this.children_[index + 1]) {
        // 大多數孩子都是組件,但視頻技術是一個 HTML 元素
        如果 (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);
    }

    // 如果需要,返回以便它可以存儲在父對像上。
    返回組件;
  }

  /**
   * 從此 `Component` 的子列表中刪除一個子 `Component`。同時刪除
   * 來自此 `Component` 元素的子 `Component` 元素。
   *
   * @param {Component} 組件
   * 要刪除的子 `Component`。
   */
  removeChild(組件){
    if (typeof component === 'string') {
      組件 = this.getChild(組件);
    }

    如果(!組件||!this.children_){
      返回;
    }

    讓 childFound = false;

    for (let i = this.children_.length - 1; i >= 0; i--) {
      如果(this.children_[i] === 組件){
        找到孩子=真;
        this.children_.splice(i, 1);
        休息;
      }
    }

    如果(!childFound){
      返回;
    }

    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());
    }
  }

  /**
   * 根據選項添加和初始化默認子組件。
   */
  初始化兒童(){
    const children = this.options_.children;

    如果(孩子們){
      // `this` 是 `parent`
      const parentOptions = this.options_;

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

        // 允許在父選項中設置子選項
        // 例如 videojs(id, { controlBar: false });
        // 而不是 videojs(id, { children: { controlBar: false });
        如果(父選項[名稱]!==未定義){
          opts = parentOptions[名稱];
        }

        // 允許禁用默認組件
        // 例如 options['children']['posterImage'] = false
        如果(選擇===假){
          返回;
        }

        // 如果沒有配置,允許將選項作為簡單的布爾值傳遞
        // 是必要的。
        如果(選擇===真){
          選擇={};
        }

        // 我們還想傳遞原始播放器選項
        // 對每個組件也是如此,所以他們不需要
        // 稍後返回播放器以獲取選項。
        opts.playerOptions = this.options_.playerOptions;

        // 創建並添加子組件。
        // 在父實例上按名稱添加對子實例的直接引用。
        // 如果使用了兩個相同的組件,則應提供不同的名稱
        // 對於每個
        const newChild = this.addChild(name, opts);

        如果(新孩子){
          這個[名字] = newChild;
        }
      };

      // 允許在選項中傳遞一組子項詳細信息
      讓工作的孩子;
      const Tech = Component.getComponent('Tech');

      如果(Array.isArray(孩子)){
        workingChildren = 孩子們;
      }其他{
        workingChildren = Object.keys(children);
      }

      童工
      // 在 this.options_ 和 workingChildren 中的孩子會
      // 給我們額外的我們不想要的孩子。所以,我們想過濾掉它們。
        .concat(Object.keys(this.options_)
          .filter(函數(子){
            返回!workingChildren.some(函數(wchild){
              如果 (typeof wchild === 'string') {
                返回孩子 === wchild;
              }
              返回孩子 === wchild.name;
            });
          }))
        .map((子) => {
          讓名字;
          讓選擇;

          如果 (typeof child === 'string') {
            名字=孩子;
            opts = children[名字] || this.options_[名稱] || {};
          }其他{
            名字=孩子的名字;
            選擇=孩子;
          }

          返回{名稱,選擇};
        })
        .filter((子) => {
        // 我們必須確保 child.name 不在 techOrder 中,因為
        // 技術被註冊為組件但不能不兼容
        // 請參閱 https://github.com/videojs/video.js/issues/2772
          const c = Component.getComponent(child.opts.componentClass ||
                                       toTitleCase(child.name));

          返回 c &&!科技科技 (c);
        })
        .forEach(句柄添加);
    }
  }

  /**
   * 構建默認的 DOM 類名。應該被子組件覆蓋。
   *
   * @return {字符串}
   * 此對象的 DOM 類名稱。
   *
   * @抽象的
   */
  buildCSSClass() {
    // 子類可以包含一個函數:
    // 返回 '類名' + this._super();
    返回 '';
  }

  /**
   * 將偵聽器綁定到組件的就緒狀態。
   * 與事件監聽器的不同之處在於,如果就緒事件已經發生
   * 它會立即觸發該功能。
   *
   * @return {組件}
   * 返回自身;方法可以鏈接。
   */
  就緒(fn,sync = false){
    如果(!fn){
      返回;
    }

    如果(!this.isReady_){
      this.readyQueue_ = this.readyQueue_ || [];
      this.readyQueue_.push(fn);
      返回;
    }

    如果(同步){
      fn.call(this);
    }其他{
      // 為了保持一致性,默認異步調用函數
      this.setTimeout(fn, 1);
    }
  }

  /**
   * 為這個 `Component` 觸發所有準備好的監聽器。
   *
   * @fires 組件#ready
   */
  觸發就緒(){
    this.isReady_ = true;

    // 確保 ready 被異步觸發
    this.setTimeout(函數() {
      const readyQueue = this.readyQueue_;

      // 重置就緒隊列
      this.readyQueue_ = [];

      如果 (readyQueue && readyQueue.length > 0) {
        readyQueue.forEach(函數(fn){
          fn.call(this);
        }, 這);
      }

      // 也允許使用事件監聽器
      /**
       * 當 `Component` 就緒時觸發。
       *
       * @event 組件#ready
       * @type {EventTarget~Event}
       */
      this.trigger('準備好');
    }, 1);
  }

  /**
   * 查找與 `selector` 匹配的單個 DOM 元素。這可以在“組件”中
   * `contentEl()` 或其他自定義上下文。
   *
   * @param {string} 選擇器
   * 一個有效的 CSS 選擇器,將傳遞給 querySelector。
   *
   * @param {Element|string} [context=this.contentEl()]
   * 在其中查詢的 DOM 元素。也可以是一個選擇器字符串
   * 在這種情況下,第一個匹配元素將用作上下文。如果
   * 使用缺少的 `this.contentEl()`。如果 `this.contentEl()` 返回
   * 什麼都沒有,它會返回到 `document`。
   *
   * @return {元素|null}
   * 找到的 dom 元素,或者為 null
   *
   * @see [關於 CSS 選擇器的信息](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $(選擇器,上下文){
    返回 Dom.$(選擇器,上下文 || this.contentEl());
  }

  /**
   * 查找所有與 `selector` 匹配的 DOM 元素。這可以在“組件”中
   * `contentEl()` 或其他自定義上下文。
   *
   * @param {string} 選擇器
   * 一個有效的 CSS 選擇器,將被傳遞給 querySelectorAll 。
   *
   * @param {Element|string} [context=this.contentEl()]
   * 在其中查詢的 DOM 元素。也可以是一個選擇器字符串
   * 在這種情況下,第一個匹配元素將用作上下文。如果
   * 使用缺少的 `this.contentEl()`。如果 `this.contentEl()` 返回
   * 什麼都沒有,它會返回到 `document`。
   *
   * @return {節點列表}
   * 找到的 dom 元素列表
   *
   * @see [關於 CSS 選擇器的信息](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $$(選擇器,上下文){
    返回 Dom.$$(選擇器,上下文 || this.contentEl());
  }

  /**
   * 檢查組件的元素是否有 CSS 類名。
   *
   * @param {string} classToCheck
   * 要檢查的 CSS 類名。
   *
   * @return {布爾值}
   * - 如果 `Component` 有類則為真。
   * - 如果 `Component` 沒有類,則為假
   */
  hasClass(classToCheck){
    返回 Dom.hasClass(this.el_, classToCheck);
  }

  /**
   * 將 CSS 類名稱添加到 `Component` 的元素。
   *
   * @param {string} classToAdd
   * 要添加的 CSS 類名
   */
  添加類(類添加){
    Dom.addClass(this.el_, classToAdd);
  }

  /**
   * 從 `Component` 的元素中移除一個 CSS 類名。
   *
   * @param {string} classToRemove
   * 要刪除的 CSS 類名
   */
  removeClass(classToRemove) {
    Dom.removeClass(this.el_, classToRemove);
  }

  /**
   * 在組件的元素中添加或刪除 CSS 類名。
   * - 當 {@link Component#hasClass} 返回 false 時添加 `classToToggle`。
   * - 當 {@link Component#hasClass} 返回 true 時,`classToToggle` 被移除。
   *
   * @param {string} classToToggle
   * 添加或刪除的類基於 (@link Component#hasClass}
   *
   * @param {boolean|Dom~predicate} [謂詞]
   * {@link Dom~predicate} 函數或布爾值
   */
  toggleClass(classToToggle,謂詞){
    Dom.toggleClass(this.el_, classToToggle, predicate);
  }

  /**
   * 如果 `Component` 的元素被隱藏,則顯示它
   * 來自它的“vjs-hidden”類名。
   */
  展示() {
    this.removeClass('vjs-hidden');
  }

  /**
   * 如果 `Component` 的元素當前正在顯示,則隱藏它,方法是添加
   * 'vjs-hidden` 類名。
   */
  隱藏() {
    this.addClass('vjs-hidden');
  }

  /**
   * 通過添加 'vjs-lock-showing' 將 `Component` 的元素鎖定在其可見狀態
   * 它的類名。在 fadeIn/fadeOut 期間使用。
   *
   * @私人的
   */
  鎖定顯示(){
    this.addClass('vjs-lock-showing');
  }

  /**
   * 通過刪除 'vjs-lock-showing' 將 `Component` 的元素從其可見狀態解鎖
   * 來自它的類名。在 fadeIn/fadeOut 期間使用。
   *
   * @私人的
   */
  解鎖顯示(){
    this.removeClass('vjs-lock-showing');
  }

  /**
   * 獲取 `Component` 元素的屬性值。
   *
   * @param {string} 屬性
   * 要從中獲取值的屬性的名稱。
   *
   * @return {字符串|空}
   * - 請求的屬性值。
   * - 如果屬性不存在,在某些瀏覽器上可以是空字符串
   * 或沒有價值
   * - 如果屬性不存在或已經存在,大多數瀏覽器將返回 null
   * 沒有價值。
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
   */
  getAttribute(屬性){
    返回 Dom.getAttribute(this.el_, attribute);
  }

  /**
   * 設置 `Component` 元素的屬性值
   *
   * @param {string} 屬性
   * 要設置的屬性的名稱。
   *
   * @param {string} 值
   * 將屬性設置為的值。
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
   */
  setAttribute(屬性,值){
    Dom.setAttribute(this.el_, attribute, value);
  }

  /**
   * 從 `Component` 的元素中刪除一個屬性。
   *
   * @param {string} 屬性
   * 要刪除的屬性的名稱。
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
   */
  刪除屬性(屬性){
    Dom.removeAttribute(this.el_, attribute);
  }

  /**
   * 根據 CSS 樣式獲取或設置組件的寬度。
   * 有關更多詳細信息,請參閱{@link Component#dimension}。
   *
   * @param {數字|字符串} [數字]
   * 您要設置的寬度後綴為“%”、“px”或什麼都不加。
   *
   * @param {boolean} [skipListeners]
   * 跳過componentresize事件觸發
   *
   * @return {數字|字符串}
   * 獲取時的寬度,沒有寬度則為零。可以是字符串
   * 用 '%' 或 'px' 後綴。
   */
  寬度(num,skipListeners){
    返回 this.dimension('width', num, skipListeners);
  }

  /**
   * 根據 CSS 樣式獲取或設置組件的高度。
   * 有關更多詳細信息,請參閱{@link Component#dimension}。
   *
   * @param {數字|字符串} [數字]
   * 您要設置的高度後綴為“%”、“px”或什麼都不加。
   *
   * @param {boolean} [skipListeners]
   * 跳過componentresize事件觸發
   *
   * @return {數字|字符串}
   * 獲取時的寬度,沒有寬度則為零。可以是字符串
   * 用 '%' 或 'px' 後綴。
   */
  高度(數量,skipListeners){
    返回 this.dimension('height', num, skipListeners);
  }

  /**
   * 同時設置 `Component` 元素的寬度和高度。
   *
   * @param {number|string} 寬度
   * 設置 `Component` 元素的寬度。
   *
   * @param {number|string} 高度
   * 將 `Component` 的元素設置為的高度。
   */
  尺寸(寬度,高度){
    // 跳過寬度上的 componentresize 監聽器以進行優化
    這個。寬度(寬度,真);
    這個。高度(高度);
  }

  /**
   * 獲取或設置 `Component` 元素的寬度或高度。這是共享代碼
   * 對於 {@link Component#width} 和 {@link Component#height}。
   *
   * 須知:
   * - 如果數字中的寬度或高度,這將返回帶有“px”後綴的數字。
   * - 如果寬度/高度是百分比,這將返回後綴為“%”的百分比
   * - 隱藏元素的寬度為 0,帶有 `window.getComputedStyle`。這個功能
   * 默認為 `Component` 的 `style.width` 並退回到 `window.getComputedStyle`。
   * 請參見 [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
   * 了解更多信息
   * - 如果您想要組件的計算樣式,請使用 {@link Component#currentWidth}
   * 和 {@link {Component#currentHeight}
   *
   * @fires 組件#componentresize
   *
   * @param {string} widthOrHeight
   8 '寬度'或'高度'
   *
   * @param {數字|字符串} [數字]
   8 新維度
   *
   * @param {boolean} [skipListeners]
   * 跳過componentresize事件觸發
   *
   * @return {數字}
   * 獲取時的維度,未設置時為0
   */
  維度(widthOrHeight,num,skipListeners){
    如果(數字!==未定義){
      //設置為零,如果為空或字面上 NaN(NaN!== NaN)
      如果(數字 === 空 || 數!== 數字){
        數 = 0;
      }

      // 檢查是否使用 css 寬度/高度(% 或 px)並調整
      如果(「+ 號)。指數('%')!== -1 || (」+ 數字). 索引 ('像素')!-1) {
        this.el_.style[widthOrHeight] = num;
      } else if (num === 'auto') {
        this.el_.style[widthOrHeight] = '';
      }其他{
        this.el_.style[widthOrHeight] = num + 'px';
      }

      // skipListeners 允許我們在設置寬度和高度時避免觸發調整大小事件
      如果(!skipListeners){
        /**
         * 調整組件大小時觸發。
         *
         * @event 組件#componentresize
         * @type {EventTarget~Event}
         */
        this.trigger('componentresize');
      }

      返回;
    }

    // 沒有設置值,所以獲取它
    // 確保元素存在
    如果(!this.el_){
      返回 0;
    }

    // 從樣式中獲取維度值
    const val = this.el_.style[widthOrHeight];
    const pxIndex = val.indexOf('px');

    如果(pX 指數!-1) {
      // 返回沒有'px'的像素值
      返回 parseInt(val.slice(0, pxIndex), 10);
    }

    // 沒有 px 所以使用 % 或沒有設置樣式,所以退回到 offsetWidth/height
    //如果組件有顯示:無,偏移量將返回 0
    // TODO: 使用 px 處理顯示:無和無尺寸樣式
    返回 parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
  }

  /**
   * 獲取組件元素的計算寬度或高度。
   *
   * 使用“window.getComputedStyle”。
   *
   * @param {string} widthOrHeight
   * 包含“寬度”或“高度”的字符串。想要哪個就拿哪個。
   *
   * @return {數字}
   * 被要求的維度,如果沒有設置則為 0
   * 對於那個維度。
   */
  當前尺寸(寬度或高度){
    讓 computedWidthOrHeight = 0;

    如果(寬度或高度!=='寬度'&&寬度或高度!=='高度'){
      throw new Error('currentDimension 只接受寬度或高度值');
    }

    computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);

    // 從變量中移除 'px' 並解析為整數
    computedWidthOrHeight = parseFloat(computedWidthOrHeight);

    // 如果計算值仍然為 0,則可能是瀏覽器在撒謊
    // 我們要檢查偏移值。
    // 此代碼也會在 getComputedStyle 不存在的地方運行。
    如果(計算寬度或高度=== 0 || isNaN(計算寬度或高度)){
      const rule = `offset${toTitleCase(widthOrHeight)}`;

      computedWidthOrHeight = this.el_[rule];
    }

    返回計算寬度或高度;
  }

  /**
   * 包含 `Component` 的寬度和高度值的對象
   * 計算風格。使用“window.getComputedStyle”。
   *
   * @typedef {Object} 組件~DimensionObject
   *
   * @property {number} 寬度
   * `Component` 的計算樣式的寬度。
   *
   * @property {number} 高度
   * `Component` 的計算樣式的高度。
   */

  /**
   * 獲取包含計算出的寬度和高度值的對象
   * 組件的元素。
   *
   * 使用“window.getComputedStyle”。
   *
   * @return {組件~維度對象}
   * 組件元素的計算尺寸。
   */
  當前尺寸(){
    返回 {
      寬度:this.currentDimension('width'),
      高度:this.currentDimension('height')
    };
  }

  /**
   * 獲取組件元素的計算寬度。
   *
   * 使用“window.getComputedStyle”。
   *
   * @return {數字}
   * 組件元素的計算寬度。
   */
  當前寬度(){
    返回 this.currentDimension('width');
  }

  /**
   * 獲取組件元素的計算高度。
   *
   * 使用“window.getComputedStyle”。
   *
   * @return {數字}
   * 組件元素的計算高度。
   */
  當前高度(){
    返回 this.currentDimension('height');
  }

  /**
   * 將焦點設置到該組件
   */
  重點() {
    這個.el_.focus();
  }

  /**
   * 從此組件中移除焦點
   */
  模糊(){
    這個.el_.blur();
  }

  /**
   * 當此組件收到它不處理的 `keydown` 事件時,
   * 它將事件傳遞給 Player 進行處理。
   *
   * @param {EventTarget~Event} 事件
   * 導致調用此函數的 `keydown` 事件。
   */
  handleKeyDown(事件){
    如果(this.player_){

      // 我們只是在這裡停止傳播,因為我們希望未處理的事件落下
      // 返回瀏覽器。排除選項卡以進行焦點捕獲。
      如果(!密鑰代碼. 一鍵 (事件, '標籤')) {
        事件.stopPropagation();
      }
      this.player_.handleKeyDown(事件);
    }
  }

  /**
   * 許多組件曾經有一個 `handleKeyPress` 方法,這是很糟糕的
   * 之所以命名是因為它監聽了 `keydown` 事件。現在這個方法名
   * 委託給 `handleKeyDown`。這意味著任何調用“handleKeyPress”的人
   * 不會看到他們的方法調用停止工作。
   *
   * @param {EventTarget~Event} 事件
   * 導致調用此函數的事件。
   */
  handleKeyPress(事件){
    這個.handleKeyDown(事件);
  }

  /**
   * 當檢測到觸摸事件支持時發出“點擊”事件。這習慣了
   * 支持通過點擊視頻來切換控件。他們得到啟用
   * 因為否則每個子組件都會有額外的開銷。
   *
   * @私人的
   * @fires 組件#tap
   * @listens 組件#touchstart
   * @listens 組件#touchmove
   * @listens 組件#touchleave
   * @listens 組件#touchcancel
   * @listens 組件#touchend

   */
  emitTapEvents() {
    // 跟踪開始時間,以便我們可以確定觸摸持續了多長時間
    讓 touchStart = 0;
    讓 firstTouch = null;

    // 觸摸事件期間允許的最大移動仍被視為點擊
    // 其他流行的庫使用從 2 (hammer.js) 到 15 的任何地方,
    // 所以 10 看起來是一個不錯的整數。
    const tapMovementThreshold = 10;

    // 在仍被視為點擊的情況下觸摸的最大長度
    const touchTimeThreshold = 200;

    讓 couldBeTap;

    this.on('touchstart', 函數(事件) {
      // 如果多於一根手指,不要考慮將其視為點擊
      如果 (event.touches.length === 1) {
        // 從對像中複製 pageX/pageY
        第一次接觸 = {
          pageX: event.touches[0].pageX,
          pageY: event.touches[0].pageY
        };
        // 記錄開始時間,以便我們可以檢測到點擊與“觸摸並按住”
        touchStart = window.performance.now();
        // 重置 couldBeTap 跟踪
        couldBeTap = 真;
      }
    });

    this.on('touchmove', 函數(事件) {
      // 如果多於一根手指,不要考慮將其視為點擊
      如果 (event.touches.length > 1) {
        couldBeTap = 假;
      } else if (firstTouch) {
        // 除了最輕微的點擊,有些設備會拋出觸摸動作。
        // 所以,如果我們只移動了一小段距離,這仍然可以是一個水龍頭
        const xdiff = event.touches[0].pageX - firstTouch.pageX;
        const ydiff = event.touches[0].pageY - firstTouch.pageY;
        const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);

        如果(touchDistance > tapMovementThreshold){
          couldBeTap = 假;
        }
      }
    });

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

    // 去做:聽原來的目標。 http://youtu.be/DujfpXOKUp8?t=13m8s
    this.on('touchleave', noTap);
    this.on('touchcancel', noTap);

    // 當觸摸結束時,測量花費了多長時間並觸發適當的
    // 事件
    this.on('touchend', function(event) {
      firstTouch = null;
      // 只有當 touchmove/leave/cancel 事件沒有發生時才繼續
      如果(couldBeTap === 真){
        // 測量觸摸持續了多長時間
        const touchTime = window.performance.now() - touchStart;

        // 確保觸摸小於被視為點擊的閾值
        如果(觸摸時間<觸摸時間閾值){
          // 不要讓瀏覽器把它變成點擊
          事件.preventDefault();
          /**
           * 當一個 `Component` 被點擊時觸發。
           *
           * @event 組件#tap
           * @type {EventTarget~Event}
           */
          this.trigger('tap');
          // 複製 touchend 事件對象並更改
          // 如果其他事件屬性不准確,則鍵入要點擊的內容
          // Events.fixEvent 運行(例如 event.target)
        }
      }
    });
  }

  /**
   * 此函數會在觸摸事件發生時報告用戶活動。這可以得到
   * 被任何希望觸摸事件以另一種方式起作用的子組件關閉。
   *
   * 發生觸摸事件時報告用戶觸摸活動。用戶活動習慣
   * 確定控件何時顯示/隱藏。說到鼠標就簡單了
   * 事件,因為任何鼠標事件都應該顯示控件。所以我們捕獲鼠標
   * 向玩家冒泡並在發生時報告活動的事件。
   * 對於觸摸事件,它不像 `touchstart` 和 `touchend` 切換播放器那麼容易
   * 控制。所以觸摸事件也不能在玩家層面幫助我們。
   *
   * 異步檢查用戶活動。所以可能發生的是點擊事件
   * 在視頻中關閉控件。然後 `touchend` 事件冒泡到
   * 播放器。其中,如果它報告用戶活動,將把控件右
   * 重新開始。我們也不希望完全阻止觸摸事件冒泡。
   * 此外,“touchmove”事件和點擊以外的任何事件都不應轉動
   * 控件重新打開。
   *
   * @listens 組件#touchstart
   * @listens 組件#touchmove
   * @listens 組件#touchend
   * @listens 組件#touchcancel
   */
  enableTouchActivity() {
    // 如果根播放器不支持報告用戶活動,則不要繼續
    如果(!這個。播放器()||!這個. 播放器 (). 報告用戶活動) {
      返回;
    }

    // 用於報告用戶處於活動狀態的偵聽器
    const report = Fn.bind(this.player(), this.player().reportUserActivity);

    讓觸摸保持;

    this.on('touchstart', function() {
      報告();
      // 只要他們正在觸摸設備或按下鼠標,
      // 即使他們沒有移動手指或鼠標,我們也認為他們是活躍的。
      // 所以我們想繼續更新他們是活躍的
      this.clearInterval(touchHolding);
      // 以與 activityCheck 相同的時間間隔報告
      touchHolding = this.setInterval(報告, 250);
    });

    const touchEnd = 函數(事件){
      報告();
      // 如果觸摸保持,則停止保持活動的間隔
      this.clearInterval(touchHolding);
    };

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

  /**
   * 沒有參數並綁定到“Component”的上下文中的回調。
   *
   * @callback 組件~GenericCallback
   * @this 組件
   */

  /**
   * 創建一個在 `x` 毫秒超時後運行的函數。這個函數是一個
   * `window.setTimeout` 的包裝器。使用這個有幾個原因
   * 相反:
   * 1。它通過 {@link Component#clearTimeout} 清除
   * {@link Component#dispose} 被調用。
   * 2。函數回調將變成 {@link Component~GenericCallback}
   *
   * > 注意:您不能在此函數返回的 id 上使用 `window.clearTimeout`。這個
   * 將導致它的處置偵聽器不被清理!請用
   * {@link Component#clearTimeout} 或 {@link Component#dispose} 代替。
   *
   * @param {Component~GenericCallback} fn
   * 將在 `timeout` 後運行的函數。
   *
   * @param {number} 超時
   * 在執行指定函數之前延遲的超時時間(以毫秒為單位)。
   *
   * @return {數字}
   * 返回用於標識超時的超時 ID。它還可以
   * 在 {@link Component#clearTimeout} 中使用以清除超時
   * 已設置。
   *
   * @listens 組件#dispose
   * @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
   */
  設置超時(fn,超時){
    // 聲明為變量,以便它們在超時函數中正確可用
    // eslint-disable-next-line
    var timeoutId,disposeFn;

    fn = Fn.bind(this, fn);

    this.clearTimersOnDispose_();

    timeoutId = window.setTimeout(() => {
      如果(this.setTimeoutIds_.has(timeoutId)){
        this.setTimeoutIds_.delete(timeoutId);
      }
      fn();
    }, 暫停);

    this.setTimeoutIds_.add(timeoutId);

    返回超時標識;
  }

  /**
   * 清除通過“window.setTimeout”或創建的超時
   * {@link 組件#setTimeout}。如果您通過 {@link Component#setTimeout} 設置超時
   * 使用此函數代替 `window.clearTimout`。如果你不處理
   * 直到 {@link Component#dispose} 才會清理監聽器!
   *
   * @param {number} timeoutId
   * 要清除的超時 ID。的返回值
   * {@link Component#setTimeout} 或 `window.setTimeout`。
   *
   * @return {數字}
   * 返回已清除的超時 ID。
   *
   * @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
   */
  clearTimeout(timeoutId) {
    如果(this.setTimeoutIds_.has(timeoutId)){
      this.setTimeoutIds_.delete(timeoutId);
      window.clearTimeout(timeoutId);
    }

    返回超時標識;
  }

  /**
   * 創建一個每“x”毫秒運行一次的函數。這個函數是一個包裝器
   * 圍繞 `window.setInterval`。不過,有幾個理由改用這個。
   * 1。它通過 {@link Component#clearInterval} 清除,當
   * {@link Component#dispose} 被調用。
   * 2。函數回調將是一個 {@link Component~GenericCallback}
   *
   * @param {Component~GenericCallback} fn
   * 每 `x` 秒運行一次的函數。
   *
   * @param {number} 間隔
   * 每 `x` 毫秒執行指定的函數。
   *
   * @return {數字}
   * 返回可用於標識間隔的 id。它也可以用於
   * {@link Component#clearInterval} 清除間隔。
   *
   * @listens 組件#dispose
   * @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
   */
  設置間隔(fn,間隔){
    fn = Fn.bind(this, fn);

    this.clearTimersOnDispose_();

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

    this.setIntervalIds_.add(intervalId);

    返回間隔ID;
  }

  /**
   * 清除通過“window.setInterval”或創建的間隔
   * {@link 組件#setInterval}。如果您通過 {@link Component#setInterval} 設置間隔
   * 使用此函數代替 `window.clearInterval`。如果你不處理
   * 直到 {@link Component#dispose} 才會清理監聽器!
   *
   * @param {number} intervalId
   * 要清除的間隔的 id。的返回值
   * {@link Component#setInterval} 或 `window.setInterval`。
   *
   * @return {數字}
   * 返回被清除的間隔 id。
   *
   * @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
   */
  clearInterval(intervalId) {
    如果(this.setIntervalIds_.has(intervalId)){
      this.setIntervalIds_.delete(intervalId);
      window.clearInterval(intervalId);
    }

    返回間隔ID;
  }

  /**
   * 排隊要傳遞給 requestAnimationFrame (rAF) 的回調,但是
   * 有一些額外的獎勵:
   *
   * - 通過回退到不支持 rAF 的瀏覽器來支持
   * {@link 組件#setTimeout}。
   *
   * - 回調變成了 {@link Component~GenericCallback}(即
   * 綁定到組件)。
   *
   * - 如果組件處理自動取消 rAF 回調
   * 在被調用之前被釋放。
   *
   * @param {Component~GenericCallback} fn
   * 將綁定到此組件並僅執行的函數
   * 在瀏覽器的下一次重繪之前。
   *
   * @return {數字}
   * 返回用於識別超時的 rAF ID。它可以
   * 也可以用在 {@link Component#cancelAnimationFrame} 中取消
   * 動畫幀回調。
   *
   * @listens 組件#dispose
   * @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
   */
  requestAnimationFrame(fn) {
    // 退回到使用定時器。
    如果(!this.supportsRaf_){
      返回 this.setTimeout(fn, 1000 / 60);
    }

    this.clearTimersOnDispose_();

    // 聲明為變量,以便它們在 rAF 函數中正確可用
    // eslint-disable-next-line
    變量標識;
    fn = Fn.bind(this, fn);

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

    返回ID;
  }

  /**
   * 請求一個動畫幀,但只有一個命名動畫
   * 框架將排隊。另一個永遠不會添加,直到
   * 前一個結束。
   *
   * @param {string} 名稱
   * 給這個requestAnimationFrame的名字
   *
   * @param {Component~GenericCallback} fn
   * 將綁定到此組件並僅執行的函數
   * 在瀏覽器的下一次重繪之前。
   */
  requestNamedAnimationFrame(名稱, fn) {
    如果(this.namedRafs_.has(名稱)){
      返回;
    }
    this.clearTimersOnDispose_();

    fn = Fn.bind(this, fn);

    const id = this.requestAnimationFrame(() => {
      fn();
      如果(this.namedRafs_.has(名稱)){
        this.namedRafs_.delete(名字);
      }
    });

    this.namedRafs_.set(name, id);

    返回名稱;
  }

  /**
   * 取消當前命名的動畫幀(如果存在)。
   *
   * @param {string} 名稱
   * 要取消的 requestAnimationFrame 的名稱。
   */
  cancelNamedAnimationFrame(名稱){
    如果(!this.namedRafs_.has(名稱)){
      返回;
    }

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

  /**
   * 取消傳遞給 {@link Component#requestAnimationFrame} 的排隊回調
   * (rAF)。
   *
   * 如果您通過 {@link Component#requestAnimationFrame} 對 rAF 回調進行排隊,
   * 使用此函數代替 `window.cancelAnimationFrame`。如果你不這樣做,
   * 在 {@link Component#dispose} 之前,你的 dispose 偵聽器不會被清理!
   *
   * @param {number} id
   * 要清除的 rAF ID。{@link Component#requestAnimationFrame} 的返回值。
   *
   * @return {數字}
   * 返回被清除的 rAF ID。
   *
   * @see [類似於]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
   */
  取消動畫框架(id){
    // 退回到使用定時器。
    如果(!this.supportsRaf_){
      返回 this.clearTimeout(id);
    }

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

    返回ID;

  }

  /**
   * 設置`requestAnimationFrame`、`setTimeout`、
   * 和 `setInterval`,清除處置。
   *
   * > 以前每個計時器都自己添加和刪除處理偵聽器。
   * 為了更好的性能,決定對它們進行批處理,並使用 `Set`s
   * 跟踪未完成的計時器 ID。
   *
   * @私人的
   */
  clearTimersOnDispose_() {
    如果(this.clearingTimersOnDispose_){
      返回;
    }

    this.clearingTimersOnDispose_ = true;
    this.one('處置', () => {
      [
        ['namedRafs_', 'cancelNamedAnimationFrame'],
        ['rafIds_', 'cancelAnimationFrame'],
        ['setTimeoutIds_', 'clearTimeout'],
        ['setIntervalIds_', 'clearInterval']
      ].forEach(([idName, cancelName]) => {
        // 對於 `Set` 鍵,實際上又是值
        // 所以 forEach((val, val) =>` 但是對於我們想要使用的地圖
        // 鑰匙。
        this[idName].forEach((val, key) => this[cancelName](key));
      });

      this.clearingTimersOnDispose_ = false;
    });
  }

  /**
   * 給定名稱和組件,用 `videojs` 註冊一個 `Component`。
   *
   * > 注意:{@link Tech} 不應註冊為“組件”。 {@link 技術}
   * 應使用 {@link Tech.registerTech} 或
   * {@link 視頻:視頻. 註冊技術}.
   *
   * > 注意:這個函數也可以在videojs上看到為
   * {@link 視頻:視頻. 註冊組件}。
   *
   * @param {string} 名稱
   * 要註冊的 `Component` 的名稱。
   *
   * @param {Component} ComponentToRegister
   * 要註冊的 `Component` 類。
   *
   * @return {組件}
   * 已註冊的 `Component`。
   */
  靜態註冊組件(名稱,ComponentToRegister){
    如果(名稱類型!=='字符串'||!名稱){
      throw new Error(`非法組件名稱,“${name}”;必須是非空字符串。`);
    }

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

    // 我們需要確保僅在 Tech 已註冊時才進行此檢查。
    const isTech = Tech && Tech.isTech(ComponentToRegister);
    const isComp = Component === ComponentToRegister ||
      Component.prototype.isPrototypeOf(ComponentToRegister.prototype);

    如果(isTech || !isComp){
      讓理由;

      如果(是科技){
        reason = '技術人員必須使用 Tech.registerTech() 進行註冊';
      }其他{
        reason = '必須是 Component 的子類';
      }

      throw new Error(`非法組件, "${name}"; ${reason}.`);
    }

    名稱 = toTitleCase(名稱);

    如果(!Component.components_){
      組件.components_ = {};
    }

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

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

      // 如果我們有被處理掉的玩家,那麼他們的名字仍然是
      // 在 Players.players.所以,我們必須遍歷並驗證該值
      // 每個項目都不為空。這允許註冊播放器組件
      // 在處理完所有玩家之後或在創建任何玩家之前。
      如果(玩家&&
          playerNames.length > 0 &&
          playerNames.map((pname) => players[pname]).every(Boolean)) {
        throw new Error('創建播放器後無法註冊播放器組件。');
      }
    }

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

    返回組件註冊;
  }

  /**
   * 根據註冊的名稱獲取一個 `Component`。
   *
   * @param {string} 名稱
   * 要獲取的組件的名稱。
   *
   * @return {組件}
   * 以給定名稱註冊的 `Component`。
   */
  靜態獲取組件(名稱){
    如果(!名稱||!Component.components_){
      返回;
    }

    返回 Component.components_[name];
  }
}

/**
 * 該組件是否支持`requestAnimationFrame`。
 *
 * 這主要是為了測試目的而公開的。
 *
 * @私人的
 * @type {布爾}
 */
Component.prototype.supportsRaf_ = typeof window.requestAnimationFrame === '函數' &&
  typeof window.cancelAnimationFrame === '函數';

Component.registerComponent('組件', 組件);

導出默認組件;