Skip to content

Instantly share code, notes, and snippets.

@mdmower
Created February 26, 2020 06:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdmower/021a2311d5bc62cbb6683401998807ae to your computer and use it in GitHub Desktop.
Save mdmower/021a2311d5bc62cbb6683401998807ae to your computer and use it in GitHub Desktop.
Cached meta
/**
* Copyright 2016 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Deferred} from '../utils/promise';
import {Observable} from '../observable';
import {Signals} from '../utils/signals';
import {VisibilityState} from '../visibility-state';
import {
addDocumentVisibilityChangeListener,
getDocumentVisibilityState,
removeDocumentVisibilityChangeListener,
} from '../utils/document-visibility';
import {dev, devAssert} from '../log';
import {getParentWindowFrameElement, registerServiceBuilder} from '../service';
import {getShadowRootNode} from '../shadow-embed';
import {isDocumentReady, whenDocumentReady} from '../document-ready';
import {isInAmpdocFieExperiment} from '../ampdoc-fie';
import {iterateCursor, rootNodeFor, waitForBodyOpenPromise} from '../dom';
import {map} from '../utils/object';
import {parseQueryString} from '../url';
/** @const {string} */
const AMPDOC_PROP = '__AMPDOC';
/** @const {string} */
const PARAMS_SENTINEL = '__AMP__';
/**
* @typedef {{
* params: (!Object<string, string>|undefined),
* signals: (?Signals|undefined),
* visibilityState: (?VisibilityState|undefined),
* }}
*/
export let AmpDocOptions;
/**
* Private ampdoc signals.
* @enum {string}
*/
const AmpDocSignals = {
// 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',
};
/**
* 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 {!Object<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_;
}
/** @private {boolean} */
this.ampdocFieExperimentOn_ = isInAmpdocFieExperiment(win);
/** @private {boolean} */
this.mightHaveShadowRoots_ = !isSingleDoc;
}
/**
* 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. If the runtime is in the single-doc mode, the one global `AmpDoc`
* instance is returned, unless specfically looking for a closer `AmpDoc`.
* Otherwise, this method locates the `AmpDoc` that contains the specified
* node and, if necessary, initializes it.
*
* TODO(#22733): rewrite docs once the ampdoc-fie is launched.
*
* @param {!Node} node
* @return {?AmpDoc}
*/
getAmpDocIfAvailable(node) {
if (this.ampdocFieExperimentOn_) {
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;
}
// Otherwise discover and possibly create the ampdoc.
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;
}
// Traverse the boundary of a friendly iframe.
const frameElement = getParentWindowFrameElement(n, this.win);
if (frameElement) {
n = frameElement;
continue;
}
if (!this.mightHaveShadowRoots_) {
break;
}
// Shadow doc.
const shadowRoot =
n.nodeType == /* DOCUMENT */ 9 ? n : getShadowRootNode(n);
if (!shadowRoot) {
break;
}
const ampdoc = shadowRoot[AMPDOC_PROP];
if (ampdoc) {
return ampdoc;
}
n = shadowRoot.host;
}
// If we were looking for the closest AmpDoc, then fall back to the single
// doc if there is no other AmpDoc that is closer.
return this.singleDoc_;
}
/**
* 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 specfically 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) {
// Ensure that node is attached if specified. This check uses a new and
// fast `isConnected` API and thus only checked on platforms that have it.
// See https://www.chromestatus.com/feature/5676110549352448.
devAssert(
node['isConnected'] === undefined || node['isConnected'] === true,
'The node must be attached to request ampdoc.'
);
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) {
this.mightHaveShadowRoots_ = true;
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;
/** @public @const {?AmpDoc} */
this.parent_ = parent;
/** @private @const */
this.signals_ = (opt_options && opt_options.signals) || new Signals();
/** @private {!Object<string, string>} */
this.params_ = (opt_options && opt_options.params) || map();
/** @protected {?Object<string, string>} */
this.meta_ = null;
/** @private @const {!Array<string>} */
this.declaredExtensions_ = [];
/** @private {?VisibilityState} */
this.visibilityStateOverride_ =
(opt_options && opt_options.visibilityState) ||
(this.params_['visibilityState'] &&
dev().assertEnumValue(
VisibilityState,
this.params_['visibilityState'],
'VisibilityState'
)) ||
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} */
this.visibilityState_ = null;
/** @private @const {!Observable<!VisibilityState>} */
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() {
this.unsubsribes_.forEach(unsubsribe => unsubsribe());
}
/**
* 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_;
}
/**
* DO NOT CALL. Retained for backward compat during rollout.
* @return {!Window}
* @deprecated Use `ampdoc.win` instead.
*/
getWin() {
return this.win;
}
/** @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 {!Object<string, string>}
*/
getMeta() {
if (this.meta_) {
return map(this.meta_);
}
this.meta_ = map();
const metaEls = dev()
.assertElement(this.win.document.head)
.querySelectorAll('meta[name]');
iterateCursor(metaEls, 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
* @return {boolean}
*/
declaresExtension(extensionId) {
return this.declaredExtensions_.indexOf(extensionId) != -1;
}
/**
* Adds a declared extension to an ampdoc.
* @param {string} extensionId
* @restricted
*/
declareExtension(extensionId) {
if (!this.declaresExtension(extensionId)) {
this.declaredExtensions_.push(extensionId);
}
}
/**
* 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 dev().assertElement(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 dev().assertString(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} 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.VISIBLE;
for (let p = this.parent_; p; p = p.getParent()) {
if (p.getVisibilityState() != VisibilityState.VISIBLE) {
parentVisibilityState = p.getVisibilityState();
break;
}
}
// Pick the most restricted visibility state.
let visibilityState;
const visibilityStateOverride =
this.visibilityStateOverride_ || VisibilityState.VISIBLE;
if (
visibilityStateOverride == VisibilityState.VISIBLE &&
parentVisibilityState == VisibilityState.VISIBLE &&
naturalVisibilityState == VisibilityState.VISIBLE
) {
visibilityState = VisibilityState.VISIBLE;
} else if (
naturalVisibilityState == VisibilityState.HIDDEN &&
visibilityStateOverride == VisibilityState.PAUSED
) {
// Hidden document state overrides "paused".
visibilityState = naturalVisibilityState;
} else if (
visibilityStateOverride == VisibilityState.PAUSED ||
visibilityStateOverride == VisibilityState.INACTIVE
) {
visibilityState = visibilityStateOverride;
} else if (
parentVisibilityState == VisibilityState.PAUSED ||
parentVisibilityState == VisibilityState.INACTIVE
) {
visibilityState = parentVisibilityState;
} else if (
visibilityStateOverride == VisibilityState.PRERENDER ||
naturalVisibilityState == VisibilityState.PRERENDER ||
parentVisibilityState == VisibilityState.PRERENDER
) {
visibilityState = VisibilityState.PRERENDER;
} else {
visibilityState = VisibilityState.HIDDEN;
}
if (this.visibilityState_ != visibilityState) {
this.visibilityState_ = visibilityState;
if (visibilityState == VisibilityState.VISIBLE) {
this.lastVisibleTime_ = Date.now();
this.signals_.signal(AmpDocSignals.FIRST_VISIBLE);
this.signals_.signal(AmpDocSignals.NEXT_VISIBLE);
} else {
this.signals_.reset(AmpDocSignals.NEXT_VISIBLE);
}
this.visibilityStateHandlers_.fire();
}
}
/**
* Returns a Promise that only ever resolved when the current
* AMP document first becomes visible.
* @return {!Promise}
*/
whenFirstVisible() {
return this.signals_
.whenSignal(AmpDocSignals.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.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.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}
*/
getVisibilityState() {
return devAssert(this.visibilityState_);
}
/**
* 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.VISIBLE;
}
/**
* Whether the AMP document has been ever visible before. Since the visiblity
* 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)} handler
* @return {!UnlistenDef}
*/
onVisibilityChanged(handler) {
return this.visibilityStateHandlers_.add(handler);
}
}
/**
* 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 this.win.location.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 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 {!Object<string, string>|undefined} initParams
* @return {!Object<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 {!Object<string, string>=} opt_initParams
*/
export function installDocService(win, isSingleDoc, opt_initParams) {
registerServiceBuilder(win, 'ampdoc', function() {
return new AmpDocService(win, isSingleDoc, opt_initParams);
});
}
/**
* @param {AmpDocService} ampdocService
* @param {boolean} value
* @visibleForTesting
*/
export function updateFieModeForTesting(ampdocService, value) {
// TODO(#22733): remove this method once ampdoc-fie is launched.
ampdocService.ampdocFieExperimentOn_ = value;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment