/**
* @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');