Skip to content

Commit

Permalink
Friendly iframe embed system
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Sep 15, 2016
1 parent f6c073e commit cc78ca5
Show file tree
Hide file tree
Showing 20 changed files with 974 additions and 34 deletions.
96 changes: 96 additions & 0 deletions extensions/amp-ife-test/0.1/amp-ife-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* 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 {addParamToUrl, assertHttpsUrl} from '../../../src/url';
import {ampdocFor} from '../../../src/ampdoc';
import {copyRuntimeStylesToShadowRoot} from '../../../src/shadow-embed';
import {dev} from '../../../src/log';
import {extensionsFor} from '../../../src/extensions';
import {installFriendlyIframeEmbed} from '../../../src/friendly-iframe-embed';
import {isLayoutSizeDefined} from '../../../src/layout';
import {isExperimentOn} from '../../../src/experiments';
import {loadPromise} from '../../../src/event-helper';

/** @const */
const TAG = 'amp-ife-test';

// DO NOT SUBMIT: This class is to simply demonstrate how a friendly iframe
// is created.
class AmpIfeTest extends AMP.BaseElement {

/** @override */
isLayoutSupported(layout) {
return isLayoutSizeDefined(layout);
}

/** @override */
buildCallback() {
/** @const @private {string} */
this.htmlContent_ = this.element.querySelector('template').innerHTML;

/** @const @private {!Element} */
this.container_ = this.element.ownerDocument.createElement('div');
this.applyFillContent(this.container_);
this.element.appendChild(this.container_);

/** @private {?HTMLIFrameElement} */
this.iframe_ = null;
}

/** @override */
layoutCallback() {
const iframe = this.element.ownerDocument.createElement('iframe');
this.iframe_ = iframe;
this.applyFillContent(iframe);

iframe.setAttribute('frameborder', '0');
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('allowtransparency', '');
iframe.setAttribute('scrolling', 'no');

const embedPromise = installFriendlyIframeEmbed(iframe, this.container_, {
url: 'https://acme.org/embed1',
html: this.htmlContent_,
extensionIds: ['amp-image-lightbox'],
fonts: ['https://fonts.googleapis.com/css?family=Roboto'],
});
return embedPromise.then(embed => {
this.embed_ = embed;

// Run some tests.
const ampdocService = ampdocFor(this.win);
const ampdoc = ampdocService.getAmpDoc(this.element);

const img = iframe.contentWindow.document.querySelector('amp-img');
const ampdoc2 = ampdocService.getAmpDoc(img);
console.error('AMPDOC2: ', ampdoc2, ampdoc2 == ampdoc);

const layoutRect = this.getViewport().getLayoutRect(img);
console.error('layoutRect: ', layoutRect.top, layoutRect);

setTimeout(() => {
// console.error('REMOVE ELEMENT');
// img.parentElement.removeChild(img);

console.error('REMOVE IFRAME');
this.container_.removeChild(iframe);
this.embed_.destroy();
}, 3000);
});
}
}

