/**
 * @file events.js。事件系統(John Resig - JS 忍者的秘密 http://jsninja.com/)
 *(原書版本不能完全使用,所以修復了一些東西並使 Closure Compiler 兼容)
 * 這應該與 jQuery 的事件非常相似,但是它是基於書本版本的,它不像
 * 像 jquery 一樣健壯,因此可能存在一些差異。
 *
 * @file events.js
 * @模塊事件
 */
從 './dom-data' 導入 DomData;
從 './guid.js' 導入 * 作為 Guid;
從 './log.js' 導入日誌;
從“全局/窗口”導入窗口;
從“全局/文檔”導入文檔;

/**
 * 清理監聽器緩存和調度器
 *
 * @param {元素|對象}元素
 * 要清理的元素
 *
 * @param {string} 類型
 * 要清理的事件類型
 */
函數_cleanUpEvents(元素,類型){
  如果 (!DomData.has(elem)) {
    返回;
  }
  const data = DomData.get(elem);

  // 如果沒有剩餘,則刪除特定類型的事件
  如果 (data.handlers[type].length === 0) {
    刪除數據處理程序[類型];
    // data.handlers[type] = null;
    // 設置為 null 會導致 data.handlers 出錯

    // 從元素中移除元處理程序
    如果(elem.removeEventListener){
      elem.removeEventListener(type, data.dispatcher, false);
    } else if (elem.detachEvent) {
      elem.detachEvent('on' + type, data.dispatcher);
    }
  }

  // 如果沒有剩餘類型,則移除事件對象
  如果 (Object.getOwnPropertyNames(data.handlers).length <= 0) {
    刪除數據處理程序;
    刪除 data.dispatcher;
    刪除數據。禁用;
  }

  // 如果沒有剩餘數據,最後移除元素數據
  如果 (Object.getOwnPropertyNames(data).length === 0) {
    DomData.delete(elem);
  }
}

/**
 * 遍歷事件類型數組並為每種類型調用請求的方法。
 *
 * @param {函數} fn
 * 我們要使用的事件方法。
 *
 * @param {元素|對象}元素
 * 綁定監聽器的元素或對象
 *
 * @param {string} 類型
 * 要綁定的事件類型。
 *
 * @param {EventTarget~EventListener} 回調
 * 事件監聽器。
 */
函數 _handleMultipleEvents(fn、elem、類型、回調){
  types.forEach(函數(類型){
    // 為每種類型調用事件方法
    fn(元素,類型,回調);
  });
}

/**
 * 修復本機事件以具有標準屬性值
 *
 * @param {Object} 事件
 * 要修復的事件對象。
 *
 * @return {對象}
 *固定事件對象。
 */
導出函數 fixEvent(事件){
  如果(事件。固定_){
    返回事件;
  }

  函數 returnTrue() {
    返回真;
  }

  函數 returnFalse() {
    返回假;
  }

  // 測試是否需要修復
  // 用於檢查 !event.stopPropagation 而不是 isPropagationStopped
  // 但原生事件為 stopPropagation 返回 true,但沒有
  // 其他預期的方法,如 isPropagationStopped。好像有問題
  // 使用 Javascript Ninja 代碼。所以我們現在只是覆蓋所有事件。
  如果 (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
    const 舊 = 事件 ||窗口事件;

    事件={};
    // 克隆舊對象,以便我們可以修改值 event = {};
    // IE8 不喜歡你亂用本機事件屬性
    // Firefox 為 event.hasOwnProperty('type') 和其他屬性返回 false
    // 這使得複制更加困難。
    // 去做:可能最好創建一個事件道具白名單
    對於(舊的常量鍵){
      // Safari 6.0.3 會在您嘗試複製已棄用的 layerX/Y 時發出警告
      // 如果您嘗試複製已棄用的 keyboardEvent.keyLocation,Chrome 會警告您
      // 和 webkitMovementX/Y
      // 如果 Event.path 被複製,Lighthouse 會報錯
      if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' &&
          key !== 'webkitMovementX' && key !== 'webkitMovementY' &&
          鍵!=='路徑'){
        // 如果您嘗試複製已棄用的 returnValue,Chrome 32+ 會發出警告,但是
        // 如果不支持 preventDefault (IE8),我們仍然想要。
        如果 (!(key === 'returnValue' && old.preventDefault)) {
          事件[鍵] = 舊[鍵];
        }
      }
    }

    // 事件發生在這個元素上
    如果(!event.target){
      event.target = event.srcElement ||文檔;
    }

    // 處理事件與哪個其他元素相關
    如果(!event.relatedTarget){
      event.relatedTarget = event.fromElement === event.target ?
        事件.toElement:
        事件.fromElement;
    }

    // 停止默認的瀏覽器操作
    event.preventDefault = function() {
      如果(舊的.preventDefault){
        舊的.preventDefault();
      }
      event.returnValue = false;
      old.returnValue = false;
      event.defaultPrevented = true;
    };

    event.defaultPrevented = false;

    // 阻止事件冒泡
    event.stopPropagation = function() {
      如果(舊的。停止傳播){
        舊的停止傳播();
      }
      event.cancelBubble = true;
      舊的.cancelBubble = true;
      event.isPropagationStopped = returnTrue;
    };

    event.isPropagationStopped = returnFalse;

    // 阻止事件冒泡並執行其他處理程序
    event.stopImmediatePropagation = function() {
      如果(舊的。stopImmediatePropagation){
        old.stopImmediatePropagation();
      }
      event.isImmediatePropagationStopped = returnTrue;
      事件.stopPropagation();
    };

    event.isImmediatePropagationStopped = returnFalse;

    // 處理鼠標位置
    如果(event.clientX !== null && event.clientX !== undefined){
      const doc = document.documentElement;
      const body = 文檔.body;

      event.pageX = event.clientX +
        (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
        (doc && doc.clientLeft || body && body.clientLeft || 0);
      event.pageY = event.clientY +
        (doc && doc.scrollTop || body && body.scrollTop || 0) -
        (doc && doc.clientTop || body && body.clientTop || 0);
    }

    // 處理按鍵
    event.which = event.charCode ||事件.keyCode;

    // 修復鼠標點擊的按鈕:
    // 0 == 左; 1 == 中間; 2 == 對
    如果(事件。按鈕!==空&&事件。按鈕!==未定義){

      // 下面是禁用的,因為它沒有通過videojs-standard
      // 和...哎呀。
      /* eslint 禁用 */
      event.button = (event.button & 1 ?0 :
        (事件按鈕 & 4 ?1 :
          (事件按鈕 & 2 ?2 :0)));
      /* eslint 啟用 */
    }
  }

  event.fixed_ = true;
  // 返回固定實例
  返回事件;
}

