/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/service/ampdoc-impl.js
1 104 строки
29 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {VisibilityState_Enum} from '#core/constants/visibility-state'; import {Observable} from '#core/data-structures/observable'; import {Deferred} from '#core/data-structures/promise'; import {Signals} from '#core/data-structures/signals'; import {isDocumentReady, whenDocumentReady} from '#core/document/ready'; import { addDocumentVisibilityChangeListener, getDocumentVisibilityState, removeDocumentVisibilityChangeListener, } from '#core/document/visibility'; import {rootNodeFor, waitForBodyOpenPromise} from '#core/dom'; import {isEnumValue} from '#core/types'; import {map} from '#core/types/object'; import {parseQueryString} from '#core/types/string/url'; import {WindowInterface} from '#core/window/interface'; import {dev, devAssert} from '#utils/log'; import { disposeServicesForDoc, getParentWindowFrameElement, registerServiceBuilder, } from '../service-helpers'; /** @const {string} */ const AMPDOC_PROP = '__AMPDOC'; /** @const {string} */ const PARAMS_SENTINEL = '__AMP__'; /** * @typedef {{ * params: (!{[key: string]: string}|undefined), * signals: (?Signals|undefined), * visibilityState: (?VisibilityState_Enum|undefined), * }} */ export let AmpDocOptions; /** * Private ampdoc signals. * @enum {string} */ const AmpDocSignals_Enum = { // A complete preinstalled list of extensions is known. EXTENSIONS_KNOWN: '-ampdoc-ext-known', // Signals the document has become visible for the first time. FIRST_VISIBLE: '-ampdoc-first-visible', // Signals when the document becomes visible the next time. NEXT_VISIBLE: '-ampdoc-next-visible', // Signals the document has been previewed for the first time. FIRST_PREVIEWED: '-ampdoc-first-previewed', }; /** * This service helps locate an ampdoc (`AmpDoc` instance) for any node, * either in the single-doc or shadow-doc environments. * * In the single-doc environment an ampdoc is equivalent to the * `window.document`. In the shadow-doc mode, any number of AMP documents * could be hosted in shadow roots in the same global `window.document`. * * @package */ export class AmpDocService { /** * @param {!Window} win * @param {boolean} isSingleDoc * @param {!{[key: string]: string}=} opt_initParams */ constructor(win, isSingleDoc, opt_initParams) { /** @const {!Window} */ this.win = win; /** @private {?AmpDoc} */ this.singleDoc_ = null; if (isSingleDoc) { this.singleDoc_ = new AmpDocSingle(win, { params: extractSingleDocParams(win, opt_initParams), }); win.document[AMPDOC_PROP] = this.singleDoc_; } } /** * Whether the runtime in the single-doc mode. Alternative is the shadow-doc * mode that supports multiple documents per a single window. * @return {boolean} */ isSingleDoc() { // TODO(#22733): remove when ampdoc-fie is launched. return !!this.singleDoc_; } /** * Returns the document in the single-doc mode. In a multi-doc mode, an * error will be thrown. * @return {!AmpDoc} */ getSingleDoc() { // TODO(#22733): once docroot migration is done, this should be renamed // to `getTopDoc()` method. return devAssert(this.singleDoc_); } /** * If the node is an AMP custom element, retrieves the AmpDoc reference. * @param {!Node} node * @return {?AmpDoc} The AmpDoc reference, if one exists. */ getCustomElementAmpDocReference_(node) { // We can only look up the AmpDoc from a custom element if it has been // attached at some point. If it is not a custom element, one or both of // these checks should fail. if (!node.everAttached || typeof node.getAmpDoc !== 'function') { return null; } return node.getAmpDoc(); } /** * Returns the instance of the ampdoc (`AmpDoc`) that contains the specified * node. * * @param {!Node} node * @return {?AmpDoc} */ getAmpDocIfAvailable(node) { let n = node; while (n) { // A custom element may already have the reference. If we are looking // for the closest AmpDoc, the element might have a reference to the // global AmpDoc, which we do not want. This occurs when using // <amp-next-page>. const cachedAmpDoc = this.getCustomElementAmpDocReference_(node); if (cachedAmpDoc) { return cachedAmpDoc; } // Root note: it's either a document, or a shadow document. const rootNode = rootNodeFor(n); if (!rootNode) { break; } const ampdoc = rootNode[AMPDOC_PROP]; if (ampdoc) { return ampdoc; } // Try to iterate to the host of the current root node. // First try the shadow root's host. if (rootNode.host) { n = rootNode.host; } else { // Then, traverse the boundary of a friendly iframe. n = getParentWindowFrameElement(rootNode, this.win); } } return null; } /** * Returns the instance of the ampdoc (`AmpDoc`) that contains the specified * node. If the runtime is in the single-doc mode, the one global `AmpDoc` * instance is returned, unless specifically looking for a closer `AmpDoc`. * Otherwise, this method locates the `AmpDoc` that contains the specified * node and, if necessary, initializes it. * * An Error is thrown in development if no `AmpDoc` is found. * @param {!Node} node * @return {!AmpDoc} */ getAmpDoc(node) { const ampdoc = this.getAmpDocIfAvailable(node); if (!ampdoc) { throw dev().createError('No ampdoc found for', node); } return ampdoc; } /** * Creates and installs the ampdoc for the shadow root. * @param {string} url * @param {!ShadowRoot} shadowRoot * @param {!AmpDocOptions=} opt_options * @return {!AmpDocShadow} * @restricted */ installShadowDoc(url, shadowRoot, opt_options) { devAssert( !shadowRoot[AMPDOC_PROP], 'The shadow root already contains ampdoc' ); const ampdoc = new AmpDocShadow(this.win, url, shadowRoot, opt_options); shadowRoot[AMPDOC_PROP] = ampdoc; return ampdoc; } /** * Creates and installs the ampdoc for the fie root. * @param {string} url * @param {!Window} childWin * @param {!AmpDocOptions=} opt_options * @return {!AmpDocFie} * @restricted */ installFieDoc(url, childWin, opt_options) { const doc = childWin.document; devAssert(!doc[AMPDOC_PROP], 'The fie already contains ampdoc'); const frameElement = devAssert(childWin.frameElement); const ampdoc = new AmpDocFie( childWin, url, this.getAmpDoc(frameElement), opt_options ); doc[AMPDOC_PROP] = ampdoc; return ampdoc; } } /** * This class represents a single ampdoc. `AmpDocService` can contain only one * global ampdoc or multiple, depending on the runtime mode: single-doc or * shadow-doc. * @abstract * @package */ export class AmpDoc { /** * @param {!Window} win * @param {?AmpDoc} parent * @param {!AmpDocOptions=} opt_options */ constructor(win, parent, opt_options) { /** @public @const {!Window} */ this.win = win; /** @private {!{[key: ../enums.AMPDOC_SINGLETON_NAME_ENUM]: boolean}} */ this.registeredSingleton_ = map(); /** @public @const {?AmpDoc} */ this.parent_ = parent; /** @private @const */ this.signals_ = (opt_options && opt_options.signals) || new Signals(); /** @private {!{[key: string]: string}} */ this.params_ = (opt_options && opt_options.params) || map(); /** @protected {?{[key: string]: string}} */ this.meta_ = null; /** @private @const {!{[key: string]: string}} */ this.declaredExtensions_ = {}; const paramsVisibilityState = this.params_['visibilityState']; devAssert( !paramsVisibilityState || isEnumValue(VisibilityState_Enum, paramsVisibilityState) ); /** @private {?VisibilityState_Enum} */ this.visibilityStateOverride_ = (opt_options && opt_options.visibilityState) || paramsVisibilityState || null; // Start with `null` to be updated by updateVisibilityState_ in the end // of the constructor to ensure the correct "update" logic and promise // resolution. /** @private {?VisibilityState_Enum} */ this.visibilityState_ = null; /** @private @const {!Observable<!VisibilityState_Enum>} */ this.visibilityStateHandlers_ = new Observable(); /** @private {?time} */ this.lastVisibleTime_ = null; /** @private @const {!Array<!UnlistenDef>} */ this.unsubsribes_ = []; const boundUpdateVisibilityState = this.updateVisibilityState_.bind(this); if (this.parent_) { this.unsubsribes_.push( this.parent_.onVisibilityChanged(boundUpdateVisibilityState) ); } addDocumentVisibilityChangeListener( this.win.document, boundUpdateVisibilityState ); this.unsubsribes_.push(() => removeDocumentVisibilityChangeListener( this.win.document, boundUpdateVisibilityState ) ); this.updateVisibilityState_(); } /** * Dispose the document. */ dispose() { disposeServicesForDoc(this); this.unsubsribes_.forEach((unsubscribe) => unsubscribe()); } /** * Whether the runtime in the single-doc mode. Alternative is the shadow-doc * mode that supports multiple documents per a single window. * @return {boolean} */ isSingleDoc() { // TODO(#22733): remove when ampdoc-fie is launched. return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * @return {?AmpDoc} */ getParent() { return this.parent_; } /** @return {!Signals} */ signals() { return this.signals_; } /** * Returns the value of a ampdoc's startup parameter with the specified * name or `null` if the parameter wasn't defined at startup time. * @param {string} name * @return {?string} */ getParam(name) { const v = this.params_[name]; return v == null ? null : v; } /** * Initializes (if necessary) cached map of an ampdoc's meta name values to * their associated content values and returns the map. * @return {!{[key: string]: string}} */ getMeta() { if (this.meta_) { return map(this.meta_); } this.meta_ = map(); const metaEls = dev() .assertElement(this.win.document.head) .querySelectorAll('meta[name]'); metaEls.forEach((metaEl) => { const name = metaEl.getAttribute('name'); const content = metaEl.getAttribute('content'); if (!name || content === null) { return; } // Retain only the first meta content value for a given name if (this.meta_[name] === undefined) { this.meta_[name] = content; } }); return map(this.meta_); } /** * Returns the value of an ampdoc's meta tag content for a given name, or * `null` if the meta tag does not exist. * @param {string} name * @return {?string} */ getMetaByName(name) { if (!name) { return null; } const content = this.getMeta()[name]; return content !== undefined ? content : null; } /** * Stores the value of an ampdoc's meta tag content for a given name. To be * implemented by subclasses. * @param {string} unusedName * @param {string} unusedContent * * Avoid using this method in components. It is only meant to be used by the * runtime for AmpDoc subclasses where <meta> elements do not exist and name/ * content pairs must be stored in this.meta_. */ setMetaByName(unusedName, unusedContent) { devAssert(null, 'not implemented'); } /** * Returns whether the specified extension has been declared on this ampdoc. * @param {string} extensionId * @param {string=} opt_version * @return {boolean} */ declaresExtension(extensionId, opt_version) { const declared = this.declaredExtensions_[extensionId]; if (!declared) { return false; } return !opt_version || declared === opt_version; } /** * Adds a declared extension to an ampdoc. * @param {string} extensionId * @param {string} version * @restricted */ declareExtension(extensionId, version) { devAssert( !this.declaredExtensions_[extensionId] || this.declaredExtensions_[extensionId] === version, 'extension already declared %s', extensionId ); this.declaredExtensions_[extensionId] = version; } /** * @param {string} extensionId * @return {?string} */ getExtensionVersion(extensionId) { return this.declaredExtensions_[extensionId] || null; } /** * Signal that the initial document set of extensions is known. * @restricted */ setExtensionsKnown() { this.signals_.signal(AmpDocSignals_Enum.EXTENSIONS_KNOWN); } /** * Resolved when the initial document set of extension is known. * @return {!Promise} */ whenExtensionsKnown() { return this.signals_.whenSignal(AmpDocSignals_Enum.EXTENSIONS_KNOWN); } /** * Returns the root node for this ampdoc. It will either be a `Document` for * the single-doc runtime mode, or a `ShadowRoot` for shadow-doc mode. This * node can be used, among other things, to add ampdoc-wide event listeners. * * @return {!Document|!ShadowRoot} */ getRootNode() { return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * Returns the head node. It's either an element or a shadow root. * @return {!Element|!ShadowRoot} * @abstract */ getHeadNode() {} /** * Returns `true` if the ampdoc's body is available. * * @return {boolean} */ isBodyAvailable() { return /** @type {?} */ (devAssert(false, 'not implemented')); } /** * Returns the ampdoc's body. Requires the body to already be available. * * See `isBodyAvailable` and `waitForBodyOpen`. * * @return {!Element} */ getBody() { return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * Returns a promise that will be resolved when the ampdoc's body is * available. * @return {!Promise<!Element>} */ waitForBodyOpen() { return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * Returns `true` if document is ready. * * See `whenReady`. * * @return {boolean} */ isReady() { return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * Returns a promise that will be resolved when the ampdoc's DOM is fully * ready. * @return {!Promise} */ whenReady() { return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * Returns the URL from which the document was loaded. * @return {string} */ getUrl() { return /** @type {?} */ (devAssert(null, 'not implemented')); } /** * Locates an element with the specified ID within the ampdoc. In the * shadow-doc mode, when multiple documents could be present, this method * localizes search only to the DOM subtree specific to this ampdoc. * * @param {string} id * @return {?Element} */ getElementById(id) { return this.getRootNode().getElementById(id); } /** * Whether the node is currently contained in the DOM of the root. * @param {?Node} node * @return {boolean} */ contains(node) { return this.getRootNode().contains(node); } /** * @param {!VisibilityState_Enum} visibilityState * @restricted */ overrideVisibilityState(visibilityState) { if (this.visibilityStateOverride_ != visibilityState) { this.visibilityStateOverride_ = visibilityState; this.updateVisibilityState_(); } } /** @private */ updateVisibilityState_() { // Natural visibility state. const naturalVisibilityState = getDocumentVisibilityState( this.win.document ); // Parent visibility: pick the first non-visible state. let parentVisibilityState = VisibilityState_Enum.VISIBLE; for (let p = this.parent_; p; p = p.getParent()) { if (p.getVisibilityState() != VisibilityState_Enum.VISIBLE) { parentVisibilityState = p.getVisibilityState(); break; } } // Pick the most restricted visibility state. let visibilityState; const visibilityStateOverride = this.visibilityStateOverride_ || VisibilityState_Enum.VISIBLE; if ( visibilityStateOverride == VisibilityState_Enum.VISIBLE && parentVisibilityState == VisibilityState_Enum.VISIBLE && naturalVisibilityState == VisibilityState_Enum.VISIBLE ) { visibilityState = VisibilityState_Enum.VISIBLE; } else if ( naturalVisibilityState == VisibilityState_Enum.HIDDEN && visibilityStateOverride == VisibilityState_Enum.PAUSED ) { // Hidden document state overrides "paused". visibilityState = naturalVisibilityState; } else if ( visibilityStateOverride == VisibilityState_Enum.PAUSED || visibilityStateOverride == VisibilityState_Enum.INACTIVE ) { visibilityState = visibilityStateOverride; } else if ( parentVisibilityState == VisibilityState_Enum.PAUSED || parentVisibilityState == VisibilityState_Enum.INACTIVE ) { visibilityState = parentVisibilityState; } else if ( visibilityStateOverride == VisibilityState_Enum.PREVIEW || naturalVisibilityState == VisibilityState_Enum.PREVIEW || parentVisibilityState == VisibilityState_Enum.PREVIEW ) { visibilityState = VisibilityState_Enum.PREVIEW; } else if ( visibilityStateOverride == VisibilityState_Enum.PRERENDER || naturalVisibilityState == VisibilityState_Enum.PRERENDER || parentVisibilityState == VisibilityState_Enum.PRERENDER ) { visibilityState = VisibilityState_Enum.PRERENDER; } else { visibilityState = VisibilityState_Enum.HIDDEN; } if (this.visibilityState_ != visibilityState) { if (visibilityState == VisibilityState_Enum.VISIBLE) { const {performance} = this.win; // We use the initial loading time of the document as the base // visibleTime. If we are initialized in visible mode, then this is // accounts for the time that user saw a blank page waiting for JS // execution. let visibleTime = Math.floor( performance.timeOrigin ?? performance.timing.navigationStart ); if (this.visibilityState_ != null) { // We're transitioning into visible mode (instead of being // initialized in it). In this case, we want to adjust the // visibleTime to the current timestamp, because the user hasn't // actually been waiting for the page to load. Remember that // performance.now() is relative to the load time of the document, so // the current timestamp is actually load+now. visibleTime += Math.floor(performance.now()); } this.lastVisibleTime_ = visibleTime; this.signals_.signal(AmpDocSignals_Enum.FIRST_VISIBLE, visibleTime); this.signals_.signal(AmpDocSignals_Enum.NEXT_VISIBLE, visibleTime); } else { this.signals_.reset(AmpDocSignals_Enum.NEXT_VISIBLE); } if (visibilityState == VisibilityState_Enum.PREVIEW) { this.signals_.signal(AmpDocSignals_Enum.FIRST_PREVIEWED); } this.visibilityState_ = visibilityState; this.visibilityStateHandlers_.fire(); } } /** * Returns a Promise that only ever resolved when the current * AMP document first reaches the `PREVIEW` visibility state. * @return {!Promise} */ whenFirstPreviewedOrVisible() { return Promise.race([this.whenFirstPreviewed(), this.whenFirstVisible()]); } /** * Returns a Promise that only ever resolved when the current * AMP document first reaches the `PREVIEW` visibility state. * @return {!Promise} */ whenFirstPreviewed() { return this.signals_ .whenSignal(AmpDocSignals_Enum.FIRST_PREVIEWED) .then(() => undefined); } /** * Returns a Promise that only ever resolved when the current * AMP document first becomes visible. * @return {!Promise} */ whenFirstVisible() { return this.signals_ .whenSignal(AmpDocSignals_Enum.FIRST_VISIBLE) .then(() => undefined); } /** * Returns a Promise that resolve when current doc becomes visible. * The promise resolves immediately if doc is already visible. * @return {!Promise} */ whenNextVisible() { return this.signals_ .whenSignal(AmpDocSignals_Enum.NEXT_VISIBLE) .then(() => undefined); } /** * Returns the time when the document has become visible for the first time. * If document has not yet become visible, the returned value is `null`. * @return {?time} */ getFirstVisibleTime() { return /** @type {?number} */ ( this.signals_.get(AmpDocSignals_Enum.FIRST_VISIBLE) ); } /** * Returns the time when the document has become visible for the last time. * If document has not yet become visible, the returned value is `null`. * @return {?time} */ getLastVisibleTime() { return this.lastVisibleTime_; } /** * Returns visibility state configured by the viewer. * See {@link isVisible}. * @return {!VisibilityState_Enum} */ getVisibilityState() { return devAssert(this.visibilityState_); } /** * Whether the AMP document currently being previewed. * @return {boolean} */ isPreview() { return this.visibilityState_ == VisibilityState_Enum.PREVIEW; } /** * Whether the AMP document currently visible. The reasons why it might not * be visible include user switching to another tab, browser running the * document in the prerender mode or viewer running the document in the * prerender mode. * @return {boolean} */ isVisible() { return this.visibilityState_ == VisibilityState_Enum.VISIBLE; } /** * Whether the AMP document has been ever visible before. Since the visibility * state of a document can be flipped back and forth we sometimes want to know * if a document has ever been visible. * @return {boolean} */ hasBeenVisible() { return this.getLastVisibleTime() != null; } /** * Adds a "visibilitychange" event listener for viewer events. The * callback can check {@link isVisible} and {@link getPrefetchCount} * methods for more info. * @param {function(!VisibilityState_Enum)} handler * @return {!UnlistenDef} */ onVisibilityChanged(handler) { return this.visibilityStateHandlers_.add(handler); } /** * Attempt to register a singleton for each ampdoc. * Caller need to handle user error when registration returns false. * @param {!../enums.AMPDOC_SINGLETON_NAME_ENUM} name * @return {boolean} */ registerSingleton(name) { if (!this.registeredSingleton_[name]) { this.registeredSingleton_[name] = true; return true; } return false; } } /** * The version of `AmpDoc` in the single-doc mode that corresponds to the * global `window.document`. * @package @visibleForTesting */ export class AmpDocSingle extends AmpDoc { /** * @param {!Window} win * @param {!AmpDocOptions=} opt_options */ constructor(win, opt_options) { super(win, /* parent */ null, opt_options); /** @private @const {!Promise<!Element>} */ this.bodyPromise_ = this.win.document.body ? Promise.resolve(this.win.document.body) : waitForBodyOpenPromise(this.win.document).then(() => this.getBody()); /** @private @const {!Promise} */ this.readyPromise_ = whenDocumentReady(this.win.document); } /** @override */ isSingleDoc() { return true; } /** @override */ getRootNode() { return this.win.document; } /** @override */ getUrl() { return WindowInterface.getLocation(this.win).href; } /** @override */ getHeadNode() { return dev().assertElement(this.win.document.head); } /** @override */ isBodyAvailable() { return !!this.win.document.body; } /** @override */ getBody() { return dev().assertElement(this.win.document.body, 'body not available'); } /** @override */ waitForBodyOpen() { return this.bodyPromise_; } /** @override */ isReady() { return isDocumentReady(this.win.document); } /** @override */ whenReady() { return this.readyPromise_; } } /** * The version of `AmpDoc` in the shadow-doc mode that is allocated for each * ampdoc hosted within a shadow root. * @package @visibleForTesting */ export class AmpDocShadow extends AmpDoc { /** * @param {!Window} win * @param {string} url * @param {!ShadowRoot} shadowRoot * @param {!AmpDocOptions=} opt_options */ constructor(win, url, shadowRoot, opt_options) { super(win, /* parent */ null, opt_options); /** @private @const {string} */ this.url_ = url; /** @private @const {!ShadowRoot} */ this.shadowRoot_ = shadowRoot; /** @private {?Element} */ this.body_ = null; const bodyDeferred = new Deferred(); /** @private {!Promise<!Element>} */ this.bodyPromise_ = bodyDeferred.promise; /** @private {function(!Element)|undefined} */ this.bodyResolver_ = bodyDeferred.resolve; /** @private {boolean} */ this.ready_ = false; const readyDeferred = new Deferred(); /** @private {!Promise} */ this.readyPromise_ = readyDeferred.promise; /** @private {function()|undefined} */ this.readyResolver_ = readyDeferred.resolve; } /** @override */ isSingleDoc() { return false; } /** @override */ getRootNode() { return this.shadowRoot_; } /** @override */ getUrl() { return this.url_; } /** @override */ getHeadNode() { return this.shadowRoot_; } /** @override */ isBodyAvailable() { return !!this.body_; } /** @override */ getBody() { return dev().assertElement(this.body_, 'body not available'); } /** * Signals that the shadow doc has a body. * @param {!Element} body * @restricted */ setBody(body) { devAssert(!this.body_, 'Duplicate body'); this.body_ = body; this.bodyResolver_(body); this.bodyResolver_ = undefined; } /** @override */ waitForBodyOpen() { return this.bodyPromise_; } /** @override */ isReady() { return this.ready_; } /** * Signals that the shadow doc is ready. * @restricted */ setReady() { devAssert(!this.ready_, 'Duplicate ready state'); this.ready_ = true; this.readyResolver_(); this.readyResolver_ = undefined; } /** @override */ whenReady() { return this.readyPromise_; } /** @override */ getMeta() { return /** @type {!{[key: string]: string}} */ (map(this.meta_)); } /** @override */ setMetaByName(name, content) { devAssert(name, 'Attempted to store invalid meta name/content pair'); if (!this.meta_) { this.meta_ = map(); } this.meta_[name] = content; } } /** * The version of `AmpDoc` for FIE embeds. * @package @visibleForTesting */ export class AmpDocFie extends AmpDoc { /** * @param {!Window} win * @param {string} url * @param {!AmpDoc} parent * @param {!AmpDocOptions=} opt_options */ constructor(win, url, parent, opt_options) { super(win, parent, opt_options); /** @private @const {string} */ this.url_ = url; /** @private @const {!Promise<!Element>} */ this.bodyPromise_ = this.win.document.body ? Promise.resolve(this.win.document.body) : waitForBodyOpenPromise(this.win.document).then(() => this.getBody()); /** @private {boolean} */ this.ready_ = false; const readyDeferred = new Deferred(); /** @private {!Promise} */ this.readyPromise_ = readyDeferred.promise; /** @private {function()|undefined} */ this.readyResolver_ = readyDeferred.resolve; } /** @override */ isSingleDoc() { return false; } /** @override */ getRootNode() { return this.win.document; } /** @override */ getUrl() { return this.url_; } /** @override */ getHeadNode() { return dev().assertElement(this.win.document.head); } /** @override */ isBodyAvailable() { return !!this.win.document.body; } /** @override */ getBody() { return dev().assertElement(this.win.document.body, 'body not available'); } /** @override */ waitForBodyOpen() { return this.bodyPromise_; } /** @override */ isReady() { return this.ready_; } /** @override */ whenReady() { return this.readyPromise_; } /** * Signals that the FIE doc is ready. * @restricted */ setReady() { devAssert(!this.ready_, 'Duplicate ready state'); this.ready_ = true; this.readyResolver_(); this.readyResolver_ = undefined; } } /** * @param {!Window} win * @param {!{[key: string]: string}|undefined} initParams * @return {!{[key: string]: string}} */ function extractSingleDocParams(win, initParams) { const params = map(); if (initParams) { // The initialization params take the highest precedence. Object.assign(params, initParams); } else { // Params can be passed via iframe hash/name with hash taking precedence. if (win.name && win.name.indexOf(PARAMS_SENTINEL) == 0) { Object.assign( params, parseQueryString(win.name.substring(PARAMS_SENTINEL.length)) ); } if (win.location && win.location.hash) { Object.assign(params, parseQueryString(win.location.hash)); } } return params; } /** * Install the ampdoc service and immediately configure it for either a * single-doc or a shadow-doc mode. The mode cannot be changed after the * initial configuration. * @param {!Window} win * @param {boolean} isSingleDoc * @param {!{[key: string]: string}=} opt_initParams */ export function installDocService(win, isSingleDoc, opt_initParams) { registerServiceBuilder(win, 'ampdoc', function () { return new AmpDocService(win, isSingleDoc, opt_initParams); }); }