/**
 * @file mixins/evented.js
 * @module 事件
 */
從“全局/窗口”導入窗口;
從 '../utils/dom' 導入 * 作為 Dom;
從“../utils/events”導入 * 作為事件;
從 '../utils/fn' 導入 * 作為 Fn;
從 '../utils/obj' 導入 * 作為 Obj;
從 '../event-target' 導入 EventTarget;
從 '../utils/dom-data' 導入 DomData;
從 '../utils/log' 導入日誌;

const objName = (obj) => {
  if (typeof obj.name === '函數') {
    返回對象名稱();
  }

  if (typeof obj.name === 'string') {
    返回對象名稱;
  }

  如果(obj.name_){
    返回對象名稱_;
  }

  如果(obj.constructor && obj.constructor.name){
    返回 obj.constructor.name;
  }

  返回對像類型;
};

/**
 * 返回對像是否應用了事件混入。
 *
 * @param {Object} 對象
 * 要測試的對象。
 *
 * @return {布爾值}
 * 對像是否出現事件。
 */
const isEvented = (對象) =>
  EventTarget 的對象實例 ||
  !!object.eventBusEl_ &&
  ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');

/**
 * 添加回調以在應用事件混入後運行。
 *
 * @param {Object} 對象
 * 要添加的對象
 * @param {函數} 回調
 * 要運行的回調。
 */
const addEventedCallback = (target, callback) => {
  如果(isEvented(目標)){
    打回來();
  }其他{
    如果(!target.eventedCallbacks){
      target.eventedCallbacks = [];
    }
    target.eventedCallbacks.push(回調);
  }
};

/**
 * 值是否是有效的事件類型 - 非空字符串或數組。
 *
 * @私人的
 * @param {string|Array} 類型
 * 要測試的類型值。
 *
 * @return {布爾值}
 * 該類型是否是有效的事件類型。
 */
const isValidEventType = (類型) =>
  // 此處的正則表達式驗證 `type` 至少包含一個非
  // 空白字符。
  (typeof type === 'string' && (/\\S/).test(type)) ||
  (Array.isArray(type) && !!type.length);

/**
 * 驗證一個值以確定它是否是一個有效的事件目標。如果沒有則拋出。
 *
 * @私人的
 * @throws {錯誤}
 * 如果目標似乎不是有效的事件目標。
 *
 * @param {Object} 目標
 * 要測試的對象。
 *
 * @param {對象}對象
 * 我們正在驗證的事件對象
 *
 * @param {string} fnName
 * 調用它的事件混合函數的名稱。
 */
const validateTarget = (target, obj, fnName) => {
  如果 (!target || (!target.nodeName && !isEvented(target))) {
    throw new Error(`${objName(obj)}#${fnName} 的無效目標;必須是 DOM 節點或事件對象。`);
  }
};

/**
 * 驗證一個值以確定它是否是一個有效的事件目標。如果沒有則拋出。
 *
 * @私人的
 * @throws {錯誤}
 * 如果該類型似乎不是有效的事件類型。
 *
 * @param {string|Array} 類型
 * 要測試的類型。
 *
 * @param {對象}對象
* 我們正在驗證的事件對象
 *
 * @param {string} fnName
 * 調用它的事件混合函數的名稱。
 */
const validateEventType = (type, obj, fnName) => {
  如果(!isValidEventType(類型)){
    throw new Error(`${objName(obj)}#${fnName} 的無效事件類型;必須是非空字符串或數組。`);
  }
};

/**
 * 驗證一個值以確定它是否是一個有效的偵聽器。如果沒有則拋出。
 *
 * @私人的
 * @throws {錯誤}
 * 如果偵聽器不是函數。
 *
 * @param {函數} 監聽器
 * 要測試的偵聽器。
 *
 * @param {對象}對象
 * 我們正在驗證的事件對象
 *
 * @param {string} fnName
 * 調用它的事件混合函數的名稱。
 */