/**
 * 是否支持被動事件監聽器
 */
讓_supportsPassive;

const supportsPassive = function() {
  如果(typeof _supportsPassive !== 'boolean'){
    _supportsPassive = false;
    嘗試{
      const opts = Object.defineProperty({}, '被動', {
        得到() {
          _supportsPassive = 真;
        }
      });

      window.addEventListener('test', null, opts);
      window.removeEventListener('test', null, opts);
    } 抓住 (e) {
      // 無視
    }
  }

  返回_supportsPassive;
};

/**
 * Chrome 期望觸摸事件是被動的
 */
const passiveEvents = [
  '觸摸啟動',
  '觸摸移動'
];

/**
 * 給元素添加一個事件監聽器
 * 它將處理函數存儲在一個單獨的緩存對像中
 * 並向元素的事件添加通用處理程序,
 * 以及元素的唯一 ID (guid)。
 *
 * @param {元素|對象}元素
 * 綁定監聽器的元素或對象
 *
 * @param {string|string[]} 類型
 * 要綁定的事件類型。
 *
 * @param {EventTarget~EventListener} fn
 * 事件監聽器。
 */
導出函數 on(elem, type, fn) {
  如果(Array.isArray(類型)){
    返回 _handleMultipleEvents(打開,元素,類型,fn);
  }

  如果 (!DomData.has(elem)) {
    DomData.set(elem, {});
  }

  const data = DomData.get(elem);

  // 我們需要一個地方來存儲我們所有的處理程序數據
  如果(!data.handlers){
    data.handlers = {};
  }

  如果(!數據處理程序[類型]){
    data.handlers[類型] = [];
  }

  如果(!fn.guid){
    fn.guid = Guid.newGUID();
  }

  data.handlers[type].push(fn);

  如果(!data.dispatcher){
    數據.disabled = false;

    data.dispatcher = function(event, hash) {

      如果(數據。禁用){
        返回;
      }

      事件 = fixEvent(事件);

      const handlers = data.handlers[event.type];

      如果(處理程序){
        // 複製處理程序,因此如果在此過程中添加/刪除處理程序,它不會丟棄所有內容。
        const handlersCopy = handlers.slice(0);

        對於(設 m = 0,n = handlersCopy.length;m < n;m++){
          如果(事件。isImmediatePropagationStopped()){
            休息;
          }其他{
            嘗試{
              handlersCopy[m].call(elem, event, hash);
            } 抓住 (e) {
              日誌.錯誤(e);
            }
          }
        }
      }
    };
  }

  如果 (data.handlers[type].length === 1) {
    如果(elem.addEventListener){
      讓選項=假;

      如果(支持被動()&&
        passiveEvents.indexOf(類型)> -1){
        選項={被動:真};
      }
      elem.addEventListener(類型,data.dispatcher,選項);
    } else if (elem.attachEvent) {
      elem.attachEvent('on' + type, data.dispatcher);
    }
  }
}

/**
 * 從元素中刪除事件監聽器
 *
 * @param {元素|對象}元素
 * 從中刪除偵聽器的對象。
 *
 * @param {字符串|字符串[]} [類型]
 * 要刪除的偵聽器類型。不包括從元素中刪除所有事件。
 *
 * @param {EventTarget~EventListener} [fn]
 * 要刪除的特定偵聽器。不包含以刪除事件的偵聽器
 * 類型。
 */
