/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
2604150425000
src/custom-element.js
2 241 строка
66 KB
renovate[bot]
📦 Update linting devDependencies (#39627)
17 ноя 2023, 21:37
Не верифицирован
17 ноя 2023, 21:37
97d264d
Код
Авторство
О чём код?
import {AmpEvents_Enum} from '#core/constants/amp-events'; import {CommonSignals_Enum} from '#core/constants/common-signals'; import {ReadyState_Enum} from '#core/constants/ready-state'; import {tryResolve} from '#core/data-structures/promise'; import {Signals} from '#core/data-structures/signals'; import * as dom from '#core/dom'; import { UPGRADE_TO_CUSTOMELEMENT_PROMISE, UPGRADE_TO_CUSTOMELEMENT_RESOLVER, } from '#core/dom/amp-element-helpers'; import { LayoutPriority_Enum, Layout_Enum, isLoadingAllowed, } from '#core/dom/layout'; import {MediaQueryProps} from '#core/dom/media-query-props'; import * as query from '#core/dom/query'; import {setStyle} from '#core/dom/style'; import {rethrowAsync} from '#core/error'; import {applyStaticLayout} from '#core/static-layout'; import {getWin} from '#core/window'; import {Services} from '#service'; import {ResourceState_Enum} from '#service/resource'; import {getSchedulerForDoc} from '#service/scheduler'; import {dev, devAssert, user, userAssert} from '#utils/log'; import {startupChunk} from './chunk'; import {shouldBlockOnConsentByMeta} from './consent'; import {ElementStub} from './element-stub'; import { blockedByConsentError, cancellation, isBlockedByConsent, isCancellation, reportError, } from './error-reporting'; import {getMode} from './mode'; import {getIntersectionChangeEntry} from './utils/intersection-observer-3p-host'; const TAG = 'CustomElement'; /** * @enum {number} */ const UpgradeState_Enum = { NOT_UPGRADED: 1, UPGRADED: 2, UPGRADE_FAILED: 3, UPGRADE_IN_PROGRESS: 4, }; const NO_BUBBLES = {bubbles: false}; const RETURN_TRUE = () => true; /** * Caches whether the template tag is supported to avoid memory allocations. * @type {boolean|undefined} */ let templateTagSupported; /** @type {!Array<AmpElement>} */ export const stubbedElements = []; /** * Extensions which have failed to load, making their elements unresolvable. * If null, then any remaining elements which don't immediately have their * implClass available are marked unresolvable. * @type {Set<string>} */ const unresolvableExtensions = new Set(); /** * Whether this platform supports template tags. * @return {boolean} */ function isTemplateTagSupported() { if (templateTagSupported === undefined) { const template = self.document.createElement('template'); templateTagSupported = 'content' in template; } return templateTagSupported; } /** * Creates a named custom element class. * * @param {!Window} win The window in which to register the custom element. * @param {function(!./service/ampdoc-impl.AmpDoc, !AmpElement, ?(typeof BaseElement))} elementConnectedCallback * @return {typeof AmpElement} The custom element class. */ export function createCustomElementClass(win, elementConnectedCallback) { const BaseCustomElement = /** @type {typeof HTMLElement} */ ( createBaseCustomElementClass(win, elementConnectedCallback) ); // It's necessary to create a subclass, because the same "base" class cannot // be registered to multiple custom elements. class CustomAmpElement extends BaseCustomElement { /** * adoptedCallback is only called when using a Native implementation of Custom Elements V1. * Our polyfill does not call this method. */ adoptedCallback() { // Work around an issue with Firefox changing the prototype of our // already constructed element to the new document's HTMLElement. if (Object.getPrototypeOf(this) !== customAmpElementProto) { Object.setPrototypeOf(this, customAmpElementProto); } } } const customAmpElementProto = CustomAmpElement.prototype; return /** @type {typeof AmpElement} */ (CustomAmpElement); } /** * Creates a base custom element class. * * @param {!Window} win The window in which to register the custom element. * @param {function(!./service/ampdoc-impl.AmpDoc, !AmpElement, ?(typeof BaseElement))} elementConnectedCallback * @return {typeof HTMLElement} */ function createBaseCustomElementClass(win, elementConnectedCallback) { if (win.__AMP_BASE_CE_CLASS) { return win.__AMP_BASE_CE_CLASS; } const htmlElement = /** @type {typeof HTMLElement} */ (win.HTMLElement); /** * @abstract @extends {HTMLElement} */ class BaseCustomElement extends htmlElement { /** */ constructor() { super(); this.createdCallback(); } /** * Called when elements is created. Sets instance vars since there is no * constructor. * @final */ createdCallback() { // Flag "notbuilt" is removed by Resource manager when the resource is // considered to be built. See "setBuilt" method. /** @private {boolean} */ this.built_ = false; /** * Several APIs require the element to be connected to the DOM tree, but * the CustomElement lifecycle APIs are async. This lead to subtle bugs * that require state tracking. See #12849, https://crbug.com/821195, and * https://bugs.webkit.org/show_bug.cgi?id=180940. * @private {boolean} */ this.isConnected_ = false; /** @private {?Promise} */ this.buildingPromise_ = null; /** * Indicates that the `mountCallback()` has been called and it hasn't * been reversed with an `unmountCallback()` call. * @private {boolean} */ this.mounted_ = false; /** @private {?Promise} */ this.mountPromise_ = null; /** @private {?AbortController} */ this.mountAbortController_ = null; /** @private {!ReadyState_Enum} */ this.readyState_ = ReadyState_Enum.UPGRADING; /** @type {boolean} */ this.everAttached = false; /** * Ampdoc can only be looked up when an element is attached. * @private {?./service/ampdoc-impl.AmpDoc} */ this.ampdoc_ = null; /** * Resources can only be looked up when an element is attached. * @private {?./service/resources-interface.ResourcesInterface} */ this.resources_ = null; /** @private {!Layout_Enum} */ this.layout_ = Layout_Enum.NODISPLAY; /** @private {number} */ this.layoutCount_ = 0; /** @private {boolean} */ this.isFirstLayoutCompleted_ = false; /** @public {boolean} */ this.warnOnMissingOverflow = true; /** * This element can be assigned by the {@link applyStaticLayout} to a * child element that will be used to size this element. * @package {?Element|undefined} */ this.sizerElement = undefined; /** @private {?Element|undefined} */ this.overflowElement_ = undefined; /** * The time at which this element was scheduled for layout relative to * the epoch. This value will be set to 0 until the this element has been * scheduled. * Note that this value may change over time if the element is enqueued, * then dequeued and re-enqueued by the scheduler. * @type {number|undefined} */ this.layoutScheduleTime = undefined; // Closure compiler appears to mark HTMLElement as @struct which // disables bracket access. Force this with a type coercion. const nonStructThis = /** @type {!Object} */ (this); // `opt_implementationClass` is only used for tests. /** @type {?(typeof ../base-element.BaseElement)} */ let Ctor = win.__AMP_EXTENDED_ELEMENTS && win.__AMP_EXTENDED_ELEMENTS[this.localName]; if (getMode().test && nonStructThis['implementationClassForTesting']) { Ctor = nonStructThis['implementationClassForTesting']; } /** @private {?(typeof ../base-element.BaseElement)} */ this.implClass_ = Ctor === ElementStub ? null : Ctor || null; if (!this.implClass_) { stubbedElements.push(this); } /** @private {?./base-element.BaseElement} */ this.impl_ = null; /** * An element always starts in a unupgraded state until it's added to DOM * for the first time in which case it can be upgraded immediately or wait * for script download or `upgradeCallback`. * @private {!UpgradeState_Enum} */ this.upgradeState_ = UpgradeState_Enum.NOT_UPGRADED; /** * Time delay imposed by baseElement upgradeCallback. If no * upgradeCallback specified or not yet executed, delay is 0. * @private {number} */ this.upgradeDelayMs_ = 0; /** * Action queue is initially created and kept around until the element * is ready to send actions directly to the implementation. * - undefined initially * - array if used * - null after unspun * @private {?Array<!./service/action-impl.ActionInvocation>|undefined} */ this.actionQueue_ = undefined; /** * Whether the element is in the template. * @private {boolean|undefined} */ this.isInTemplate_ = undefined; /** @private @const */ this.signals_ = new Signals(); if (this.implClass_) { this.signals_.signal(CommonSignals_Enum.READY_TO_UPGRADE); } const perf = Services.performanceForOrNull(win); /** @private {boolean} */ this.perfOn_ = perf && perf.isPerformanceTrackingOn(); /** @private {?MediaQueryProps} */ this.mediaQueryProps_ = null; if (nonStructThis[UPGRADE_TO_CUSTOMELEMENT_RESOLVER]) { nonStructThis[UPGRADE_TO_CUSTOMELEMENT_RESOLVER](nonStructThis); delete nonStructThis[UPGRADE_TO_CUSTOMELEMENT_RESOLVER]; delete nonStructThis[UPGRADE_TO_CUSTOMELEMENT_PROMISE]; } } /** @return {!ReadyState_Enum} */ get readyState() { return this.readyState_; } /** @return {!Signals} */ signals() { return this.signals_; } /** * Returns the associated ampdoc. Only available after attachment. It throws * exception before the element is attached. * @return {!./service/ampdoc-impl.AmpDoc} * @final * @package */ getAmpDoc() { devAssert(this.ampdoc_, 'no ampdoc yet, since element is not attached'); return /** @type {!./service/ampdoc-impl.AmpDoc} */ (this.ampdoc_); } /** * Returns Resources manager. Only available after attachment. It throws * exception before the element is attached. * @return {!./service/resources-interface.ResourcesInterface} * @final * @package */ getResources() { devAssert( this.resources_, 'no resources yet, since element is not attached' ); return /** @type {!./service/resources-interface.ResourcesInterface} */ ( this.resources_ ); } /** * Whether the element has been upgraded yet. Always returns false when * the element has not yet been added to DOM. After the element has been * added to DOM, the value depends on the `BaseElement` implementation and * its `upgradeElement` callback. * @return {boolean} * @final */ isUpgraded() { return this.upgradeState_ == UpgradeState_Enum.UPGRADED; } /** @return {!Promise} */ whenUpgraded() { return this.signals_.whenSignal(CommonSignals_Enum.UPGRADED); } /** * Upgrades the element to the provided new implementation. If element * has already been attached, it's layout validation and attachment flows * are repeated for the new implementation. * @param {typeof ./base-element.BaseElement} newImplClass * @final @package */ upgrade(newImplClass) { if (this.isInTemplate_) { return; } if (this.upgradeState_ != UpgradeState_Enum.NOT_UPGRADED) { // Already upgraded or in progress or failed. return; } this.implClass_ = newImplClass; this.signals_.signal(CommonSignals_Enum.READY_TO_UPGRADE); if (this.everAttached) { // Usually, we do an implementation upgrade when the element is // attached to the DOM. But, if it hadn't yet upgraded from // ElementStub, we couldn't. Now that it's upgraded from a stub, go // ahead and do the full upgrade. this.upgradeOrSchedule_(); } } /** * When the document is ready (meaning all external resources are loaded or * failed), we mark any stubbed elements as unresolved. If they haven't * been upgraded yet (or pending upgrade or deferredBuild elements), then * the extension failed to load. */ markUnresolved() { if (!this.implClass_) { this.classList.add('amp-unresolved', 'i-amphtml-unresolved'); } } /** * Time delay imposed by baseElement upgradeCallback. If no * upgradeCallback specified or not yet executed, delay is 0. * @return {number} */ getUpgradeDelayMs() { return this.upgradeDelayMs_; } /** * Completes the upgrade of the element with the provided implementation. * @param {!./base-element.BaseElement} newImpl * @param {number} upgradeStartTime * @final @private */ completeUpgrade_(newImpl, upgradeStartTime) { this.impl_ = newImpl; this.upgradeDelayMs_ = win.Date.now() - upgradeStartTime; this.upgradeState_ = UpgradeState_Enum.UPGRADED; this.setReadyStateInternal(ReadyState_Enum.BUILDING); this.classList.remove('amp-unresolved', 'i-amphtml-unresolved'); this.assertLayout_(); this.dispatchCustomEventForTesting(AmpEvents_Enum.ATTACHED); if (!this.R1()) { this.getResources().upgraded(this); } this.signals_.signal(CommonSignals_Enum.UPGRADED); } /** @private */ assertLayout_() { if ( this.layout_ != Layout_Enum.NODISPLAY && this.impl_ && !this.impl_.isLayoutSupported(this.layout_) ) { userAssert( this.getAttribute('layout'), 'The element did not specify a layout attribute. ' + 'Check https://amp.dev/documentation/guides-and-tutorials/' + 'develop/style_and_layout/control_layout and the respective ' + 'element documentation for details.' ); userAssert(false, `Layout not supported: ${this.layout_}`); } } /** * Get the priority to build the element. * @return {number} */ getBuildPriority() { return this.implClass_ ? this.implClass_.getBuildPriority(this) : LayoutPriority_Enum.BACKGROUND; } /** * Get the priority to load the element. * @return {number} * TODO(#31915): remove once R1 migration is complete. */ getLayoutPriority() { return this.impl_ ? this.impl_.getLayoutPriority() : LayoutPriority_Enum.BACKGROUND; } /** * Get the default action alias. * @return {?string} */ getDefaultActionAlias() { devAssert( this.isUpgraded(), 'Cannot get default action alias of unupgraded element' ); return this.impl_.getDefaultActionAlias(); } /** @return {boolean} */ isBuilding() { return !!this.buildingPromise_; } /** * Whether the element has been built. A built element had its * {@link buildCallback} method successfully invoked. * @return {boolean} * @final */ isBuilt() { return this.built_; } /** * Returns the promise that's resolved when the element has been built. If * the build fails, the resulting promise is rejected. * @return {!Promise} */ whenBuilt() { return this.signals_.whenSignal(CommonSignals_Enum.BUILT); } /** * Requests or requires the element to be built. The build is done by * invoking {@link BaseElement.buildCallback} method. * * Can only be called on a upgraded element. May only be called from * resource.js to ensure an element and its resource are in sync. * * @return {?Promise} * @final * @restricted */ buildInternal() { assertNotTemplate(this); devAssert(this.implClass_, 'Cannot build unupgraded element'); if (this.buildingPromise_) { return this.buildingPromise_; } this.setReadyStateInternal(ReadyState_Enum.BUILDING); // Create the instance. const implPromise = this.createImpl_(); this.getSizer_(); // Wait for consent. const consentPromise = implPromise.then(() => { const policyId = this.getConsentPolicy_(); const purposeConsents = !policyId ? this.getPurposesConsent_() : null; if (!policyId && !purposeConsents) { return; } // Must have policyId or granularExp w/ purposeConsents return Services.consentPolicyServiceForDocOrNull(this) .then((policy) => { if (!policy) { return true; } return policyId ? policy.whenPolicyUnblock(policyId) : policy.whenPurposesUnblock(purposeConsents); }) .then((shouldUnblock) => { if (!shouldUnblock) { throw blockedByConsentError(); } }); }); // Build callback. const buildPromise = consentPromise.then(() => devAssert(this.impl_).buildCallback() ); // Build the element. return (this.buildingPromise_ = buildPromise.then( () => { this.built_ = true; this.classList.add('i-amphtml-built'); this.classList.remove('i-amphtml-notbuilt', 'amp-notbuilt'); this.signals_.signal(CommonSignals_Enum.BUILT); if (this.R1()) { this.setReadyStateInternal( this.readyState_ != ReadyState_Enum.BUILDING ? this.readyState_ : ReadyState_Enum.MOUNTING ); } else { this.setReadyStateInternal(ReadyState_Enum.LOADING); this.preconnect(/* onLayout */ false); } if (this.isConnected_) { this.connected_(); } if (this.actionQueue_) { // Only schedule when the queue is not empty, which should be // the case 99% of the time. Services.timerFor(getWin(this)).delay( this.dequeueActions_.bind(this), 1 ); } if (!this.getPlaceholder()) { const placeholder = this.createPlaceholder(); if (placeholder) { this.appendChild(placeholder); } } }, (reason) => { this.signals_.rejectSignal( CommonSignals_Enum.BUILT, /** @type {!Error} */ (reason) ); if (this.R1()) { this.setReadyStateInternal(ReadyState_Enum.ERROR, reason); } if (!isBlockedByConsent(reason)) { reportError(reason, this); } throw reason; } )); } /** * @return {!Promise} */ build() { if (this.buildingPromise_) { return this.buildingPromise_; } const readyPromise = this.signals_.whenSignal( CommonSignals_Enum.READY_TO_UPGRADE ); return readyPromise.then(() => { if (this.R1()) { const scheduler = getSchedulerForDoc(this.getAmpDoc()); scheduler.scheduleAsap(this); } return this.whenBuilt(); }); } /** * Mounts the element by calling the `BaseElement.mountCallback` method. * * Can only be called on a upgraded element. May only be called from * scheduler.js. * * @return {!Promise} * @final * @restricted */ mountInternal() { if (this.mountPromise_) { return this.mountPromise_; } this.mountAbortController_ = this.mountAbortController_ || new AbortController(); const {signal} = this.mountAbortController_; return (this.mountPromise_ = this.buildInternal() .then(() => { devAssert(this.R1()); if (signal.aborted) { // Mounting has been canceled. return; } this.setReadyStateInternal( this.readyState_ != ReadyState_Enum.MOUNTING ? this.readyState_ : this.implClass_.usesLoading(this) ? ReadyState_Enum.LOADING : ReadyState_Enum.MOUNTING ); this.mounted_ = true; const result = this.impl_.mountCallback(signal); // The promise supports the V0 format for easy migration. If the // `mountCallback` returns a promise, the assumption is that the // element has finished loading when the promise completes. return result ? result.then(RETURN_TRUE) : false; }) .then((hasLoaded) => { this.mountAbortController_ = null; if (signal.aborted) { throw cancellation(); } this.signals_.signal(CommonSignals_Enum.MOUNTED); if (!this.implClass_.usesLoading(this) || hasLoaded) { this.setReadyStateInternal(ReadyState_Enum.COMPLETE); } }) .catch((reason) => { this.mountAbortController_ = null; if (isCancellation(reason)) { this.mountPromise_ = null; } else { this.signals_.rejectSignal( CommonSignals_Enum.MOUNTED, /** @type {!Error} */ (reason) ); this.setReadyStateInternal(ReadyState_Enum.ERROR, reason); } throw reason; })); } /** * Requests the element to be mounted as soon as possible. * @return {!Promise} * @final */ mount() { if (this.mountPromise_) { return this.mountPromise_; } // Create the abort controller right away to ensure that we the unmount // will properly cancel this operation. this.mountAbortController_ = this.mountAbortController_ || new AbortController(); const {signal} = this.mountAbortController_; const readyPromise = this.signals_.whenSignal( CommonSignals_Enum.READY_TO_UPGRADE ); return readyPromise.then(() => { if (!this.R1()) { return this.whenBuilt(); } if (signal.aborted) { throw cancellation(); } const scheduler = getSchedulerForDoc(this.getAmpDoc()); scheduler.scheduleAsap(this); return this.whenMounted(); }); } /** * Unmounts the element and makes it ready for the next mounting * operation. * @final */ unmount() { // Ensure that the element is paused. if (this.isConnected_) { this.pause(); } // Legacy R0 elements simply unlayout. if (!this.R1()) { this.unlayout_(); return; } // Cancel the currently mounting operation. if (this.mountAbortController_) { this.mountAbortController_.abort(); this.mountAbortController_ = null; } // Unschedule a currently pending mount request. const scheduler = getSchedulerForDoc(this.getAmpDoc()); scheduler.unschedule(this); // Try to unmount if the element has been built already. if (this.mounted_) { this.impl_.unmountCallback(); } // Complete unmount and reset the state. this.mounted_ = false; this.mountPromise_ = null; this.reset_(); // Prepare for the next mount if the element is connected. if (this.isConnected_) { this.upgradeOrSchedule_(/* opt_disablePreload */ true); } } /** * Returns the promise that's resolved when the element has been mounted. If * the mount fails, the resulting promise is rejected. * @return {!Promise} */ whenMounted() { return this.signals_.whenSignal(CommonSignals_Enum.MOUNTED); } /** * @return {!Promise} * @final */ whenLoaded() { return this.signals_.whenSignal(CommonSignals_Enum.LOAD_END); } /** * Ensure that the element is eagerly loaded. * * @param {number=} opt_parentPriority * @return {!Promise} * @final */ ensureLoaded(opt_parentPriority) { return this.mount().then(() => { if (this.R1()) { if (this.implClass_.usesLoading(this)) { this.impl_.ensureLoaded(); } return this.whenLoaded(); } // Very ugly! The "built" signal must be resolved from the Resource // and not the element itself because the Resource has not correctly // set its state for the downstream to process it correctly. const resource = this.getResource_(); return resource.whenBuilt().then(() => { if (resource.getState() == ResourceState_Enum.LAYOUT_COMPLETE) { return; } if ( resource.getState() != ResourceState_Enum.LAYOUT_SCHEDULED || resource.isMeasureRequested() ) { resource.measure(); } if (!resource.isDisplayed()) { return; } this.getResources().scheduleLayoutOrPreload( resource, /* layout */ true, opt_parentPriority, /* forceOutsideViewport */ true ); return this.whenLoaded(); }); }); } /** * See `BaseElement.setAsContainer`. * * @param {!Element=} opt_scroller A child of the container that should be * monitored. Typically a scrollable element. * @restricted * @final */ setAsContainerInternal(opt_scroller) { const builder = getSchedulerForDoc(this.getAmpDoc()); builder.setContainer(this, opt_scroller); } /** * See `BaseElement.removeAsContainer`. * @restricted * @final */ removeAsContainerInternal() { const builder = getSchedulerForDoc(this.getAmpDoc()); builder.removeContainer(this); } /** * Update the internal ready state. * * @param {!ReadyState_Enum} state * @param {*=} opt_failure * @protected * @final */ setReadyStateInternal(state, opt_failure) { if (state === this.readyState_) { return; } this.readyState_ = state; if (!this.R1()) { return; } switch (state) { case ReadyState_Enum.LOADING: this.signals_.signal(CommonSignals_Enum.LOAD_START); this.signals_.reset(CommonSignals_Enum.UNLOAD); this.signals_.reset(CommonSignals_Enum.LOAD_END); this.classList.add('i-amphtml-layout'); // Potentially start the loading indicator. this.toggleLoading(true); this.dispatchCustomEventForTesting(AmpEvents_Enum.LOAD_START); return; case ReadyState_Enum.COMPLETE: // LOAD_START is set just in case. It won't be overwritten if // it had been set before. this.signals_.signal(CommonSignals_Enum.LOAD_START); this.signals_.signal(CommonSignals_Enum.LOAD_END); this.signals_.reset(CommonSignals_Enum.UNLOAD); this.classList.add('i-amphtml-layout'); this.toggleLoading(false); dom.dispatchCustomEvent(this, 'load', null, NO_BUBBLES); this.dispatchCustomEventForTesting(AmpEvents_Enum.LOAD_END); return; case ReadyState_Enum.ERROR: this.signals_.rejectSignal( CommonSignals_Enum.LOAD_END, /** @type {!Error} */ (opt_failure) ); this.toggleLoading(false); dom.dispatchCustomEvent(this, 'error', opt_failure, NO_BUBBLES); return; } } /** * Called to instruct the element to preconnect to hosts it uses during * layout. * @param {boolean} onLayout Whether this was called after a layout. * TODO(#31915): remove once R1 migration is complete. */ preconnect(onLayout) { devAssert(this.isUpgraded()); if (onLayout) { this.impl_.preconnectCallback(onLayout); } else { // If we do early preconnects we delay them a bit. This is kind of // an unfortunate trade off, but it seems faster, because the DOM // operations themselves are not free and might delay startupChunk(this.getAmpDoc(), () => { if (!this.ownerDocument || !this.ownerDocument.defaultView) { return; } this.impl_.preconnectCallback(onLayout); }); } } /** * See `BaseElement.R1()`. * * @return {boolean} * @final */ R1() { return this.implClass_ ? this.implClass_.R1() : false; } /** * See `BaseElement.deferredMount()`. * * @return {boolean} * @final */ deferredMount() { return this.implClass_ ? this.implClass_.deferredMount(this) : false; } /** * Whether the custom element declares that it has to be fixed. * @return {boolean} */ isAlwaysFixed() { return this.impl_ ? this.impl_.isAlwaysFixed() : false; } /** * Updates the layout box of the element. * Should only be called by Resources. * @param {!./layout-rect.LayoutRectDef} layoutBox * @param {boolean} sizeChanged */ updateLayoutBox(layoutBox, sizeChanged = false) { if (this.isBuilt()) { this.onMeasure(sizeChanged); } } /** * Calls onLayoutMeasure() on the BaseElement implementation. * Should only be called by Resources. */ onMeasure() { devAssert(this.isBuilt()); try { this.impl_.onLayoutMeasure(); } catch (e) { reportError(e, this); } } /** * @return {?Element} * @private */ getSizer_() { if ( this.sizerElement === undefined && (this.layout_ === Layout_Enum.RESPONSIVE || this.layout_ === Layout_Enum.INTRINSIC) ) { // Expect sizer to exist, just not yet discovered. this.sizerElement = this.querySelector('i-amphtml-sizer'); this.sizerElement?.setAttribute('slot', 'i-amphtml-svc'); } return this.sizerElement || null; } /** * @param {Element} sizer * @private */ resetSizer_(sizer) { if (this.layout_ === Layout_Enum.RESPONSIVE) { setStyle(sizer, 'paddingTop', '0'); return; } if (this.layout_ === Layout_Enum.INTRINSIC) { const intrinsicSizerImg = sizer.querySelector( '.i-amphtml-intrinsic-sizer' ); if (!intrinsicSizerImg) { return; } intrinsicSizerImg.setAttribute('src', ''); return; } } /** @private */ initMediaAttrs_() { const hasMediaAttrs = this.hasAttribute('media') || (this.hasAttribute('sizes') && !this.hasAttribute('disable-inline-width')) || this.hasAttribute('heights'); const hadMediaAttrs = !!this.mediaQueryProps_; const win = this.ownerDocument.defaultView; if (hasMediaAttrs != hadMediaAttrs && win) { if (hasMediaAttrs) { this.mediaQueryProps_ = new MediaQueryProps(win, () => this.applyMediaAttrs_() ); this.applyMediaAttrs_(); } else { this.disposeMediaAttrs_(); } } } /** @private */ disposeMediaAttrs_() { if (this.mediaQueryProps_) { this.mediaQueryProps_.dispose(); this.mediaQueryProps_ = null; } } /** @private */ applyMediaAttrs_() { const props = this.mediaQueryProps_; if (!props) { return; } props.start(); // Media query. const mediaAttr = this.getAttribute('media') || null; const matchesMedia = mediaAttr ? props.resolveMatchQuery(mediaAttr) : true; this.classList.toggle('i-amphtml-hidden-by-media-query', !matchesMedia); // Sizes. const sizesAttr = this.hasAttribute('disable-inline-width') ? null : this.getAttribute('sizes'); if (sizesAttr) { setStyle(this, 'width', props.resolveListQuery(sizesAttr)); } // Heights. const heightsAttr = this.layout_ === Layout_Enum.RESPONSIVE ? this.getAttribute('heights') : null; if (heightsAttr) { const sizer = this.getSizer_(); if (sizer) { setStyle(sizer, 'paddingTop', props.resolveListQuery(heightsAttr)); } } props.complete(); this.getResource_().requestMeasure(); } /** * Applies a size change to the element. * * This method is called by Resources and shouldn't be called by anyone * else. This method must always be called in the mutation context. * * @param {number|undefined} newHeight * @param {number|undefined} newWidth * @param {!./layout-rect.LayoutMarginsDef=} opt_newMargins * @final * @package */ applySize(newHeight, newWidth, opt_newMargins) { const sizer = this.getSizer_(); if (sizer) { // From the moment height is changed the element becomes fully // responsible for managing its height. Aspect ratio is no longer // preserved. this.sizerElement = null; this.resetSizer_(sizer); this.mutateOrInvoke_(() => { if (sizer) { dom.removeElement(sizer); } }); } if (newHeight !== undefined) { setStyle(this, 'height', newHeight, 'px'); } if (newWidth !== undefined) { setStyle(this, 'width', newWidth, 'px'); } if (opt_newMargins) { if (opt_newMargins.top != null) { setStyle(this, 'marginTop', opt_newMargins.top, 'px'); } if (opt_newMargins.right != null) { setStyle(this, 'marginRight', opt_newMargins.right, 'px'); } if (opt_newMargins.bottom != null) { setStyle(this, 'marginBottom', opt_newMargins.bottom, 'px'); } if (opt_newMargins.left != null) { setStyle(this, 'marginLeft', opt_newMargins.left, 'px'); } } if (this.isAwaitingSize_()) { this.sizeProvided_(); } dom.dispatchCustomEvent(this, AmpEvents_Enum.SIZE_CHANGED); } /** * Called when the element is first connected to the DOM. * * This callback is guarded by checks to see if the element is still * connected. Chrome and Safari can trigger connectedCallback even when * the node is disconnected. See #12849, https://crbug.com/821195, and * https://bugs.webkit.org/show_bug.cgi?id=180940. Thankfully, * connectedCallback will later be called when the disconnected root is * connected to the document tree. * * @final */ connectedCallback() { if (!isTemplateTagSupported() && this.isInTemplate_ === undefined) { this.isInTemplate_ = !!query.closestAncestorElementBySelector( this, 'template' ); } if (this.isInTemplate_) { return; } if (this.isConnected_ || !dom.isConnectedNode(this)) { return; } this.isConnected_ = true; if (!this.everAttached) { this.classList.add( 'i-amphtml-element', 'i-amphtml-notbuilt', 'amp-notbuilt' ); } if (!this.ampdoc_) { // Ampdoc can now be initialized. const win = getWin(this); const ampdocService = Services.ampdocServiceFor(win); const ampdoc = ampdocService.getAmpDoc(this); this.ampdoc_ = ampdoc; elementConnectedCallback(ampdoc, this, this.implClass_); } if (!this.resources_) { // Resources can now be initialized since the ampdoc is now available. this.resources_ = Services.resourcesForDoc(this.ampdoc_); } this.getResources().add(this); if (this.everAttached) { const reconstruct = this.reconstructWhenReparented(); if (reconstruct) { this.reset_(); } if (this.isUpgraded()) { if (reconstruct && !this.R1()) { this.getResources().upgraded(this); } this.connected_(); this.dispatchCustomEventForTesting(AmpEvents_Enum.ATTACHED); } if (this.implClass_ && this.R1()) { this.upgradeOrSchedule_(); } } else { this.everAttached = true; try { this.layout_ = applyStaticLayout(this); this.initMediaAttrs_(); } catch (e) { reportError(e, this); } if (this.implClass_) { this.upgradeOrSchedule_(); } else if ( unresolvableExtensions.has('*') || unresolvableExtensions.has(this.tagName.toLowerCase()) ) { this.markUnresolved(); } if (!this.isUpgraded()) { this.dispatchCustomEventForTesting(AmpEvents_Enum.STUBBED); } } this.toggleLoading(true); } /** * @return {boolean} * @private */ isAwaitingSize_() { return this.classList.contains('i-amphtml-layout-awaiting-size'); } /** * @private */ sizeProvided_() { this.classList.remove('i-amphtml-layout-awaiting-size'); } /** * Upgrade or schedule element based on R1. * @param {boolean=} opt_disablePreload * @private @final */ upgradeOrSchedule_(opt_disablePreload) { if (!this.R1()) { this.tryUpgrade_(); return; } if (this.mountPromise_) { // Already mounting. return; } // Schedule build and mount. const scheduler = getSchedulerForDoc(this.getAmpDoc()); scheduler.schedule(this); this.classList.remove('amp-unresolved', 'i-amphtml-unresolved'); if (this.buildingPromise_) { // Already built or building: just needs to be mounted. this.setReadyStateInternal( this.implClass_ && this.implClass_.usesLoading(this) ? ReadyState_Enum.LOADING : ReadyState_Enum.MOUNTING ); } else { // Not built yet: execute prebuild steps. this.setReadyStateInternal(ReadyState_Enum.BUILDING); // Schedule preconnects. if (!opt_disablePreload) { const urls = this.implClass_.getPreconnects(this); if (urls && urls.length > 0) { // If we do early preconnects we delay them a bit. This is kind of // an unfortunate trade off, but it seems faster, because the DOM // operations themselves are not free and might delay const ampdoc = this.getAmpDoc(); startupChunk(ampdoc, () => { const {win} = ampdoc; if (!win) { return; } const preconnect = Services.preconnectFor(win); urls.forEach((url) => preconnect.url(ampdoc, url, /* alsoConnecting */ false) ); }); } } } } /** * Try to upgrade the element with the provided implementation. * @return {!Promise|undefined} * @private @final */ tryUpgrade_() { if (this.isInTemplate_) { return; } if (this.upgradeState_ != UpgradeState_Enum.NOT_UPGRADED) { // Already upgraded or in progress or failed. return; } const Ctor = devAssert( this.implClass_, 'Implementation must not be a stub' ); const impl = new Ctor(this); // The `upgradeCallback` only allows redirect once for the top-level // non-stub class. We may allow nested upgrades later, but they will // certainly be bad for performance. this.upgradeState_ = UpgradeState_Enum.UPGRADE_IN_PROGRESS; const startTime = win.Date.now(); const res = impl.upgradeCallback(); if (!res) { // Nothing returned: the current object is the upgraded version. this.completeUpgrade_(impl, startTime); } else if (typeof res.then == 'function') { // It's a promise: wait until it's done. return res .then((upgrade) => { this.completeUpgrade_(upgrade || impl, startTime); }) .catch((reason) => { this.upgradeState_ = UpgradeState_Enum.UPGRADE_FAILED; rethrowAsync(reason); }); } else { // It's an actual instance: upgrade immediately. this.completeUpgrade_( /** @type {!./base-element.BaseElement} */ (res), startTime ); } } /** * Called when the element is disconnected from the DOM. * * @final */ disconnectedCallback() { this.disconnect(/* pretendDisconnected */ false); } /** @private */ connected_() { if (this.built_) { this.impl_.attachedCallback(); } } /** * Called when an element is disconnected from DOM, or when an ampDoc is * being disconnected (the element itself may still be connected to ampDoc). * * This callback is guarded by checks to see if the element is still * connected. See #12849, https://crbug.com/821195, and * https://bugs.webkit.org/show_bug.cgi?id=180940. * If the element is still connected to the document, you'll need to pass * opt_pretendDisconnected. * * @param {boolean} pretendDisconnected Forces disconnection regardless * of DOM isConnected. */ disconnect(pretendDisconnected) { if (this.isInTemplate_ || !this.isConnected_) { return; } if (!pretendDisconnected && dom.isConnectedNode(this)) { return; } // This path only comes from Resource#disconnect, which deletes the // Resource instance tied to this element. Therefore, it is no longer // an AMP Element. But, DOM queries for i-amphtml-element assume that // the element is tied to a Resource. if (pretendDisconnected) { this.classList.remove('i-amphtml-element'); } this.isConnected_ = false; this.getResources().remove(this); if (this.impl_) { this.impl_.detachedCallback(); } if (this.R1()) { this.unmount(); } this.toggleLoading(false); this.disposeMediaAttrs_(); } /** * Dispatches a custom event only in testing environment. * * @param {string} name * @param {!Object=} opt_data Event data. * @final */ dispatchCustomEventForTesting(name, opt_data) { if (!getMode().test) { return; } dom.dispatchCustomEvent(this, name, opt_data); } /** * Whether the element can pre-render. * @return {boolean} * @final */ prerenderAllowed() { if (this.hasAttribute('noprerender')) { return false; } return this.implClass_ ? this.implClass_.prerenderAllowed(this) : false; } /** * Whether the element can preview. * @return {boolean} * @final */ previewAllowed() { return this.implClass_ ? this.implClass_.previewAllowed(this) : false; } /** * Whether the element has render-blocking service. * @return {boolean} * @final */ isBuildRenderBlocking() { return this.impl_ ? this.impl_.isBuildRenderBlocking() : false; } /** * Creates a placeholder for the element. * @return {?Element} * @final */ createPlaceholder() { return this.impl_ ? this.impl_.createPlaceholderCallback() : null; } /** * Creates a loader logo. * @return {{ * content: (!Element|undefined), * color: (string|undefined), * }} * @final */ createLoaderLogo() { return this.implClass_ ? this.implClass_.createLoaderLogoCallback(this) : {}; } /** * Whether the element should ever render when it is not in viewport. * @return {boolean|number} * @final */ renderOutsideViewport() { return this.impl_ ? this.impl_.renderOutsideViewport() : false; } /** * Whether the element should render outside of renderOutsideViewport when * the scheduler is idle. * @return {boolean|number} * @final */ idleRenderOutsideViewport() { return this.impl_ ? this.impl_.idleRenderOutsideViewport() : false; } /** * Returns a previously measured layout box adjusted to the viewport. This * mainly affects fixed-position elements that are adjusted to be always * relative to the document position in the viewport. * @return {!./layout-rect.LayoutRectDef} * @final */ getLayoutBox() { return this.getResource_().getLayoutBox(); } /** * Returns a previously measured layout size. * @return {!./layout-rect.LayoutSizeDef} * @final */ getLayoutSize() { return this.getResource_().getLayoutSize(); } /** * @return {?Element} * @final */ getOwner() { return this.getResource_().getOwner(); } /** * Returns a change entry for that should be compatible with * IntersectionObserverEntry. * @return {?IntersectionObserverEntry} A change entry. * @final */ getIntersectionChangeEntry() { const box = this.impl_ ? this.impl_.getIntersectionElementLayoutBox() : this.getLayoutBox(); const owner = this.getOwner(); const viewport = Services.viewportForDoc(this.getAmpDoc()); const viewportBox = viewport.getRect(); // TODO(jridgewell, #4826): We may need to make this recursive. const ownerBox = owner && owner.getLayoutBox(); return getIntersectionChangeEntry(box, ownerBox, viewportBox); } /** * Returns the resource of the element. * @return {!./service/resource.Resource} * @private */ getResource_() { return this.getResources().getResourceForElement(this); } /** * Returns the resource ID of the element. * @return {number} */ getResourceId() { return this.getResource_().getId(); } /** * The runtime calls this method to determine if {@link layoutCallback} * should be called again when layout changes. * @return {boolean} * @package @final * TODO(#31915): remove once R1 migration is complete. */ isRelayoutNeeded() { return this.impl_ ? this.impl_.isRelayoutNeeded() : false; } /** * Returns reference to upgraded implementation. * @param {boolean} waitForBuild If true, waits for element to be built before * resolving the returned Promise. Default is true. * @return {!Promise<!./base-element.BaseElement>} */ getImpl(waitForBuild = true) { const waitFor = waitForBuild ? this.build() : this.createImpl_(); return waitFor.then(() => this.impl_); } /** * @return {!Promise<!./base-element.BaseElement>} * @private */ createImpl_() { return this.signals_ .whenSignal(CommonSignals_Enum.READY_TO_UPGRADE) .then(() => { this.tryUpgrade_(); return this.whenUpgraded(); }); } /** * Returns the object which holds the API surface (the thing we add the * custom methods/properties onto). In Bento, this is the imperative API * object. In AMP, this is the BaseElement instance. * * @return {!Promise<!Object>} */ getApi() { return this.getImpl().then((impl) => impl.getApi()); } /** * Returns the layout of the element. * @return {!Layout_Enum} */ getLayout() { return this.layout_; } /** * Instructs the element to layout its content and load its resources if * necessary by calling the {@link BaseElement.layoutCallback} method that * should be implemented by BaseElement subclasses. Must return a promise * that will yield when the layout and associated loadings are complete. * * This method is always called for the first layout, but for subsequent * layouts the runtime consults {@link isRelayoutNeeded} method. * * Can only be called on a upgraded and built element. * * @param {!AbortSignal} signal * @return {!Promise} * @package @final * TODO(#31915): remove once R1 migration is complete. */ layoutCallback(signal) { assertNotTemplate(this); devAssert(this.isBuilt(), 'Must be built to receive viewport events'); // A lot of tests call layoutCallback manually, and don't pass a signal. if ((!getMode().test || signal) && signal.aborted) { return Promise.reject(cancellation()); } this.dispatchCustomEventForTesting(AmpEvents_Enum.LOAD_START); const isLoadEvent = this.layoutCount_ == 0; // First layout is "load". this.signals_.reset(CommonSignals_Enum.UNLOAD); if (isLoadEvent) { this.signals_.signal(CommonSignals_Enum.LOAD_START); } // Potentially start the loading indicator. this.toggleLoading(true); const promise = tryResolve(() => this.impl_.layoutCallback()); this.preconnect(/* onLayout */ true); this.classList.add('i-amphtml-layout'); return promise.then( () => { if ((!getMode().test || signal) && signal.aborted) { throw cancellation(); } if (isLoadEvent) { this.signals_.signal(CommonSignals_Enum.LOAD_END); } this.setReadyStateInternal(ReadyState_Enum.COMPLETE); this.layoutCount_++; this.toggleLoading(false); // Check if this is the first success layout that needs // to call firstLayoutCompleted. if (!this.isFirstLayoutCompleted_) { this.impl_.firstLayoutCompleted(); this.isFirstLayoutCompleted_ = true; this.dispatchCustomEventForTesting(AmpEvents_Enum.LOAD_END); } }, (reason) => { if ((!getMode().test || signal) && signal.aborted) { throw cancellation(); } // add layoutCount_ by 1 despite load fails or not if (isLoadEvent) { this.signals_.rejectSignal( CommonSignals_Enum.LOAD_END, /** @type {!Error} */ (reason) ); } this.setReadyStateInternal(ReadyState_Enum.ERROR, reason); this.layoutCount_++; this.toggleLoading(false); throw reason; } ); } /** * Pauses the element. * * @package @final */ pause() { if (!this.isBuilt()) { // Not built yet. return; } this.impl_.pauseCallback(); // Legacy unlayoutOnPause support. if (!this.R1() && this.impl_.unlayoutOnPause()) { this.unlayout_(); } } /** * Requests the resource to resume its activity when the document returns * from an inactive state. The scope is up to the actual component. Among * other things the active playback of video or audio content may be * resumed. * * @package @final */ resume() { if (!this.isBuilt()) { return; } this.impl_.resumeCallback(); } /** * Requests the element to unload any expensive resources when the element * goes into non-visible state. The scope is up to the actual component. * * Calling this method on unbuilt or unupgraded element has no effect. * * @return {boolean} * @package @final * TODO(#31915): remove once R1 migration is complete. */ unlayoutCallback() { assertNotTemplate(this); if (!this.isBuilt()) { return false; } this.signals_.signal(CommonSignals_Enum.UNLOAD); const isReLayoutNeeded = this.impl_.unlayoutCallback(); if (isReLayoutNeeded) { this.reset_(); } this.dispatchCustomEventForTesting(AmpEvents_Enum.UNLOAD); return isReLayoutNeeded; } /** @private */ unlayout_() { this.getResource_().unlayout(); if (this.isConnected_ && this.resources_) { this.resources_./*OK*/ schedulePass(); } } /** @private */ reset_() { this.layoutCount_ = 0; this.isFirstLayoutCompleted_ = false; this.signals_.reset(CommonSignals_Enum.MOUNTED); this.signals_.reset(CommonSignals_Enum.RENDER_START); this.signals_.reset(CommonSignals_Enum.LOAD_START); this.signals_.reset(CommonSignals_Enum.LOAD_END); this.signals_.reset(CommonSignals_Enum.INI_LOAD); } /** * Whether the element needs to be reconstructed after it has been * re-parented. Many elements cannot survive fully the reparenting and * are better to be reconstructed from scratch. * * @return {boolean} * @package @final */ reconstructWhenReparented() { return this.impl_ ? this.impl_.reconstructWhenReparented() : false; } /** * Collapses the element, and notifies its owner (if there is one) that the * element is no longer present. */ collapse() { if (this.impl_) { this.impl_./*OK*/ collapse(); } } /** * Called every time an owned AmpElement collapses itself. * @param {!AmpElement} element */ collapsedCallback(element) { if (this.impl_) { this.impl_.collapsedCallback(element); } } /** * Expands the element, and notifies its owner (if there is one) that the * element is now present. */ expand() { if (this.impl_) { this.impl_./*OK*/ expand(); } } /** * Called when one or more attributes are mutated. * Note: Must be called inside a mutate context. * Note: Boolean attributes have a value of `true` and `false` when * present and missing, respectively. * @param {!JsonObject<string, (null|boolean|string|number|Array|Object)>} mutations */ mutatedAttributesCallback(mutations) { if (this.impl_) { this.impl_.mutatedAttributesCallback(mutations); } else if (this.R1()) { // If amp-bind is trying to mutate an uninitialized BaseElement, it is probably about time // to initialize it. const scheduler = getSchedulerForDoc(this); scheduler.scheduleAsap(this); } } /** * Enqueues the action with the element. If element has been upgraded and * built, the action is dispatched to the implementation right away. * Otherwise the invocation is enqueued until the implementation is ready * to receive actions. * @param {!./service/action-impl.ActionInvocation} invocation * @final */ enqueAction(invocation) { assertNotTemplate(this); if (!this.isBuilt()) { if (this.actionQueue_ === undefined) { this.actionQueue_ = []; } devAssert(this.actionQueue_).push(invocation); // Schedule build sooner. this.build(); } else { this.executionAction_(invocation, false); } } /** * Dequeues events from the queue and dispatches them to the implementation * with "deferred" flag. * @private */ dequeueActions_() { if (!this.actionQueue_) { return; } const actionQueue = devAssert(this.actionQueue_); this.actionQueue_ = null; // Notice, the actions are currently not de-duped. actionQueue.forEach((invocation) => { this.executionAction_(invocation, true); }); } /** * Executes the action immediately. All errors are consumed and reported. * @param {!./service/action-impl.ActionInvocation} invocation * @param {boolean} deferred * @final * @private */ executionAction_(invocation, deferred) { try { this.impl_.executeAction(invocation, deferred); } catch (e) { rethrowAsync( 'Action execution failed:', e, invocation.node.tagName, invocation.method ); } } /** * Get the consent policy to follow. * @return {?string} */ getConsentPolicy_() { let policyId = this.getAttribute('data-block-on-consent'); if (policyId === null) { if (shouldBlockOnConsentByMeta(this)) { policyId = 'default'; this.setAttribute('data-block-on-consent', policyId); } else { // data-block-on-consent attribute not set return null; } } if (policyId == '' || policyId == 'default') { // data-block-on-consent value not set, up to individual element // Note: data-block-on-consent and data-block-on-consent='default' is // treated exactly the same return devAssert(this.impl_).getConsentPolicy(); } return policyId; } /** * Get the purpose consents that should be granted. * @return {Array<string>|undefined} */ getPurposesConsent_() { const purposes = this.getAttribute('data-block-on-consent-purposes') || null; return purposes?.replace(/\s+/g, '')?.split(','); } /** * Returns an optional placeholder element for this custom element. * @return {?Element} * @package @final */ getPlaceholder() { return query.lastChildElement(this, (el) => { return ( el.hasAttribute('placeholder') && // Denylist elements that has a native placeholder property // like input and textarea. These are not allowed to be AMP // placeholders. !isInputPlaceholder(el) ); }); } /** * Hides or shows the placeholder, if available. * @param {boolean} show * @package @final */ togglePlaceholder(show) { assertNotTemplate(this); if (show) { const placeholder = this.getPlaceholder(); if (placeholder) { dev().assertElement(placeholder).classList.remove('amp-hidden'); } } else { const placeholders = query.childElementsByAttr(this, 'placeholder'); for (let i = 0; i < placeholders.length; i++) { // Don't toggle elements with a native placeholder property // e.g. input, textarea if (isInputPlaceholder(placeholders[i])) { continue; } placeholders[i].classList.add('amp-hidden'); } } } /** * Returns an optional fallback element for this custom element. * @return {?Element} * @package @final */ getFallback() { return query.childElementByAttr(this, 'fallback'); } /** * Hides or shows the fallback, if available. This function must only * be called inside a mutate context. * @param {boolean} show * @package @final */ toggleFallback(show) { assertNotTemplate(this); const resourceState = this.getResource_().getState(); // Do not show fallback before layout if ( !this.R1() && show && (resourceState == ResourceState_Enum.NOT_BUILT || resourceState == ResourceState_Enum.NOT_LAID_OUT || resourceState == ResourceState_Enum.READY_FOR_LAYOUT) ) { return; } // This implementation is notably less efficient then placeholder // toggling. The reasons for this are: (a) "not supported" is the state of // the whole element, (b) some relayout is expected and (c) fallback // condition would be rare. this.classList.toggle('amp-notsupported', show); if (show == true) { const fallbackElement = this.getFallback(); if (fallbackElement) { Services.ownersForDoc(this.getAmpDoc()).scheduleLayout( this, fallbackElement ); } } } /** * An implementation can call this method to signal to the element that * it has started rendering. * @package @final */ renderStarted() { this.signals_.signal(CommonSignals_Enum.RENDER_START); this.togglePlaceholder(false); this.toggleLoading(false); } /** * Whether the loading can be shown for this element. * @param {boolean} force * @return {boolean} * @private */ isLoadingEnabled_(force) { // No loading indicator will be shown if either one of these conditions // true: // 1. The document is A4A. // 2. `noloading` attribute is specified; // 3. The element has already been laid out, and does not support reshowing the indicator (include having loading // error); // 4. The element is too small or has not yet been measured; // 5. The element has not been allowlisted; // 6. The element is an internal node (e.g. `placeholder` or `fallback`); // 7. The element's layout is not nodisplay. const laidOut = this.layoutCount_ > 0 || this.signals_.get(CommonSignals_Enum.RENDER_START); if ( this.layout_ == Layout_Enum.NODISPLAY || this.hasAttribute('noloading') || (laidOut && !force) || !isLoadingAllowed(this) || query.isInternalOrServiceNode(this) ) { return false; } return true; } /** * Turns the loading indicator on or off. * @param {boolean} state * @param {boolean=} force * @public @final */ toggleLoading(state, force = false) { // TODO(dvoytenko, #9177): cleanup `this.ownerDocument.defaultView` // once investigation is complete. It appears that we get a lot of // errors here once the iframe is destroyed due to timer. if (!this.ownerDocument || !this.ownerDocument.defaultView) { return; } const loadingIndicator = Services.loadingIndicatorOrNull( this.getAmpDoc() ); if (loadingIndicator) { state = state && this.isLoadingEnabled_(force); if (state) { loadingIndicator.track(this); } else { loadingIndicator.untrack(this); } } } /** * Returns an optional overflow element for this custom element. * @return {?Element} */ getOverflowElement() { if (this.overflowElement_ === undefined) { this.overflowElement_ = query.childElementByAttr(this, 'overflow'); if (this.overflowElement_) { if (!this.overflowElement_.hasAttribute('tabindex')) { this.overflowElement_.setAttribute('tabindex', '0'); } if (!this.overflowElement_.hasAttribute('role')) { this.overflowElement_.setAttribute('role', 'button'); } } } return this.overflowElement_; } /** * Hides or shows the overflow, if available. This function must only * be called inside a mutate context. * @param {boolean} overflown * @param {number|undefined} requestedHeight * @param {number|undefined} requestedWidth * @package @final */ overflowCallback(overflown, requestedHeight, requestedWidth) { this.getOverflowElement(); if (!this.overflowElement_) { if (overflown && this.warnOnMissingOverflow) { user().warn( TAG, 'Cannot resize element and overflow is not available', this ); } } else { this.overflowElement_.classList.toggle('amp-visible', overflown); if (overflown) { this.overflowElement_.onclick = () => { const mutator = Services.mutatorForDoc(this.getAmpDoc()); mutator.forceChangeSize(this, requestedHeight, requestedWidth); mutator.mutateElement(this, () => { this.overflowCallback( /* overflown */ false, requestedHeight, requestedWidth ); }); }; } else { this.overflowElement_.onclick = null; } } } /** * Mutates the element using resources if available. * * @param {function()} mutator * @param {?Element=} opt_element * @param {boolean=} opt_skipRemeasure */ mutateOrInvoke_(mutator, opt_element, opt_skipRemeasure = false) { if (this.ampdoc_) { Services.mutatorForDoc(this.getAmpDoc()).mutateElement( opt_element || this, mutator, opt_skipRemeasure ); } else { mutator(); } } } win.__AMP_BASE_CE_CLASS = BaseCustomElement; return /** @type {typeof HTMLElement} */ (win.__AMP_BASE_CE_CLASS); } /** * @param {!Element} element * @return {boolean} */ function isInputPlaceholder(element) { return 'placeholder' in element; } /** @param {!Element} element */ function assertNotTemplate(element) { devAssert(!element.isInTemplate_, 'Must never be called in template'); } /** * Creates a new custom element class prototype. * * @param {!Window} win The window in which to register the custom element. * @param {(typeof ./base-element.BaseElement)=} opt_implementationClass For testing only. * @param {function(!./service/ampdoc-impl.AmpDoc, !AmpElement, ?(typeof BaseElement))=} opt_elementConnectedCallback * @return {!Object} Prototype of element. * @visibleForTesting */ export function createAmpElementForTesting( win, opt_implementationClass, opt_elementConnectedCallback ) { const Element = createCustomElementClass( win, opt_elementConnectedCallback || (() => {}) ); if (getMode().test && opt_implementationClass) { Element.prototype.implementationClassForTesting = opt_implementationClass; } return Element; } /** * @visibleForTesting */ export function resetStubsForTesting() { stubbedElements.length = 0; } /** * @param {!AmpElement} element * @return {?(typeof BaseElement)} * @visibleForTesting */ export function getImplClassSyncForTesting(element) { return element.implClass_; } /** * @param {!AmpElement} element * @return {!BaseElement} * @visibleForTesting */ export function getImplSyncForTesting(element) { return element.impl_; } /** * @param {!AmpElement} element * @return {?Array<!./service/action-impl.ActionInvocation>|undefined} * @visibleForTesting */ export function getActionQueueForTesting(element) { return element.actionQueue_; } /** * Marks each element that still stubbed as unresolved. * @param {string=} opt_extension */ export function markUnresolvedElements(opt_extension) { unresolvableExtensions.add(opt_extension || '*'); for (const el of stubbedElements) { if (opt_extension == null || el.tagName.toLowerCase() === opt_extension) { el.markUnresolved(); } } } /** */ export function resetUnresolvedElementsForTesting() { unresolvableExtensions.clear(); }