/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/core/dom/index.js
576 строк
16 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {devAssert} from '#core/assert'; import * as mode from '#core/mode'; import {parseJson} from '#core/types/object/json'; import {getWin} from '#core/window'; import {childElementsByTag, matches} from './query'; const HTML_ESCAPE_CHARS = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`', }; const HTML_ESCAPE_REGEX = /(&|<|>|"|'|`)/g; /** * @typedef {{ * bubbles: (boolean|undefined), * cancelable: (boolean|undefined), * }} CustomEventOptionsDef; */ /** @const {CustomEventOptionsDef} */ const DEFAULT_CUSTOM_EVENT_OPTIONS = {bubbles: true, cancelable: true}; /** * Waits until the child element is constructed. Once the child is found, the * callback is executed. * @param {Element} parent * @param {function(Element):boolean} checkFunc * @param {function():void} callback */ export function waitForChild(parent, checkFunc, callback) { if (checkFunc(parent)) { callback(); return; } const win = getWin(parent); if (mode.isEsm() || win.MutationObserver) { const observer = new win.MutationObserver(() => { if (checkFunc(parent)) { observer.disconnect(); callback(); } }); observer.observe(parent, {childList: true}); } else { const interval = win.setInterval(() => { if (checkFunc(parent)) { win.clearInterval(interval); callback(); } }, /* milliseconds */ 5); } } /** * Waits until the child element is constructed. Once the child is found, the * promise is resolved. * @param {Element} parent * @param {function(Element):boolean} checkFunc * @return {Promise<void>} */ export function waitForChildPromise(parent, checkFunc) { return new Promise((resolve) => { waitForChild(parent, checkFunc, /** @type {function():void} */ (resolve)); }); } /** * Waits for document's body to be available and ready. * @param {Document} doc * @param {function():void} callback */ export function waitForBodyOpen(doc, callback) { waitForChild(doc.documentElement, () => !!doc.body, callback); } /** * Waits for document's body to be available. * @param {Document} doc * @return {Promise<void>} */ export function waitForBodyOpenPromise(doc) { return new Promise((resolve) => waitForBodyOpen(doc, /** @type {function():void} */ (resolve)) ); } /** * Removes the element. * @param {Element} element */ export function removeElement(element) { element.parentElement?.removeChild(element); } /** * Removes all child nodes of the specified element. * @param {Element|DocumentFragment} parent */ export function removeChildren(parent) { while (parent.firstChild) { parent.removeChild(parent.firstChild); } } /** * Copies all children nodes of element "from" to element "to". Child nodes * are deeply cloned. Notice, that this method should be used with care and * preferably on smaller subtrees. * @param {Element} from * @param {Element|DocumentFragment} to */ export function copyChildren(from, to) { const frag = to.ownerDocument.createDocumentFragment(); for (let n = from.firstChild; n; n = n.nextSibling) { frag.appendChild(n.cloneNode(true)); } to.appendChild(frag); } /** * Insert the element in the root after the element named after or * if that is null at the beginning. * @param {Element|ShadowRoot} root * @param {Element} element * @param {?Node=} after */ export function insertAfterOrAtStart(root, element, after = null) { if (!after) { insertAtStart(root, element); return; } const before = after.nextSibling; root.insertBefore(element, before); } /** * Insert the element in the root after the element named after or * if that is null at the beginning. * @param {Element|ShadowRoot} root * @param {Element} element */ export function insertAtStart(root, element) { root.insertBefore(element, root.firstChild); } /** * Add attributes to an element. * @param {Element} element * @param {Record<string, string>} attributes * @return {Element} created element */ export function addAttributesToElement(element, attributes) { for (const attr in attributes) { element.setAttribute(attr, attributes[attr]); } return element; } /** * Create a new element on document with specified tagName and attributes. * @param {Document} doc * @param {string} tagName * @param {Record<string, string>} attributes * @return {Element} created element */ export function createElementWithAttributes(doc, tagName, attributes) { const element = doc.createElement(tagName); return addAttributesToElement(element, attributes); } /** * Returns true if node is connected (attached). * @param {Node} node * @return {boolean} * @see https://dom.spec.whatwg.org/#connected */ export function isConnectedNode(node) { const connected = node.isConnected; if (connected !== undefined) { return connected; } // "An element is connected if its shadow-including root is a document." let n = node; do { n = rootNodeFor(n); if (/** @type {ShadowRoot} */ (n).host) { n = /** @type {ShadowRoot} */ (n).host; } else { break; } } while (true); return n.nodeType === Node.DOCUMENT_NODE; } /** * Returns the root for a given node. Does not cross shadow DOM boundary. * @param {Node} node * @return {ShadowRoot|Document} */ export function rootNodeFor(node) { // https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode // @ts-ignore this always is truthy according to TS, but isn't in IE 11. if (Node.prototype.getRootNode) { // Type checker says `getRootNode` may return null. return /** @type {ShadowRoot|Document} */ (node.getRootNode()); } let n; // Check isShadowRoot() is only needed for the polyfill case. for ( n = node; !!n.parentNode && !isShadowRoot(/** @type {HTMLElement} */ (n)); n = n.parentNode ) {} return /** @type {ShadowRoot|Document} */ (n); } /** * Determines if value is actually a `ShadowRoot` node. * @param {?HTMLElement} value * @return {boolean} */ export function isShadowRoot(value) { if (!value) { return false; } // Node.nodeType == DOCUMENT_FRAGMENT to speed up the tests. Unfortunately, // nodeType of DOCUMENT_FRAGMENT is used currently for ShadowRoot nodes. if (value.tagName == 'I-AMPHTML-SHADOW-ROOT') { return true; } return ( value.nodeType == /* DOCUMENT_FRAGMENT */ 11 && Object.prototype.toString.call(value) === '[object ShadowRoot]' ); } /** * Returns element data-param- attributes as url parameters key-value pairs. * e.g. data-param-some-attr=value -> {someAttr: value}. * @param {HTMLElement} element * @param {function(string):string=} opt_computeParamNameFunc to compute the * parameter name, get passed the camel-case parameter name. * @param {RegExp=} opt_paramPattern Regex pattern to match data attributes. * @return {JsonObject} */ export function getDataParamsFromAttributes( element, opt_computeParamNameFunc, opt_paramPattern ) { const computeParamNameFunc = opt_computeParamNameFunc || ((key) => key); const {dataset} = element; const params = /** @type {!JsonObject} */ ({}); const paramPattern = opt_paramPattern || /^param(.+)/; for (const key in dataset) { const matches = key.match(paramPattern); if (matches) { const param = matches[1][0].toLowerCase() + matches[1].substr(1); params[computeParamNameFunc(param)] = dataset[key]; } } return params; } /** * Whether the element have a next node in the document order. * This means either: * a. The element itself has a nextSibling. * b. Any of the element ancestors has a nextSibling. * @param {Element} element * @param {?Node} opt_stopNode * @return {boolean} */ export function hasNextNodeInDocumentOrder(element, opt_stopNode) { /** @type {?Element} */ let currentElement = element; do { if (currentElement.nextSibling) { return true; } } while ( (currentElement = /** @type {?Element} */ (currentElement.parentNode)) && currentElement != opt_stopNode ); return false; } /** * Returns a clone of the content of a template element. * * Polyfill to replace .content access for browsers that do not support * HTMLTemplateElements natively. * * @param {HTMLTemplateElement|Element} template * @return {DocumentFragment} */ export function templateContentClone(template) { if ('content' in template) { return /** @type {DocumentFragment} */ (template.content.cloneNode(true)); } else { const content = template.ownerDocument.createDocumentFragment(); copyChildren(template, content); return content; } } /** * Iterate over an array-like. * Test cases: https://jsbench.github.io/#f638cacc866a1b2d6e517e6cfa900d6b * @param {ArrayLike<T>} iterable * @param {function(T, number):void} cb * @template T */ export function iterateCursor(iterable, cb) { const {length} = iterable; for (let i = 0; i < length; i++) { cb(iterable[i], i); } } /** * Whether the element is a script tag with application/json type. * @param {Element} element * @return {boolean} */ export function isJsonScriptTag(element) { return ( element.tagName == 'SCRIPT' && element.getAttribute('type')?.toUpperCase() == 'APPLICATION/JSON' ); } /** * Whether the element is a script tag with application/json type. * @param {Element} element * @return {boolean} */ export function isJsonLdScriptTag(element) { return ( element.tagName == 'SCRIPT' && element.getAttribute('type')?.toUpperCase() == 'APPLICATION/LD+JSON' ); } /** * Whether the page's direction is right to left or not. * @param {Document} doc * @return {boolean} */ export function isRTL(doc) { const dir = doc.body.getAttribute('dir') || doc.documentElement.getAttribute('dir') || 'ltr'; return dir == 'rtl'; } /** * Escapes `<`, `>` and other HTML characters with their escaped forms. * @param {string} text * @return {string} */ export function escapeHtml(text) { if (!text) { return text; } return text.replace(HTML_ESCAPE_REGEX, escapeHtmlChar); } /** * @param {keyof HTML_ESCAPE_CHARS} c * @return {string} */ function escapeHtmlChar(c) { return HTML_ESCAPE_CHARS[c]; } /** * Tries to focus on the given element; fails silently if browser throws an * exception. * @param {HTMLElement} element */ export function tryFocus(element) { try { element./*OK*/ focus(); } catch (e) { // IE <= 7 may throw exceptions when focusing on hidden items. } } /** * Whether the given window is in an iframe or not. * @param {Window} win * @return {boolean} */ export function isIframed(win) { return win.parent && win.parent != win; } /** * Returns true if node is not disabled. * * IE8 can return false positives, see {@link matches}. * @param {HTMLInputElement} element * @return {boolean} * @see https://www.w3.org/TR/html5/forms.html#concept-fe-disabled */ export function isEnabled(element) { return !(element.disabled || matches(element, ':disabled')); } /** * A sorting comparator that sorts elements in DOM tree order. * A first sibling is sorted to be before its nextSibling. * A parent node is sorted to be before a child. * See https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition * * @param {Element} element1 * @param {Element} element2 * @return {number} */ export function domOrderComparator(element1, element2) { if (element1 === element2) { return 0; } const pos = element1.compareDocumentPosition(element2); const precedingOrContains = Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS; // if fe2 is preceding or contains fe1 then, fe1 is after fe2 if (pos & precedingOrContains) { return 1; } // if fe2 is following or contained by fe1, then fe1 is before fe2 return -1; } /** * Like `Element.prototype.toggleAttribute`. This either toggles an attribute * on by adding an attribute with an empty value, or toggles it off by removing * the attribute. This does not mutate the element if the new state matches * the existing state. * @param {Element} element An element to toggle the attribute for. * @param {string} name The name of the attribute. * @param {boolean=} forced Whether the attribute should be forced on/off. If * not specified, it will be toggled from the current state. * @return {boolean} Whether or not the element now has the attribute. */ export function toggleAttribute(element, name, forced) { const hasAttribute = element.hasAttribute(name); const enabled = forced !== undefined ? forced : !hasAttribute; if (enabled !== hasAttribute) { if (enabled) { element.setAttribute(name, ''); } else { element.removeAttribute(name); } } return enabled; } /** * Parses a string as a boolean value using the expanded rules for DOM boolean * attributes: * - a `null` or `undefined` returns `null`; * - an empty string returns `true`; * - a "false" string returns `false`; * - otherwise, `true` is returned. * * @param {?string|undefined} s * @return {boolean|undefined} */ export function parseBooleanAttribute(s) { return s == null ? undefined : s !== 'false'; } /** * @param {Window} win * @return {number} The width of the vertical scrollbar, in pixels. */ export function getVerticalScrollbarWidth(win) { const {documentElement} = win.document; const windowWidth = win./*OK*/ innerWidth; const documentWidth = documentElement./*OK*/ clientWidth; return windowWidth - documentWidth; } /** * Dispatches a custom event. * * @param {Node} node * @param {string} name * @param {Object=} opt_data Event data. * @param {CustomEventOptionsDef=} opt_options */ export function dispatchCustomEvent(node, name, opt_data, opt_options) { const data = opt_data || {}; // Constructors of events need to come from the correct window. Sigh. devAssert(node.ownerDocument); const {bubbles, cancelable} = opt_options || DEFAULT_CUSTOM_EVENT_OPTIONS; const event = new MessageEvent(name, {data, bubbles, cancelable}); node.dispatchEvent(event); } /** * Ensures the child is contained by the parent, but not the parent itself. * * @param {Node} parent * @param {Node} child * @return {boolean} */ export function containsNotSelf(parent, child) { return child !== parent && parent.contains(child); } /** * Helper method to get the json config from an element <script> tag * @param {HTMLElement} element * @return {?JsonObject} * @throws {Error} If element does not have exactly one <script> child * with type="application/json", or if the <script> contents are not valid JSON. */ export function getChildJsonConfig(element) { const scripts = childElementsByTag(element, 'script'); const {length} = scripts; if (length !== 1) { throw new Error(`Found ${length} <script> children. Expected 1.`); } const script = /** @type {HTMLScriptElement} */ (scripts[0]); if (!isJsonScriptTag(script)) { throw new Error('<script> child must have type="application/json"'); } try { return parseJson(script.textContent ?? ''); } catch { throw new Error('Failed to parse <script> contents. Is it valid JSON?'); } } /** * Returns true if an element was server rendered. * @param {Element} element * @return {boolean} */ export function isServerRendered(element) { return element.hasAttribute('i-amphtml-ssr'); } /** * Propagate the nonce found in <head> to a new script element. * Recent browsers force nonce to be accessed via property instead of attribute. * * @param {Document} doc * @param {HTMLScriptElement} scriptEl */ export function propagateNonce(doc, scriptEl) { /** @type {?HTMLScriptElement} */ const currentScript = doc.head.querySelector('script[nonce]'); if (currentScript) { const nonce = currentScript.nonce || currentScript.getAttribute('nonce'); scriptEl.setAttribute('nonce', nonce ?? ''); } }