導出函數 off(elem, type, fn) {
  // 如果不需要,不想通過getElData添加緩存對象
  如果 (!DomData.has(elem)) {
    返回;
  }

  const data = DomData.get(elem);

  // 如果不存在任何事件,則無需解除綁定
  如果(!data.handlers){
    返回;
  }

  如果(Array.isArray(類型)){
    返回 _handleMultipleEvents(關閉,elem,類型,fn);
  }

  // 實用功能
  const removeType = function(el, t) {
    data.handlers[t] = [];
    _cleanUpEvents(el, t);
  };

  // 我們要刪除所有綁定事件嗎?
  如果(類型===未定義){
    for (const t in data.handlers) {
      如果 (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
        移除類型(元素,t);
      }
    }
    返回;
  }

  const handlers = data.handlers[類型];

  // 如果不存在處理程序,則無需解除綁定
  如果(!處理程序){
    返回;
  }

  // 如果沒有提供監聽器,移除所有類型的監聽器
  如果(!fn){
    刪除類型(元素,類型);
    返回;
  }

  // 我們只刪除一個處理程序
  如果(fn.guid){
    for (let n = 0; n < handlers.length; n++) {
      如果(處理程序[n].guid === fn.guid){
        handlers.splice(n--, 1);
      }
    }
  }

  _cleanUpEvents(元素,類型);
}

/**
 * 為元素觸發事件
 *
 * @param {元素|對象}元素
 * 觸發事件的元素
 *
 * @param {EventTarget~Event|string} 事件
 * 字符串(類型)或具有類型屬性的事件對象
 *
 * @param {對象} [哈希]
 * 隨事件一起傳遞的數據散列
 *
 * @return {布爾值|未定義}
 * 如果默認是,則返回 `defaultPrevented` 的反義詞
 * 阻止。否則,返回 `undefined`
 */
導出函數觸發器(元素,事件,散列){
  // 獲取元素數據和對父級的引用(用於冒泡)。
  // 不想為每個父級添加一個數據對象來緩存,
  // 所以先檢查 hasElData。
  const elemData = DomData.has(elem) ?DomData.get(元素) : {};
  const parent = elem.parentNode || elem.ownerDocument;
  // type = event.type ||事件,
  //處理程序;

  // 如果事件名稱作為字符串傳遞,則從中創建一個事件
  如果(事件類型==='字符串'){
    事件 = {類型:事件,目標:元素};
  } else if (!event.target) {
    event.target = elem;
  }

  // 規範化事件屬性。
  事件 = fixEvent(事件);

  // 如果傳遞的元素有調度程序,則執行已建立的處理程序。
  如果(elemData.dispatcher){
    elemData.dispatcher.call(elem, event, hash);
  }

  // 除非明確停止或事件不冒泡(例如媒體事件)
  // 遞歸調用此函數以將事件冒泡到 DOM 中。
  如果(父母&&!event.isPropagationStopped()&& event.bubbles === true){
    trigger.call(null, parent, event, hash);

  // 如果在 DOM 的頂部,除非禁用,否則觸發默認操作。
  } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
    如果 (!DomData.has(event.target)) {
      DomData.set(event.target, {});
    }
    const targetData = DomData.get(event.target);

    // 檢查目標是否有針對此事件的默認操作。
    如果(事件目標[事件類型]){
      // 暫時禁用目標上的事件調度,因為我們已經執行了處理程序。
      targetData.disabled = true;
      // 執行默認操作。
      如果 (typeof event.target[event.type] === '函數') {
        事件目標[事件類型]();
      }
      // 重新啟用事件調度。
      targetData.disabled = false;
    }
  }

  // 如果通過返回 false 來阻止默認設置,則通知觸發器
  返回!event.defaultPrevented;
}

/**
 * 一個事件只觸發一次監聽器。
 *
 * @param {元素|對象}元素
 * 要綁定到的元素或對象。
 *
 * @param {string|string[]} 類型
 * 活動名稱/類型
 *
 * @param {Event~EventListener} fn
 * 事件監聽函數
 */
導出函數 one(elem, type, fn) {
  如果(Array.isArray(類型)){
    返回 _handleMultipleEvents(一個,元素,類型,fn);
  }
  const func = function() {
    關閉(元素,類型,功能);
    fn.apply(這個,參數);
  };

  // 將 guid 複製到新函數,以便使用原始函數的 ID 刪除它
  func.guid = fn.guid = fn.guid || Guid.newGUID();
  on(elem, type, func);
}

/**
 * 只觸發一個監聽器一次然後關閉
 * 配置的事件
 *
 * @param {元素|對象}元素
 * 要綁定到的元素或對象。
 *
 * @param {string|string[]} 類型
 * 活動名稱/類型
 *
 * @param {Event~EventListener} fn
 * 事件監聽函數
 */
導出函數 any(elem, type, fn) {
  const func = function() {
    關閉(元素,類型,功能);
    fn.apply(這個,參數);
  };

  // 將 guid 複製到新函數,以便使用原始函數的 ID 刪除它
  func.guid = fn.guid = fn.guid || Guid.newGUID();

  // 多次開啟,但一次關閉
  on(elem, type, func);
}