const validateListener = (listener, obj, fnName) => {
  if (typeof listener !== 'function') {
    throw new Error(`${objName(obj)}#${fnName} 的偵聽器無效;必須是一個函數。`);
  }
};

/**
 * 獲取給 `on()` 或 `one()` 的參數數組,驗證它們,然後
 * 將它們規範化為一個對象。
 *
 * @私人的
 * @param {Object} 自我
 * 調用 `on()` 或 `one()` 的事件對象。這個
 * 對象將被綁定為偵聽器的“this”值。
 *
 * @param {數組} 參數
 * 傳遞給 `on()` 或 `one()` 的參數數組。
 *
 * @param {string} fnName
 * 調用它的事件混合函數的名稱。
 *
 * @return {對象}
 * 包含對 `on()` 或 `one()` 調用有用的值的對象。
 */
const normalizeListenArgs = (self, args, fnName) => {

  // 如果參數個數小於 3,目標總是
  // 事件對象本身。
  const isTargetingSelf = args.length < 3 || args[0] === 自我 || args[0] === self.eventBusEl_;
  讓目標;
  讓類型;
  讓聽眾;

  如果(isTargetingSelf){
    target = self.eventBusEl_;

    // 處理我們得到 3 個參數但仍在監聽的情況
    // 事件對象本身。
    如果(args.length >= 3){
      args.shift();
    }

    [類型,監聽器] = args;
  }其他{
    [目標、類型、監聽器] = args;
  }

  驗證目標(目標,自我,fnName);
  validateEventType(類型,自我,fnName);
  validateListener(監聽器,自我,fnName);

  listener = Fn.bind(self, listener);

  返回 {isTargetingSelf,目標,類型,偵聽器};
};

/**
 * 將偵聽器添加到目標上的事件類型,規範化
 * 目標類型。
 *
 * @私人的
 * @param {元素|對象}目標
 * DOM 節點或事件對象。
 *
 * @param {string} 方法
 * 要使用的事件綁定方法(“on”或“one”)。
 *
 * @param {string|Array} 類型
 * 一種或多種事件類型。
 *
 * @param {函數} 監聽器
 * 監聽函數。
 */
const listen = (target, method, type, listener) => {
  驗證目標(目標、目標、方法);

  如果(target.nodeName){
    事件[方法](目標、類型、偵聽器);
  }其他{
    目標[方法](類型,偵聽器);
  }
};

/**
 * 包含為傳遞的對象提供事件功能的方法
 * 到 {@link module:evented|evented}。
 *
 * @mixin 事件混合
 */
