Skip to content

Commit

Permalink
Online iframe resizing rules with fallback.
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Oct 30, 2015
1 parent 1d7173c commit 69a4fbb
Show file tree
Hide file tree
Showing 14 changed files with 844 additions and 37 deletions.
25 changes: 21 additions & 4 deletions css/amp.css
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ i-amp-scroll-container {
display: block !important;
}

/**
* `-amp-loading-container`, `-amp-loader` and `-amp-loader-dot` all support
* a "loading indicator" usecase. `-amp-loading-container` is mostly responsible
* for alighning the loading indicator, while `-amp-loader` and
* `-amp-loader-dot` are an implementation for a default loading indicator. The
* default implementation includes the three-dot layout and animation.
*/
.-amp-loading-container.amp-hidden {
visibility: hidden;
}
Expand All @@ -216,10 +223,6 @@ i-amp-scroll-container {
white-space: nowrap;
}

/**
* We want to be a bit more specific here as we don't want a global active
* to just trigger the loading animation.
*/
.-amp-loader.amp-active .-amp-loader-dot {
animation: -amp-loader-dots 2s infinite;
}
Expand Down Expand Up @@ -263,6 +266,20 @@ i-amp-scroll-container {
}


/**
* `-amp-overflow` is a support for "overflow" element. This is
* an element shown when more content is available. Typically tapping on this
* element shows the full content.
*/
.-amp-overflow {
z-index: 1;
}

.-amp-overflow.amp-hidden {
visibility: hidden;
}


