Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create skeleton and navigation for FIE ampdocs #22737

Merged
merged 6 commits into from Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {}
dvoytenko marked this conversation as resolved.
Show resolved Hide resolved
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]'
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this code path? I think we can just check tagName inline and call it a day.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. It's too difficult to assume that this API will only be called in the polyfill case. It's a reasonable check for any node. Unfortunately, shadow roots have DOCUMENT_FRAGMENT type and thus difficult to determine.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I mean we could remove this function and just check tagName anywhere we care. Or rename to isPolyfillShadowRoot. Save a few bytes for an unused code path. :)

}

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