/**
 * @file dom.js
 * @module dom
 */
從“全局/文檔”導入文檔;
從“全局/窗口”導入窗口;
從 '../fullscreen-api' 導入 fs;
從 './log.js' 導入日誌;
從 './obj' 導入 {isObject};
從 './computed-style' 導入 computedStyle;
import * as browser from './browser';

/**
 * 檢測值是否為包含任何非空白字符的字符串。
 *
 * @私人的
 * @param {string} 海峽
 * 要檢查的字符串
 *
 * @return {布爾值}
 * 如果字符串為非空,則為“true”,否則為“false”。
 *
 */
函數 isNonBlankString(str) {
  // 我們使用 str.trim 因為它會修剪任何空白字符
  // 從非空白字符的前面或後面。又名
  // 任何包含非空白字符的字符串都會
  // 在 `trim` 之後仍然包含它們,但只有空格字符串
  // 長度為 0,檢查失敗。
  返回類型 str === 'string' && Boolean(str.trim());
}

/**
 * 如果傳遞的字符串有空格則拋出錯誤。這是由
 * 類方法與 classList API 相對一致。
 *
 * @私人的
 * @param {string} 海峽
 * 要檢查空格的字符串。
 *
 * @throws {錯誤}
 * 如果字符串中有空格則拋出錯誤。
 */
函數 throwIfWhitespace(str) {
  // str.indexOf 而不是正則表達式,因為 str.indexOf 性能更快。
  如果 (str.indexOf(' ') >= 0) {
    throw new Error('類有非法空白字符');
  }
}

/**
 * 生成用於匹配元素類名中的類名的正則表達式。
 *
 * @私人的
 * @param {string} 類名
 * 為其生成 RegExp 的類名。
 *
 * @return {正則表達式}
 * 將檢查元素中特定“className”的 RegExp
 * 班級名稱。
 */
功能類RegExp(類名){
  return new RegExp('(^|\\\\s)' + className + '($|\\\\s)');
}

/**
 * 當前的 DOM 界面是否看起來是真實的(即不是模擬的)。
 *
 * @return {布爾值}
 * 如果 DOM 看起來是真實的,則為“true”,否則為“false”。
 */
導出函數 isReal() {
  // 由於 `global`,文檔和窗口永遠不會未定義。
  返回文件 === window.document;
}

/**
 * 通過 duck typing 確定一個值是否是 DOM 元素。
 *
 * @param {混合} 值
 * 要檢查的值。
 *
 * @return {布爾值}
 * 如果值是 DOM 元素,則為“true”,否則為“false”。
 */
導出函數 isEl(值) {
  返回 isObject(value) && value.nodeType === 1;
}

/**
 * 確定當前 DOM 是否嵌入到 iframe 中。
 *
 * @return {布爾值}
 * 如果 DOM 嵌入到 iframe 中則為 true,否則為 false
 * 否則。
 */
導出函數 isInFrame() {

  // 我們在這裡需要一個 try/catch,因為 Safari 會在嘗試時拋出錯誤
  // 獲取 `parent` 或 `self`
  嘗試{
    返回 window.parent !== window.self;
  } 抓住(x){
    返回真;
  }
}

/**
 * 創建使用給定方法查詢 DOM 的函數。
 *
 * @私人的
 * @param {string} 方法
 * 創建查詢的方法。
 *
 * @return {函數}
 * 查詢方法
 */
函數 createQuerier(方法){
  返回函數(選擇器,上下文){
    如果(!isNonBlankString(選擇器)){
      返回文件[方法](空);
    }
    如果(isNonBlankString(上下文)){
      context = document.querySelector(context);
    }

    const ctx = isEl(上下文) ?上下文:文檔;

    返回 ctx[方法] && ctx[方法](選擇器);
  };
}

/**
 * 創建一個元素並應用屬性、屬性並插入內容。
 *
 * @param {string} [tagName='div']
 * 要創建的標籤名稱。
 *
 * @param {對象} [屬性={}]
 * 要應用的元素屬性。
 *
 * @param {對象} [屬性={}]
 * 要應用的元素屬性。
 *
 * @param {module:dom~ContentDescriptor} 內容
 * 內容描述符對象。
 *
 * @return {元素}
 * 創建的元素。
 */