const EventedMixin = {

  /**
   * 添加一個偵聽器到這個對像或另一個事件的一個或多個事件
   * 目的。
   *
   * @param {string|Array|Element|Object} targetOrType
   * 如果這是一個字符串或數組,它表示事件類型
   * 這將觸發偵聽器。
   *
   * 可以在這里傳遞另一個事件對象,它將
   * 使偵聽器偵聽_that_ 對像上的事件。
   *
   * 在任何一種情況下,偵聽器的 `this` 值都將綁定到
   * 這個對象。
   *
   * @param {string|Array|Function} typeOrListener
   * 如果第一個參數是字符串或數組,這應該是
   * 監聽函數。否則,這是一個字符串或事件數組
   *類型。
   *
   * @param {函數} [監聽器]
   * 如果第一個參數是另一個事件對象,這將是
   * 監聽函數。
   */
  上(...參數){
    const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'on');

    listen(target, 'on', type, listener);

    // 如果這個對象正在監聽另一個事件對象。
    如果(!isTargetingSelf){

      // 如果這個對像被釋放,移除監聽器。
      const removeListenerOnDispose = () => this.off(target, type, listener);

      // 使用與偵聽器相同的函數 ID,以便我們稍後將其刪除
      // 使用原始偵聽器的 ID。
      removeListenerOnDispose.guid = listener.guid;

      // 也為目標的處置事件添加一個偵聽器。這確保了
      // 如果目標在此對象之前被釋放,我們將刪除
      // 剛剛添加的刪除偵聽器。否則,我們會造成內存洩漏。
      const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);

      // 使用與偵聽器相同的函數 ID,以便稍後刪除它
      // 它使用原始偵聽器的 ID。
      removeRemoverOnTargetDispose.guid = listener.guid;

      listen(this, 'on', 'dispose', removeListenerOnDispose);
      listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
    }
  },

  /**
   * 添加一個偵聽器到這個對像或另一個事件的一個或多個事件
   * 目的。每個事件將調用一次偵聽器,然後將其刪除。
   *
   * @param {string|Array|Element|Object} targetOrType
   * 如果這是一個字符串或數組,它表示事件類型
   * 這將觸發偵聽器。
   *
   * 可以在這里傳遞另一個事件對象,它將
   * 使偵聽器偵聽_that_ 對像上的事件。
   *
   * 在任何一種情況下,偵聽器的 `this` 值都將綁定到
   * 這個對象。
   *
   * @param {string|Array|Function} typeOrListener
   * 如果第一個參數是字符串或數組,這應該是
   * 監聽函數。否則,這是一個字符串或事件數組
   *類型。
   *
   * @param {函數} [監聽器]
   * 如果第一個參數是另一個事件對象,這將是
   * 監聽函數。
   */
  一個(...參數){
    const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'one');

    // 以這個事件對象為目標。
    如果(isTargetingSelf){
      listen(target, 'one', type, listener);

    // 定位另一個事件對象。
    }其他{
      // 去做:這個包裝不正確!它應該只
      // 移除調用它的事件類型的包裝器。
      // 相反,所有偵聽器都在第一次觸發時被刪除!
      // 參見 https://github.com/videojs/video.js/issues/5962
      const wrapper = (...largs) => {
        this.off(目標,類型,包裝器);
        listener.apply(null, largs);
      };

      // 使用與偵聽器相同的函數 ID,以便稍後刪除它
      // 它使用原始偵聽器的 ID。
      wrapper.guid = listener.guid;
      聽(目標,'一個',類型,包裝);
    }
  },

  /**
   * 添加一個偵聽器到這個對像或另一個事件的一個或多個事件
   * 目的。對於觸發的第一個事件,偵聽器只會被調用一次
   * 然後刪除。
   *
   * @param {string|Array|Element|Object} targetOrType
   * 如果這是一個字符串或數組,它表示事件類型
   * 這將觸發偵聽器。
   *
   * 可以在這里傳遞另一個事件對象,它將
   * 使偵聽器偵聽_that_ 對像上的事件。
   *
   * 在任何一種情況下,偵聽器的 `this` 值都將綁定到
   * 這個對象。
   *
   * @param {string|Array|Function} typeOrListener
   * 如果第一個參數是字符串或數組,這應該是
   * 監聽函數。否則,這是一個字符串或事件數組
   *類型。
   *
   * @param {函數} [監聽器]
   * 如果第一個參數是另一個事件對象,這將是
   * 監聽函數。
   */
  任何(...參數){
    const {isTargetingSelf, target, type, listener} = normalizeListenArgs(this, args, 'any');

    // 以這個事件對象為目標。
    如果(isTargetingSelf){
      listen(target, 'any', type, listener);

    // 定位另一個事件對象。
    }其他{
      const wrapper = (...largs) => {
        this.off(目標,類型,包裝器);
        listener.apply(null, largs);
      };

      // 使用與偵聽器相同的函數 ID,以便稍後刪除它
      // 它使用原始偵聽器的 ID。
      wrapper.guid = listener.guid;
      聽(目標,'任何',類型,包裝);
    }
  },

  /**
   * 從事件對象的事件中移除監聽器。
   *
   * @param {string|Array|Element|Object} [targetOrType]
   * 如果這是一個字符串或數組,它表示事件類型。
   *
   * 可以在這里傳遞另一個事件對象,在這種情況下
   * 所有 3 個參數都是_required_。
   *
   * @param {字符串|數組|函數} [typeOrListener]
   * 如果第一個參數是字符串或數組,這可能是
   * 監聽函數。否則,這是一個字符串或事件數組
   *類型。
   *
   * @param {函數} [監聽器]
   * 如果第一個參數是另一個事件對象,這將是
   * 監聽函數;否則,_all_ 聽眾綁定到
   * 事件類型將被刪除。
   */
  off(targetOrType, typeOrListener, listener) {

    // 以這個事件對象為目標。
    如果(!targetOrType || isValidEventType(targetOrType)){
      Events.off(this.eventBusEl_, targetOrType, typeOrListener);

    // 定位另一個事件對象。
    }其他{
      const target = targetOrType;
      const type = typeOrListener;

      // 以一種有意義的方式快速失敗!
      validateTarget(目標,這個,'關閉');
      validateEventType(類型,這個,'關閉');
      validateListener(監聽器,這個,'關閉');

      // 確保至少有一個 guid,即使函數沒有被使用
      listener = Fn.bind(this, listener);

      // 刪除給定的事件對像上的處置偵聽器
      // 與 on() 中的事件偵聽器相同的 guid。
      this.off('dispose', listener);

      如果(target.nodeName){
        Events.off(目標,類型,監聽器);
        Events.off(target, 'dispose', listener);
      } else if (isEvented(target)) {
        target.off(類型,監聽器);
        target.off('處置', 偵聽器);
      }
    }
  },

  /**
   * 在這個事件對像上觸發一個事件,導致它的監聽器被調用。
   *
   * @param {string|Object} 事件
   * 事件類型或具有類型屬性的對象。
   *
   * @param {對象} [哈希]
   * 傳遞給聽眾的附加對象。
   *
   * @return {布爾值}
   * 是否阻止了默認行為。
   */
  觸發器(事件,散列){
    validateTarget(this.eventBusEl_, this, 'trigger');

    const type = event && typeof event !== 'string' ?事件類型:事件;

    如果(!isValidEventType(類型)){
      const error = `${objName(this)}#trigger 的無效事件類型; ` +
        '必須是非空字符串或對象,其類型鍵具有非空值。';

      如果(事件){
        (this.log || log).error(錯誤);
      }其他{
        拋出新的錯誤(錯誤);
      }
    }
    返回 Events.trigger(this.eventBusEl_, event, hash);
  }
};

