Skip to content

Commit

Permalink
Create skeleton and navigation for FIE ampdocs (#22737)
Browse files Browse the repository at this point in the history
* Create skeleton and navigation for FIE ampdocs

* review fixes

* lints

* fixes

* test fixes

* fix tests
  • Loading branch information
Dima Voytenko committed Jun 24, 2019
1 parent 5d25607 commit 4f3d688
Show file tree
Hide file tree
Showing 12 changed files with 614 additions and 74 deletions.
24 changes: 23 additions & 1 deletion src/dom.js
Expand Up @@ -214,10 +214,32 @@ export function rootNodeFor(node) {
return node.getRootNode() || node;
}
let n;
for (n = node; !!n.parentNode; n = n.parentNode) {}
// Check isShadowRoot() is only needed for the polyfill case.
for (n = node; !!n.parentNode && !isShadowRoot(n); n = n.parentNode) {}
return n;
}

/**
* Determines if value is actually a `ShadowRoot` node.
* @param {*} value
* @return {boolean}
*/
export function isShadowRoot(value) {
// TODO(#22733): remove in preference to dom's `rootNodeFor`.
if (!value) {
return false;
}
// Node.nodeType == DOCUMENT_FRAGMENT to speed up the tests. Unfortunately,
// nodeType of DOCUMENT_FRAGMENT is used currently for ShadowRoot nodes.
if (value.tagName == 'I-AMPHTML-SHADOW-ROOT') {
return true;
}
return (
value.nodeType == /* DOCUMENT_FRAGMENT */ 11 &&
Object.prototype.toString.call(value) === '[object ShadowRoot]'
);
}