導出函數 createEl(tagName = 'div', properties = {}, attributes = {}, content) {
  const el = document.createElement(tagName);

  Object.getOwnPropertyNames(properties).forEach(函數(propName) {
    const val = 屬性[propName];

    // 參見#2176
    // 我們最初在
    // 相同的對象,但效果不是很好。
    if (propName.indexOf('aria-') !== -1 || propName === '角色' || propName === '類型') {
      log.warn('在createEl()的第二個參數中設置屬性\\n' +
               '已被棄用。請改用第三個參數。\\n' +
               `createEl(類型,屬性,屬性)。正在嘗試將 ${propName} 設置為 ${val}。`);
      el.setAttribute(propName, val);

    // 處理 textContent,因為它不是到處都支持,我們有一個
    // 它的方法。
    } else if (propName === 'textContent') {
      文本內容(el,val);
    } else if (el[propName] !== val || propName === 'tabIndex') {
      el[propName] = val;
    }
  });

  Object.getOwnPropertyNames(屬性).forEach(函數(屬性名) {
    el.setAttribute(attrName, attributes[attrName]);
  });

  如果(內容){
    appendContent(el, 內容);
  }

  返回 el;
}

/**
 * 將文本注入元素,完全替換任何現有內容。
 *
 * @param {元素} el
 * 添加文本內容的元素
 *
 * @param {string} 文本
 * 要添加的文本內容。
 *
 * @return {元素}
 * 添加文本內容的元素。
 */
導出函數 textContent(el, text) {
  if (typeof el.textContent === 'undefined') {
    el.innerText = 文本;
  }其他{
    el.textContent = 文本;
  }
  返回 el;
}

/**
 * 插入一個元素作為另一個元素的第一個子節點
 *
 * @param {Element} 孩子
 * 要插入的元素
 *
 * @param {Element} 父母
 * 將孩子插入的元素
 */
導出函數 prependTo(child, parent) {
  如果(父母。第一個孩子){
    parent.insertBefore(child, parent.firstChild);
  }其他{
    parent.appendChild(孩子);
  }
}

/**
 * 檢查元素是否有類名。
 *
 * @param {元素} 元素
 * 要檢查的元素
 *
 * @param {string} classToCheck
 * 要檢查的類名
 *
 * @return {布爾值}
 * 如果元素有類,則為“true”,否則為“false”。
 *
 * @throws {錯誤}
 * 如果 `classToCheck` 有空格則拋出錯誤。
 */
導出函數 hasClass(element, classToCheck) {
  throwIfWhitespace(classToCheck);
  如果(元素。類列表){
    返回 element.classList.contains(classToCheck);
  }
  返回 classRegExp(classToCheck).test(element.className);
}

/**
 * 向元素添加類名。
 *
 * @param {元素} 元素
 * 要添加類名的元素。
 *
 * @param {string} classToAdd
 * 要添加的類名。
 *
 * @return {元素}
 * 添加了類名的 DOM 元素。
 */
導出函數 addClass(element, classToAdd) {
  如果(元素。類列表){
    element.classList.add(classToAdd);

  // 這裡不需要 `throwIfWhitespace` 因為 `hasElClass` 會做
  // 在不支持 classList 的情況下。
  } else if (!hasClass(element, classToAdd)) {
    element.className = (element.className + ' ' + classToAdd).trim();
  }

  返回元素;
}

/**
 * 從元素中刪除類名。
 *
 * @param {元素} 元素
 * 要從中刪除類名的元素。
 *
 * @param {string} classToRemove
 * 要刪除的類名
 *
 * @return {元素}
 * 類名被移除的 DOM 元素。
 */
導出函數 removeClass(element, classToRemove) {
  // 保護以防玩家被處置
  如果(!元素){
    log.warn("使用不存在的元素調用了 removeClass");
    返回空值;
  }
  如果(元素。類列表){
    element.classList.remove(classToRemove);
  }其他{
    throwIfWhitespace(classToRemove);
    element.className = element.className.split(/\\s+/).filter(函數(c) {
      返回 c !== classToRemove;
    })。加入(' ');
  }

  返回元素;
}