amp-pixel {
position: absolute !important;
top: 0 !important;
Expand Down
94 changes: 93 additions & 1 deletion extensions/amp-iframe/0.1/amp-iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
* limitations under the License.
*/

import {childElementByAttr} from '../../../src/dom';
import {getLengthNumeral, isLayoutSizeDefined} from '../../../src/layout';
import {loadPromise} from '../../../src/event-helper';
import {log} from '../../../src/log';
import {parseUrl} from '../../../src/url';

/** @const {string} */
const TAG_ = 'AmpIframe';

/** @type {number} */
var count = 0;

/** @const */
var assert = AMP.assert;
const assert = AMP.assert;

class AmpIframe extends AMP.BaseElement {
/** @override */
Expand Down Expand Up @@ -93,16 +97,31 @@ class AmpIframe extends AMP.BaseElement {
this.preconnect.url(this.iframeSrc);
}

/** @override */
buildCallback() {
/** @private {?Element} */
this.overflowElement_ = childElementByAttr(this.element, 'overflow');
if (this.overflowElement_) {
this.overflowElement_.classList.add('-amp-overflow');
this.overflowElement_.classList.toggle('amp-hidden', true);
}
}

/** @override */
layoutCallback() {
this.assertPosition();
if (!this.iframeSrc) {
// This failed already, lets not signal another error.
return Promise.resolve();
}

var width = this.element.getAttribute('width');
var height = this.element.getAttribute('height');
var iframe = document.createElement('iframe');

/** @private @const {!HTMLIFrameElement} */
this.iframe_ = iframe;

this.applyFillContent(iframe);
iframe.width = getLengthNumeral(width);
iframe.height = getLengthNumeral(height);
Expand All @@ -111,15 +130,61 @@ class AmpIframe extends AMP.BaseElement {
// Chrome does not reflect the iframe readystate.
this.readyState = 'complete';
};

/** @private @const {boolean} */
this.isResizable_ = this.element.hasAttribute('resizable');
if (this.isResizable_) {
this.element.setAttribute('scrolling', 'no');
assert(this.overflowElement_,
'Overflow element must be defined for resizable frames: %s',
this.element);
}

/** @const {!Element} */
this.propagateAttributes(
['frameborder', 'allowfullscreen', 'allowtransparency', 'scrolling'],
iframe);
setSandbox(this.element, iframe);
iframe.src = this.iframeSrc;
this.element.appendChild(makeIOsScrollable(this.element, iframe));

listen(iframe, 'embed-size', data => {
if (data.width !== undefined) {
iframe.width = data.width;
this.element.setAttribute('width', data.width);
}
if (data.height !== undefined) {
let newHeight = Math.max(this.element./*OK*/offsetHeight + data.height -
this.iframe_./*OK*/offsetHeight, data.height);
iframe.height = data.height;
this.element.setAttribute('height', newHeight);
this.updateHeight_(newHeight);
}
});
return loadPromise(iframe);
}

/**
* Updates the elements height to accommodate the iframe's requested height.
* @param {number} newHeight
* @private
*/
updateHeight_(newHeight) {
if (!this.isResizable_) {
log.warn(TAG_,
'ignoring embed-size request because this iframe is not resizable',
this.element);
return;
}
this.requestChangeHeight(newHeight, actualHeight => {
assert(this.overflowElement_);
this.overflowElement_.classList.toggle('amp-hidden', false);
this.overflowElement_.onclick = () => {
this.overflowElement_.classList.toggle('amp-hidden', true);
this.changeHeight(actualHeight);
};
});
}
};

/**
Expand Down Expand Up @@ -150,4 +215,31 @@ function makeIOsScrollable(element, iframe) {
return iframe;
}

/**
* Listens for message from the iframe.
* @param {!Element} iframe
* @param {string} typeOfMessage
* @param {function(!Object)} callback
*/
function listen(iframe, typeOfMessage, callback) {
assert(iframe.src, 'only iframes with src supported');
let origin = parseUrl(iframe.src).origin;
let win = iframe.ownerDocument.defaultView;
win.addEventListener('message', function(event) {
if (event.origin != origin) {
return;
}
if (event.source != iframe.contentWindow) {
return;
}
if (!event.data || event.data.sentinel != 'amp') {
return;
}
if (event.data.type != typeOfMessage) {
return;
}
callback(event.data);
});
}

AMP.registerElement('amp-iframe', AmpIframe);
82 changes: 79 additions & 3 deletions extensions/amp-iframe/0.1/test/test-amp-iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ describe('amp-iframe', () => {
if (opt_translateY) {
i.style.transform = 'translateY(' + opt_translateY + ')';
}
if (attributes.resizable !== undefined) {
let overflowEl = iframe.doc.createElement('div');
overflowEl.setAttribute('overflow', '');
i.appendChild(overflowEl);
}
iframe.doc.body.appendChild(i);
// Wait an event loop for the iframe to be created.
return pollForLayout(iframe.win, 1).then(() => {
Expand Down Expand Up @@ -82,6 +87,16 @@ describe('amp-iframe', () => {
});
}

function getAmpIframeObject() {
return getAmpIframe({
src: iframeSrc,
width: 100,
height: 100
}).then(amp => {
return amp.container.implementation_;
});
}

it('should render iframe', () => {
return getAmpIframe({
src: iframeSrc,
Expand Down Expand Up @@ -243,13 +258,74 @@ describe('amp-iframe', () => {
});
});

function getAmpIframeObject() {
it('should listen for resize events', () => {
return getAmpIframe({
src: iframeSrc,
sandbox: 'allow-scripts allow-same-origin',
width: 100,
height: 100,
resizable: ''
}).then(amp => {
let impl = amp.container.implementation_;
impl.layoutCallback();
let p = new Promise((resolve, reject) => {
impl.updateHeight_ = newHeight => {
resolve({amp: amp, newHeight: newHeight});
};
});
amp.iframe.contentWindow.postMessage({
sentinel: 'amp-test',
type: 'requestHeight',
height: 217
}, '*');
return p;
}).then(res => {
expect(res.newHeight).to.equal(217);
expect(res.amp.iframe.height).to.equal('217');
});
});

it('should fallback for resize with overflow element', () => {
return getAmpIframe({
src: iframeSrc,
sandbox: 'allow-scripts',
width: 100,
height: 100,
resizable: ''
}).then(amp => {
let impl = amp.container.implementation_;
impl.requestChangeHeight = sinon.spy();
impl.changeHeight = sinon.spy();
impl.updateHeight_(217);
expect(impl.changeHeight.callCount).to.equal(0);
expect(impl.requestChangeHeight.callCount).to.equal(1);
expect(impl.requestChangeHeight.firstCall.args[0]).to.equal(217);

let fallback = impl.requestChangeHeight.firstCall.args[1];
fallback(219);
expect(impl.overflowElement_).to.not.be.null;
expect(impl.overflowElement_).to.have.class('-amp-overflow');
expect(impl.overflowElement_).to.not.have.class('amp-hidden');
impl.overflowElement_.onclick();
expect(impl.overflowElement_).to.have.class('amp-hidden');
expect(impl.changeHeight.callCount).to.equal(1);
expect(impl.changeHeight.firstCall.args[0]).to.equal(219);
});
});

it('should not resize a non-resizable frame', () => {
return getAmpIframe({
src: iframeSrc,
sandbox: 'allow-scripts',
width: 100,
height: 100
}).then(amp => {
return amp.container.implementation_;
let impl = amp.container.implementation_;
impl.requestChangeHeight = sinon.spy();
impl.changeHeight = sinon.spy();
impl.updateHeight_(217);
expect(impl.changeHeight.callCount).to.equal(0);
expect(impl.requestChangeHeight.callCount).to.equal(0);
});
}
});
});
44 changes: 44 additions & 0 deletions extensions/amp-iframe/amp-iframe.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,47 @@ Example:
**src, srcdoc, sandbox, frameborder, allowfullscreen, allowtransparency**

The attributes above should all behave like they do on standard iframes.


#### IFrame Resizing

An `amp-iframe` must have static layout defined as is the case with any other AMP element. However,
it's possible to resize an `amp-iframe` in runtime. To do so:

1. The `amp-iframe` must be defined with `resizable` attribute;
2. The `amp-iframe` must have `overflow` child element;
3. The IFrame document has to send a `embed-size` request as a window message.

Notice that `resizable` overrides `scrolling` value to `no`.

Example of `amp-iframe` with `overflow` element:
```html
<amp-iframe width=300 height=300
layout="responsive"
sandbox="allow-scripts"
resizable
src="https://foo.com/iframe">
<div overflow tabindex=0 role=button aria-label="Read more">Read more!</div>
</amp-iframe>
```

Example of IFrame resize request:
```javascript
window.parent./*OK*/postMessage({
sentinel: 'amp',
type: 'embed-size',
height: document.body./*OK*/scrollHeight
}, '*');
```

Once this message is received the AMP runtime will try to accommodate this request as soon as
possible, but it will take into account where the reader is currently reading, whether the scrolling
is ongoing and any other UX or performance factors. If the runtime cannot satisfy the resize events
the `amp-iframe` will show an `overflow` element. Clicking on the `overflow` element will immediately
resize the `amp-iframe` since it's triggered by a user action.

Here are some factors that affect how fast the resize will be executed:

- Whether the resize is triggered by the user action;
- Whether the resize is requested for a currently active IFrame;
- Whether the resize is requested for an IFrame below the viewport or above the viewport.
15 changes: 15 additions & 0 deletions src/base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,21 @@ export class BaseElement {
this.resources_.changeHeight(this.element, newHeight);
}

/**
* Requests the runtime to update the height of this element to the specified
* value. The runtime will schedule this request and attempt to process it
* as soon as possible. However, unlike in {@link changeHeight}, the runtime
* may refuse to make a change in which case it will call the provided
* fallback with the height value. The fallback is expected to provide the
* reader with the user action to update the height manually.
* @param {number} newHeight
* @param {function(number)} fallback
* @protected
*/
requestChangeHeight(newHeight, fallback) {
this.resources_.requestChangeHeight(this.element, newHeight, fallback);
}

/**
* Schedules callback to be complete within the next batch. This call is
* intended for heavy DOM mutations that typically cause re-layouts.
Expand Down
5 changes: 2 additions & 3 deletions src/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,10 +488,9 @@ export function createAmpElementProto(win, name, implementationClass) {
// From the moment height is changed the element becomes fully
// responsible for managing its height. Aspect ratio is no longer
// preserved.
this.sizerElement_.style.paddingTop = newHeight + 'px';
} else {
this.style.height = newHeight + 'px';
this.sizerElement_.style.paddingTop = '0';
}
this.style.height = newHeight + 'px';
};

/**
Expand Down
Loading

0 comments on commit 69a4fbb

Please sign in to comment.