AMP.registerElement(TAG, AmpIfeTest);
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ declareExtension('amp-vine', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-viz-vega', '0.1', true, 'NO_TYPE_CHECK');
declareExtension('amp-google-vrview-image', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-youtube', '0.1', false);
declareExtension('amp-ife-test', '0.1', false, 'NO_TYPE_CHECK');

/**
* @param {string} name
Expand Down
31 changes: 27 additions & 4 deletions src/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,7 @@ export function stubElements(win) {
// If amp-ad and amp-embed haven't been registered, manually register them
// with ElementStub, in case the script to the element is not included.
if (!knownElements['amp-ad'] && !knownElements['amp-embed']) {
win.ampExtendedElements['amp-ad'] = true;
registerElement(win, 'amp-ad', ElementStub);
win.ampExtendedElements['amp-embed'] = true;
registerElement(win, 'amp-embed', ElementStub);
stubLegacyElements(win);
}
}
const list = win.document.querySelectorAll('[custom-element]');
Expand All @@ -166,6 +163,16 @@ export function stubElements(win) {
}
}

/**
* @param {!Window} win
*/
function stubLegacyElements(win) {
win.ampExtendedElements['amp-ad'] = true;
registerElement(win, 'amp-ad', ElementStub);
win.ampExtendedElements['amp-embed'] = true;
registerElement(win, 'amp-embed', ElementStub);
}

/**
* Stub element if not yet known.
* @param {!Window} win
Expand All @@ -182,6 +189,22 @@ export function stubElementIfNotKnown(win, name) {
registerElement(win, name, ElementStub);
}

/**
* Copies the specified element to child window (friendly iframe). This way
* all implementations of the AMP elements are shared between all friendly
* frames.
* @param {!Window} childWin
* @param {string} name
*/
export function copyElementToChildWindow(childWin, name) {
if (!childWin.ampExtendedElements) {
childWin.ampExtendedElements = {};
stubLegacyElements(childWin);
}
childWin.ampExtendedElements[name] = true;
registerElement(childWin, name, knownElements[name] || ElementStub);
}


/**
* Applies layout to the element. Visible for testing only.
Expand Down
15 changes: 15 additions & 0 deletions src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,3 +544,18 @@ export function escapeCssSelectorIdent(win, ident) {
// Polyfill.
return cssEscape(ident);
}


/**
* Returns a frame element if available.
* @param {!Window} win
* @return {?HTMLIFrameElement}
*/
export function getFrameElement(win) {
try {
return win.frameElement;
} catch (e) {
// Ignore the error.
return null;
}
}
172 changes: 172 additions & 0 deletions src/friendly-iframe-embed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* 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 {createElementWithAttributes} from './dom';
import {dev} from './log';
import {extensionsFor} from './extensions';
import {getTopWindow, setParentWindow} from './service';
import {loadPromise} from './event-helper';
import {resourcesForDoc} from './resources';


/**
* Parameters used to create the new "friendly iframe" embed.
* - html: The complete content of an AMP embed, which is itself an AMP
* document. Can include whatever is normally allowed in an AMP document,
* except for AMP `<script>` declarations. Those should be passed as an
* array of `extensionIds`.
* - extensionsIds: An optional array of AMP extension IDs used in this embed.
* - fonts: An optional array of fonts used in this embed.
*
* @typedef {{
* url: string,
* html: string,
* extensionIds: (?Array<string>|undefined),
* fonts: (?Array<string>|undefined),
* }}
*/
export let FriendlyIframeSpec;


/**
* @type {boolean|undefined}
* @visiblefortesting
*/
let srcdocSupported;

/**
* @param {boolean|undefined} val
* @visiblefortesting
*/
export function setSrcdocSupportedForTesting(val) {
srcdocSupported = val;
}

/**
* Returns `true` if the Friendly Iframes are supported.
* @return {boolean}
*/
function isSrcdocSupported() {
if (srcdocSupported === undefined) {
srcdocSupported = 'srcdoc' in HTMLIFrameElement.prototype;
}
return srcdocSupported;
}


/**
* Creates the requested "friendly iframe" embed. Returns the promise that
* will be resolved as soon as the embed is available. The actual
* initialization of the embed will start as soon as the `iframe` is added
* to the DOM.
* @param {!HTMLIFrameElement} iframe
* @param {!Element} container
* @param {!FriendlyIframeSpec} spec
* @return {!Promise<FriendlyIframeEmbed>}
*/
export function installFriendlyIframeEmbed(iframe, container, spec) {
const win = getTopWindow(iframe.ownerDocument.defaultView);
const extensions = extensionsFor(win);

iframe.style.visibility = 'hidden';
iframe.setAttribute('referrerpolicy', 'unsafe-url');

// Pre-load extensions.
if (spec.extensionIds) {
spec.extensionIds.forEach(
extensionId => extensions.loadExtension(extensionId));
}

// Receive the signal when iframe is ready: it's document is formed.
iframe.onload = () => {
// Chrome does not reflect the iframe readystate.
iframe.readyState = 'complete';
};
let readyPromise;
if (isSrcdocSupported()) {
iframe.srcdoc = spec.html;
// TODO(dvoytenko): Look for a way to get a faster call from here.
// Experiments show that the iframe's "load" event is consistently 50-100ms
// later than the contentWindow actually available.
readyPromise = loadPromise(iframe);
container.appendChild(iframe);
} else {
iframe.src = 'about:blank';
container.appendChild(iframe);
const childDoc = iframe.contentWindow.document;
childDoc.open();
childDoc.write(spec.html);
childDoc.close();
readyPromise = Promise.resolve();
}
return readyPromise.then(() => {
const childWin = iframe.contentWindow;
const childDoc = childWin.document;

// Add <BASE> tag.
childDoc.head.appendChild(createElementWithAttributes(
childDoc,
'base',
{'href': spec.url}));

// Load fonts.
if (spec.fonts) {
spec.fonts.forEach(font => {
childDoc.head.appendChild(createElementWithAttributes(
childDoc,
'link', {
'href': font,
'rel': 'stylesheet',
'type': 'text/css',
}));
});
}

// Add extensions.
extensionsFor(win).installExtensionsInChildWindow(
childWin, spec.extensionIds || []);
iframe.style.visibility = '';
return new FriendlyIframeEmbed(iframe, spec);
});
}


/**
*/
export class FriendlyIframeEmbed {

/**
* @param {!HTMLIFrameElement} iframe
* @param {!FriendlyIframeSpec} spec
*/
constructor(iframe, spec) {
/** @const {!HTMLIFrameElement} */
this.iframe = iframe;

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

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

/**
* Ensures that all resources from this iframe have been released.
*/
destroy() {
resourcesForDoc(this.iframe).removeForChildWindow(this.win);
}
}
Loading

0 comments on commit cc78ca5

Please sign in to comment.