/**
 * toggleClass 的回調定義。
 *
 * @callback 模塊:dom~PredicateCallback
 * @param {元素} 元素
 * 組件的 DOM 元素。
 *
 * @param {string} classToToggle
 * 想要切換的 `className`
 *
 * @return {布爾值|未定義}
 * 如果返回 `true`,則將 `classToToggle` 添加到
 *`元素`。如果為 `false`,則 `classToToggle` 將從
 * `元素`。如果 `undefined`,回調將被忽略。
 */

/**
 * 根據可選的元素向/從元素添加或刪除類名
 * 條件或類名的存在/不存在。
 *
 * @param {元素} 元素
 * 用於切換類名的元素。
 *
 * @param {string} classToToggle
 * 應該切換的類。
 *
 * @param {boolean|module:dom~PredicateCallback} [謂詞]
 * 查看 {@link module:dom~PredicateCallback} 的返回值
 *
 * @return {元素}
 * 具有已切換類的元素。
 */
導出函數 toggleClass(element, classToToggle, predicate) {

  // 這不能在內部使用 `classList` 因為 IE11 不支持
  // `classList.toggle()` 方法的第二個參數!這很好,因為
  // `classList` 將被添加/刪除函數使用。
  const has = hasClass(元素, classToToggle);

  if (typeof predicate === 'function') {
    謂詞=謂詞(元素,classToToggle);
  }

  if (typeof predicate !== 'boolean') {
    謂詞=!有;
  }

  // 如果必要的類操作匹配當前狀態
  // 元素,不需要任何操作。
  如果(謂詞 === 有){
    返回;
  }

  如果(謂詞){
    添加類(元素,classToToggle);
  }其他{
    removeClass(元素,classToToggle);
  }

  返回元素;
}

/**
 * 將屬性應用於 HTML 元素。
 *
 * @param {元素} el
 * 要添加屬性的元素。
 *
 * @param {對象} [屬性]
 * 要應用的屬性。
 */
導出函數 setAttributes(el, attributes) {
  Object.getOwnPropertyNames(屬性).forEach(函數(屬性名) {
    const attrValue = attributes[attrName];

    如果(attrValue === null || typeof attrValue === 'undefined' || attrValue === false){
      el.removeAttribute(屬性名);
    }其他{
      el.setAttribute(attrName, (attrValue === true ? '' : attrValue));
    }
  });
}

/**
 * 獲取元素的屬性值,如 HTML 標記中所定義。
 *
 * 屬性與特性不同。它們在標籤上定義
 * 或使用 setAttribute。
 *
 * @param {元素} 標籤
 * 從中獲取標籤屬性的元素。
 *
 * @return {對象}
 * 元素的所有屬性。布爾屬性將為“真”或
 * `false`,其他將是字符串。
 */
導出函數 getAttributes(標籤){
  常量對象 = {};

  // 已知的布爾屬性
  // 我們可以檢查匹配的布爾屬性,但不是所有瀏覽器
  // 並不是所有的標籤都知道這些屬性,所以,我們仍然想手動檢查它們
  const knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ',';

  如果(標記 && 標記。屬性 && 標記。屬性。長度 > 0){
    const attrs = tag.attributes;

    for (let i = attrs.length - 1; i >= 0; i--) {
      const attrName = attrs[i].name;
      讓 attrVal = attrs[i].value;

      // 檢查已知的布爾值
      // 匹配的元素屬性將返回一個 typeof 的值
      if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) {
        // 包含的布爾屬性的值通常為空
        // string ('') 如果我們只檢查 false 值,它將等於 false。
        // 我們也不希望支持像 autoplay='false' 這樣的錯誤代碼
        attrVal = (attrVal !== null) ?真假;
      }

      obj[屬性名] = 屬性值;
    }
  }

  返回對象;
}

/**
 * 獲取元素屬性的值。
 *
 * @param {元素} el
 * 一個 DOM 元素。
 *
 * @param {string} 屬性
 * 獲取值的屬性。
 *
 * @return {字符串}
 * 屬性值。
 */
導出函數 getAttribute(el, attribute) {
  返回 el.getAttribute(屬性);
}

/**
 * 設置元素屬性的值。
 *
 * @param {元素} el
 * 一個 DOM 元素。
 *
 * @param {string} 屬性
 * 要設置的屬性。
 *
 * @param {string} 值
 * 將屬性設置為的值。
 */