/**
* Finds the closest element that satisfies the callback from this element
* up the DOM subtree.
Expand Down
45 changes: 35 additions & 10 deletions src/friendly-iframe-embed.js
Expand Up @@ -22,6 +22,7 @@ import {closestAncestorElementBySelector, escapeHtml} from './dom';
import {dev, rethrowAsync, userAssert} from './log';
import {disposeServicesForEmbed, getTopWindow} from './service';
import {isDocumentReady} from './document-ready';
import {isExperimentOn} from './experiments';
import {layoutRectLtwh, moveLayoutRect} from './layout-rect';
import {loadPromise} from './event-helper';
import {
Expand Down Expand Up @@ -120,19 +121,24 @@ export function getFriendlyIframeEmbedOptional(iframe) {
* @param {!HTMLIFrameElement} iframe
* @param {!Element} container
* @param {!FriendlyIframeSpec} spec
* @param {function(!Window)=} opt_preinstallCallback
* @param {function(!Window, ?./service/ampdoc-impl.AmpDoc=)=} opt_preinstallCallback
* @return {!Promise<!FriendlyIframeEmbed>}
*/
export function installFriendlyIframeEmbed(
iframe,
container,
spec,
opt_preinstallCallback
opt_preinstallCallback // TODO(#22733): remove "window" argument.
) {
/** @const {!Window} */
const win = getTopWindow(toWin(iframe.ownerDocument.defaultView));
/** @const {!./service/extensions-impl.Extensions} */
const extensions = Services.extensionsFor(win);
const ampdocFieExperimentOn = isExperimentOn(win, 'ampdoc-fie');
/** @const {?./service/ampdoc-impl.AmpDocService} */
const ampdocService = ampdocFieExperimentOn
? Services.ampdocServiceFor(win)
: null;

setStyle(iframe, 'visibility', 'hidden');
iframe.setAttribute('referrerpolicy', 'unsafe-url');
Expand Down Expand Up @@ -211,16 +217,28 @@ export function installFriendlyIframeEmbed(
}

return readyPromise.then(() => {
const embed = new FriendlyIframeEmbed(iframe, spec, loadedPromise);
const childWin = /** @type {!Window} */ (iframe.contentWindow);
const ampdoc =
ampdocFieExperimentOn && ampdocService
? ampdocService.installFieDoc(spec.url, childWin)
: null;
const embed = new FriendlyIframeEmbed(iframe, spec, loadedPromise, ampdoc);
iframe[EMBED_PROP] = embed;

const childWin = /** @type {!Window} */ (iframe.contentWindow);
// Add extensions.
extensions.installExtensionsInChildWindow(
childWin,
spec.extensionIds || [],
opt_preinstallCallback
);
if (ampdoc && ampdocFieExperimentOn) {
extensions.installExtensionsInFie(
ampdoc,
spec.extensionIds || [],
opt_preinstallCallback
);
} else {
extensions.installExtensionsInChildWindow(
childWin,
spec.extensionIds || [],
opt_preinstallCallback
);
}
// Ready to be shown.
embed.startRender_();
return embed;
Expand Down Expand Up @@ -329,14 +347,18 @@ export class FriendlyIframeEmbed {
* @param {!HTMLIFrameElement} iframe
* @param {!FriendlyIframeSpec} spec
* @param {!Promise} loadedPromise
* @param {?./service/ampdoc-impl.AmpDocFie} ampdoc
*/
constructor(iframe, spec, loadedPromise) {
constructor(iframe, spec, loadedPromise, ampdoc) {
/** @const {!HTMLIFrameElement} */
this.iframe = iframe;

/** @const {!Window} */
this.win = /** @type {!Window} */ (iframe.contentWindow);

/** @const {?./service/ampdoc-impl.AmpDocFie} */
this.ampdoc = ampdoc;

/** @const {!FriendlyIframeSpec} */
this.spec = spec;

Expand All @@ -361,6 +383,9 @@ export class FriendlyIframeEmbed {

/** @private @const {!Promise} */
this.winLoadedPromise_ = Promise.all([loadedPromise, this.whenReady()]);
if (this.ampdoc) {
this.whenReady().then(() => this.ampdoc.setReady());
}
}

/**
Expand Down
180 changes: 179 additions & 1 deletion src/service/ampdoc-impl.js
Expand Up @@ -21,7 +21,7 @@ import {getParentWindowFrameElement, registerServiceBuilder} from '../service';
import {getShadowRootNode} from '../shadow-embed';
import {isDocumentReady, whenDocumentReady} from '../document-ready';
import {isExperimentOn} from '../experiments';
import {waitForBodyOpenPromise} from '../dom';
import {rootNodeFor, waitForBodyOpenPromise} from '../dom';

/** @const {string} */
const AMPDOC_PROP = '__AMPDOC';
Expand Down Expand Up @@ -49,10 +49,14 @@ export class AmpDocService {
this.singleDoc_ = null;
if (isSingleDoc) {
this.singleDoc_ = new AmpDocSingle(win);
win.document[AMPDOC_PROP] = this.singleDoc_;
}

/** @private @const */
this.alwaysClosestAmpDoc_ = isExperimentOn(win, 'ampdoc-closest');

/** @private @const */
this.ampdocFieExperimentOn_ = isExperimentOn(win, 'ampdoc-fie');
}

/**
Expand All @@ -61,6 +65,7 @@ export class AmpDocService {
* @return {boolean}
*/
isSingleDoc() {
// TODO(#22733): remove when ampdoc-fie is launched.
return !!this.singleDoc_;
}

Expand All @@ -71,6 +76,8 @@ export class AmpDocService {
* 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.
*
* TODO(#17614): We should always look for the closest AmpDoc (make
* closestAmpDoc always true).
*
Expand All @@ -81,6 +88,44 @@ export class AmpDocService {
* @return {?AmpDoc}
*/
getAmpDocIfAvailable(opt_node = undefined, {closestAmpDoc = false} = {}) {
if (this.ampdocFieExperimentOn_) {
// TODO(#22733): make node not optional.
const node = opt_node;
devAssert(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>.
if (n.ampdoc_) {
return n.ampdoc_;
}

// 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;
}

// Single document: return it immediately.
if (this.singleDoc_ && !closestAmpDoc && !this.alwaysClosestAmpDoc_) {
return this.singleDoc_;
Expand Down Expand Up @@ -147,6 +192,7 @@ export class AmpDocService {
* @return {!AmpDoc}
*/
getAmpDoc(opt_node, opt_options) {
// TODO(#22733): make node not optional.
// 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.
Expand Down Expand Up @@ -182,6 +228,24 @@ export class AmpDocService {
shadowRoot[AMPDOC_PROP] = ampdoc;
return ampdoc;
}

/**
* Creates and installs the ampdoc for the shadow root.
* @param {string} url
* @param {!Window} childWin
* @return {!AmpDocFie}
* @restricted
*/
installFieDoc(url, childWin) {
const doc = childWin.document;
devAssert(!doc[AMPDOC_PROP], 'The fie already contains ampdoc');
const frameElement = /** @type {!Node} */ (devAssert(
getParentWindowFrameElement(doc, this.win)
));
const ampdoc = new AmpDocFie(childWin, url, this.getAmpDoc(frameElement));
doc[AMPDOC_PROP] = ampdoc;
return ampdoc;
}
}

/**
Expand Down Expand Up @@ -212,9 +276,17 @@ export class AmpDoc {
* @return {boolean}
*/
isSingleDoc() {
// TODO(#22733): remove when ampdoc-fie is launched.
return /** @type {?} */ (devAssert(null, 'not implemented'));
}

/**
* @return {?AmpDoc}
*/
getParent() {
return null;
}

/**
* DO NOT CALL. Retained for backward compat during rollout.
* @return {!Window}
Expand Down Expand Up @@ -372,6 +444,11 @@ export class AmpDocSingle extends AmpDoc {
return true;
}

/** @override */
getParent() {
return null;
}

/** @override */
getRootNode() {
return this.win.document;
Expand Down Expand Up @@ -459,6 +536,11 @@ export class AmpDocShadow extends AmpDoc {
return false;
}

/** @override */
getParent() {
return null;
}

/** @override */
getRootNode() {
return this.shadowRoot_;
Expand Down Expand Up @@ -523,6 +605,102 @@ export class AmpDocShadow extends AmpDoc {
}
}

/**
* The version of `AmpDoc` for FIE embeds.
* @package @visibleForTesting
*/
export class AmpDocFie extends AmpDoc {
/**
* @param {!Window} win
* @param {string} url
* @param {!AmpDoc} parent
*/
constructor(win, url, parent) {
super(win);

/** @private @const {string} */
this.url_ = url;

/** @private @const {!AmpDoc} */
this.parent_ = parent;

/** @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 */
getParent() {
return this.parent_;
}

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

/**
* 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
Expand Down

0 comments on commit 4f3d688

Please sign in to comment.