/**
 * 將 {@link module:evented~EventedMixin|EventedMixin} 應用於目標對象。
 *
 * @param {Object} 目標
 * 添加事件方法的對象。
 *
 * @param {對象} [選項={}]
 * 自定義混合行為的選項。
 *
 * @param {string} [options.eventBusKey]
 * 默認情況下,向目標對象添加一個 `eventBusEl_` DOM 元素,
 * 用作事件總線。如果目標對像已經有一個
 * 應該使用的 DOM 元素,在這里傳遞它的鍵。
 *
 * @return {對象}
 * 目標對象。
 */
函數事件(目標,選項={}){
  const {eventBusKey} = 選項;

  // 設置或創建 eventBusEl_。
  如果(事件總線鍵){
    如果 (!target[eventBusKey].nodeName) {
      throw new Error(`The eventBusKey "${eventBusKey}" 沒有引用元素。`);
    }
    target.eventBusEl_ = target[eventBusKey];
  }其他{
    target.eventBusEl_ = Dom.createEl('span', {className: 'vjs-event-bus'});
  }

  Obj.assign(target, EventedMixin);

  如果(target.eventedCallbacks){
    target.eventedCallbacks.forEach((回調) => {
      打回來();
    });
  }

  // 當處理任何事件對象時,它會刪除所有的偵聽器。
  target.on('處置', () => {
    target.off();
    [target, target.el_, target.eventBusEl_].forEach(function(val) {
      如果(val && DomData.has(val)){
        DomData.delete(val);
      }
    });
    window.setTimeout(() => {
      target.eventBusEl_ = null;
    }, 0);
  });

  返回目標;
}

導出默認事件;
出口{isEvented};
出口{addEventedCallback};