導出函數 setAttribute(el, attribute, value) {
  el.setAttribute(屬性, 值);
}

/**
 * 刪除元素的屬性。
 *
 * @param {元素} el
 * 一個 DOM 元素。
 *
 * @param {string} 屬性
 * 要刪除的屬性。
 */
導出函數 removeAttribute(el, attribute) {
  el.removeAttribute(屬性);
}

/**
 * 嘗試阻止選擇文本的能力。
 */
導出函數 blockTextSelection() {
  文檔.body.focus();
  document.onselectstart = function() {
    返回假;
  };
}

/**
 * 關閉文本選擇阻止。
 */
導出函數 unblockTextSelection() {
  document.onselectstart = function() {
    返回真;
  };
}

/**
 * 與原生的 getBoundingClientRect 函數相同,但確保
 * 完全支持該方法(在我們聲稱支持的所有瀏覽器中)
 * 並且該元素在繼續之前位於 DOM 中。
 *
 * 這個包裝函數還填充了一些沒有提供的屬性
 * 舊版瀏覽器(即 IE8)。
 *
 * 此外,某些瀏覽器不支持將屬性添加到
 * `ClientRect`/`DOMRect` 對象;所以,我們用標準淺拷貝它
 * 屬性(不廣泛支持的 `x` 和 `y` 除外)。這有助於
 * 避免鍵不可枚舉的實現。
 *
 * @param {元素} el
 * 我們要計算其 ClientRect 的元素。
 *
 * @return {對象|未定義}
 * 總是返回一個普通對象——如果不能返回則為 `undefined`。
 */
導出函數 getBoundingClientRect(el) {
  如果 (el && el.getBoundingClientRect && el.parentNode) {
    const rect = el.getBoundingClientRect();
    常量結果 = {};

    ['底部', '高度', '左', '右', '頂部', '寬度'].forEach(k => {
      如果(矩形[k]!==未定義){
        結果[k] = 矩形[k];
      }
    });

    如果(!結果。高度){
      result.height = parseFloat(computedStyle(el, 'height'));
    }

    如果(!結果。寬度){
      result.width = parseFloat(computedStyle(el, 'width'));
    }

    返回結果;
  }
}

/**
 * 代表一個DOM元素在頁面上的位置。
 *
 * @typedef {Object} 模塊:dom~位置
 *
 * @property {number} 離開
 * 左邊的像素。
 *
 * @property {number} 頂部
 * 從頂部開始的像素。
 */

/**
 * 獲取元素在 DOM 中的位置。
 *
 * 使用 John Resig 的“getBoundingClientRect”技術。
 *
 * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
 *
 * @param {元素} el
 * 要偏移的元素。
 *
 * @return {模塊:dom~位置}
 * 傳入的元素的位置。
 */
導出函數 findPosition(el) {
  如果 (!el || (el && !el.offsetParent)) {
    返回 {
      左邊:0,
      頂部:0,
      寬度:0,
      高度: 0
    };
  }
  const width = el.offsetWidth;
  const height = el.offsetHeight;
  讓左= 0;
  讓頂部= 0;

  while (el.offsetParent && el !== document[fs.fullscreenElement]) {
    左 += el.offsetLeft;
    top += el.offsetTop;

    el = el.offsetParent;
  }

  返回 {
    左邊,
    頂部,
    寬度,
    高度
  };
}

/**
 * 表示 DOM 元素或鼠標指針的 x 和 y 坐標。
 *
 * @typedef {Object} 模塊:dom~坐標
 *
 * @property {number} x
 * x 像素坐標
 *
 * @property {number} y
 * 以像素為單位的 y 坐標
 */

/**
 * 獲取元素內的指針位置。
 *
 * 坐標的基礎是元素的左下角。
 *
 * @param {元素} el
 * 獲取指針位置的元素。
 *
 * @param {EventTarget~Event} 事件
 * 事件對象。
 *
 * @return {模塊:dom~坐標}
 * 鼠標位置對應的坐標對象。
 *
 */
導出函數 getPointerPosition(el, event) {
  const 翻譯 = {
    X:0,
    是: 0
  };

  如果(瀏覽器.IS_IOS){
    讓項目= el;

    while (item && item.nodeName.toLowerCase() !== 'html') {
      const transform = computedStyle(item, 'transform');

      如果 (/^matrix/.test(transform)) {
        常量值 = transform.slice(7, -1).split(/,\\s/).map(Number);

        translated.x += 值[4];
        translated.y += 值[5];
      } else if (/^matrix3d/.test(transform)) {
        常量值 = transform.slice(9, -1).split(/,\\s/).map(Number);

        translated.x += 值[12];
        translated.y += 值[13];
      }

      item = item.parentNode;
    }
  }

  常量位置 = {};
  const boxTarget = findPosition(event.target);
  const box = findPosition(el);
  const boxW = box.width;
  const boxH = box.height;
  讓 offsetY = event.offsetY - (box.top - boxTarget.top);
  讓 offsetX = event.offsetX - (box.left - boxTarget.left);

  如果 (event.changedTouches) {
    offsetX = event.changedTouches[0].pageX - box.left;
    offsetY = event.changedTouches[0].pageY + box.top;
    如果(瀏覽器.IS_IOS){
      offsetX -= translated.x;
      offsetY -= translated.y;
    }
  }

  position.y = (1 - Math.max(0, Math.min(1, offsetY / boxH)));
  position.x = Math.max(0, Math.min(1, offsetX / boxW));
  返回位置;
}

/**
 * 通過 duck typing 確定值是否為文本節點。
 *
 * @param {混合} 值
 * 檢查此值是否為文本節點。
 *
 * @return {布爾值}
 * 如果值為文本節點,則為“true”,否則為“false”。
 */
導出函數 isTextNode(value) {
  返回 isObject(value) && value.nodeType === 3;
}

/**
 * 清空元素的內容。
 *
 * @param {元素} el
 * 清空子元素的元素
 *
 * @return {元素}
 * 沒有孩子的元素
 */
導出函數 emptyEl(el) {
  而(el.firstChild){
    el.removeChild(el.firstChild);
  }
  返回 el;
}

/**
 * 這是一個混合值,描述要注入到 DOM 中的內容
 * 通過某種方法。它可以是以下類型:
 *
 * 類型 |描述
 * ----------|------------
 *`字符串`|該值將被規範化為文本節點。
 * `元素` |該值將按原樣接受。
 * `文本節點` |該值將按原樣接受。
 * `數組` |字符串、元素、文本節點或函數的一維數組。這些函數應該返回一個字符串、元素或文本節點(任何其他返回值,如數組,將被忽略)。
 *`功能`|一個函數,它應該返回一個字符串、元素、文本節點或數組——上述任何其他可能的值。這意味著內容描述符可以是返回函數數組的函數,但那些二級函數必須返回字符串、元素或文本節點。
 *
 * @typedef {string|Element|TextNode|Array|Function} 模塊:dom~ContentDescriptor
 */

/**
 * 規範化內容以最終插入到 DOM 中。
 *
 * 這允許廣泛的內容定義方法,但有助於保護
 * 避免陷入簡單地寫入 `innerHTML` 的陷阱,這可能
 * 成為 XSS 問題。
 *
 * 元素的內容可以以多種類型傳遞,並且
 * 組合,其行為如下:
 *
 * @param {module:dom~ContentDescriptor} 內容
 * 內容描述符值。
 *
 * @return {數組}
 * 所有傳入的內容,規範化為一個數組
 * 元素或文本節點。
 */
導出函數 normalizeContent(內容){

  // 首先,如果內容是一個函數,則調用它。如果它產生一個數組,
  // 這需要在規範化之前發生。
  if (typeof content === 'function') {
    內容=內容();
  }

  // 接下來,規範化為數組,因此可以規範化一個或多個項目,
  //過濾,並返回。
  返回 (Array.isArray(content) ? content : [content]).map(value => {

    // 首先,調用值,如果它是一個產生新值的函數,
    // 隨後將被規範化為某種節點。
    如果(類型值==='函數'){
      價值=價值();
    }

    如果(isEl(值)|| isTextNode(值)){
      返回值;
    }

    if (typeof value === 'string' && (/\\S/).test(value)) {
      返回 document.createTextNode(value);
    }
  }).filter(值=>值);
}

/**
 * 規範化內容並將其附加到元素。
 *
 * @param {元素} el
 * 將規範化內容附加到的元素。
 *
 * @param {module:dom~ContentDescriptor} 內容
 * 內容描述符值。
 *
 * @return {元素}
 * 附加規範化內容的元素。
 */
導出函數 appendContent(el, content) {
  normalizeContent(content).forEach(node => el.appendChild(node));
  返回 el;
}

/**
 * 規範化內容並將其插入到元素中;這與
 * `appendContent()`,除了它首先清空元素。
 *
 * @param {元素} el
 * 插入規範化內容的元素。
 *
 * @param {module:dom~ContentDescriptor} 內容
 * 內容描述符值。
 *
 * @return {元素}
 * 插入規範化內容的元素。
 */
導出函數 insertContent(el, content) {
  返回 appendContent(emptyEl(el),內容);
}

/**
 * 檢查事件是否為單擊左鍵。
 *
 * @param {EventTarget~Event} 事件
 * 事件對象。
 *
 * @return {布爾值}
 * 如果單擊左鍵則為“true”,否則為“false”。
 */
導出函數 isSingleLeftClick(event) {
  // 注意:如果你創建了可拖動的東西,一定要
  // 在 `mousedown` 和 `mousemove` 事件上調用它,
  // 否則 `mousedown` 應該足夠一個按鈕

  if (event.button === undefined && event.buttons === undefined) {
    // 為什麼我們需要 `buttons`?
    // 因為,鼠標中鍵有時會有這個:
    // e.button === 0 和 e.buttons === 4
    // 此外,我們要防止組合點擊,比如
    // HOLD middlemouse 然後左鍵單擊,這將是
    // e.button === 0, e.buttons === 5
    // 只有 `button` 是行不通的

    // 好吧,那麼這個塊是做什麼的?
    // 這是為 chrome `模擬移動設備`
    // 我也想支持這個

    返回真;
  }

  如果 (event.button === 0 && event.buttons === undefined) {
    // 觸摸屏,有時在某些特定設備上,`buttons`
    // 沒有任何東西(ios 上的 safari,黑莓......)

    返回真;
  }

  // 一次左鍵單擊的 `mouseup` 事件有
  // `button` 和 `buttons` 等於 0
  if (event.type === 'mouseup' && event.button === 0 &&
      事件.buttons === 0) {
    返回真;
  }

  如果(事件。按鈕!== 0 || 事件。按鈕!== 1){
    // 這就是我們上面有 if else 塊的原因
    // 如果有任何特殊情況我們可以捕捉並讓它滑動
    // 我們上面做的,到這裡的時候,肯定是這個
    // 不是左鍵單擊

    返回假;
  }

  返回真;
}

/**
 * 在可選的範圍內查找與 `selector` 匹配的單個 DOM 元素
 * 另一個 DOM 元素的“上下文”(默認為“文檔”)。
 *
 * @param {string} 選擇器
 * 一個有效的 CSS 選擇器,將傳遞給 querySelector。
 *
 * @param {元素|字符串} [上下文=文檔]
 * 在其中查詢的 DOM 元素。也可以是選擇器
 * string 在這種情況下將使用第一個匹配元素
 * 作為上下文。如果缺失(或沒有元素匹配選擇器),則落下
 * 回到“文檔”。
 *
 * @return {元素|null}
 * 找到的元素或為空。
 */
導出常量 $ = createQuerier('querySelector');

/**
 * 在可選範圍內查找所有匹配 `selector` 的 DOM 元素
 * 另一個 DOM 元素的“上下文”(默認為“文檔”)。
 *
 * @param {string} 選擇器
 * 一個有效的 CSS 選擇器,將被傳遞給 querySelectorAll 。
 *
 * @param {元素|字符串} [上下文=文檔]
 * 在其中查詢的 DOM 元素。也可以是選擇器
 * string 在這種情況下將使用第一個匹配元素
 * 作為上下文。如果缺失(或沒有元素匹配選擇器),則落下
 * 回到“文檔”。
 *
 * @return {節點列表}
 * 找到的元素的元素列表。如果沒有則為空
 * 被找到。
 *
 */
導出常量 $$ = createQuerier('querySelectorAll');