Skip to content

Commit

Permalink
make amp-ad resizable
Browse files Browse the repository at this point in the history
  • Loading branch information
camelburrito committed Jan 22, 2016
1 parent 8653046 commit b7d7507
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 5 deletions.
8 changes: 8 additions & 0 deletions 3p/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ window.draw3p = function(opt_configCallback) {
window.context.isMaster = window.context.master == window;
window.context.data = data;
window.context.noContentAvailable = triggerNoContentAvailable;
window.context.resize = triggerResizeRequest;

if (data.type === 'facebook' || data.type === 'twitter') {
// Only make this available to selected embeds until the generic solution is
Expand All @@ -140,6 +141,13 @@ function triggerDimensions(width, height) {
});
}

function triggerResizeRequest(width, height) {
nonSensitiveDataPostMessage('embed-size', {
width: width,
height: height
});
}

function nonSensitiveDataPostMessage(type, opt_object) {
if (window.parent == window) {
return; // Nothing to do.
Expand Down
27 changes: 27 additions & 0 deletions ads/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ Example usage:
});
```

### Ad resizing

Ads can call the special API
`window.context.resize(width, height)` to send a resize request.

Example of resize request:
```javascript
window.parent.postMessage({
sentinel: 'amp-3p',
type: 'embed-size',
height: document.body.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-ad` will show an `overflow` element. Clicking on the `overflow` element will immediately
resize the `amp-ad` 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 ad;
- Whether the resize is requested for an ad below the viewport or above the viewport.


### Minimizing HTTP requests

#### JS reuse across iframes
Expand Down
46 changes: 43 additions & 3 deletions builtins/amp-ad.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import {assert} from '../src/asserts';
import {getIframe, prefetchBootstrap} from '../src/3p-frame';
import {IntersectionObserver} from '../src/intersection-observer';
import {isLayoutSizeDefined} from '../src/layout';
import {listenOnce} from '../src/iframe-helper';
import {listen, listenOnce} from '../src/iframe-helper';
import {loadPromise} from '../src/event-helper';
import {log} from '../src/log';
import {registerElement} from '../src/custom-element';
import {timer} from '../src/timer';

Expand All @@ -31,6 +32,8 @@ const POSITION_FIXED_TAG_WHITELIST = {
'AMP-LIGHTBOX': true
};

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

/**
* @param {!Window} win Destination window for the new element.
Expand Down Expand Up @@ -110,6 +113,9 @@ export function installAd(win) {

/** @private {IntersectionObserver} */
this.intersectionObserver_ = null;

/** @private @const {boolean} */
this.isResizable_ = this.element.hasAttribute('resizable');
}

/**
Expand Down Expand Up @@ -217,6 +223,12 @@ export function installAd(win) {
assert(!this.isInFixedContainer_,
'<amp-ad> is not allowed to be placed in elements with ' +
'position:fixed: %s', this.element);
if (this.isResizable_) {
this.element.setAttribute('scrolling', 'no');
assert(this.getOverflowElement(),
'Overflow element must be defined for resizable ads: %s',
this.element);
}
if (!this.iframe_) {
this.iframe_ = getIframe(this.element.ownerDocument.defaultView,
this.element);
Expand All @@ -227,12 +239,25 @@ export function installAd(win) {
// Triggered by context.noContentAvailable() inside the ad iframe.
listenOnce(this.iframe_, 'no-content', () => {
this.noContentHandler_();
}, /* opt_is3P */true);
}, /* opt_is3P */ true);
// Triggered by context.reportRenderedEntityIdentifier(…) inside the ad
// iframe.
listenOnce(this.iframe_, 'entity-id', info => {
this.element.setAttribute('creative-id', info.id);
}, /* opt_is3P */true);
}, /* opt_is3P */ true);
listen(this.iframe_, 'embed-size', data => {
if (data.width !== undefined) {
this.iframe_.width = data.width;
this.element.setAttribute('width', data.width);
}
if (data.height !== undefined) {
const newHeight = Math.max(this.element./*OK*/offsetHeight +
data.height - this.iframe_./*OK*/offsetHeight, data.height);
this.iframe_.height = data.height;
this.element.setAttribute('height', newHeight);
this.updateHeight_(newHeight);
}
}, /* opt_is3P */ true);
}
return loadPromise(this.iframe_);
}
Expand All @@ -244,6 +269,21 @@ export function installAd(win) {
}
}

/**
* 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 ad is not resizable',
this.element);
return;
}
this.attemptChangeHeight(newHeight);
}

/**
* Activates the fallback if the ad reports that the ad slot cannot
* be filled.
Expand Down
19 changes: 19 additions & 0 deletions builtins/amp-ad.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ Most ad networks require further configuration. This can be passed to the networ

Optional attribute to pass configuration to the ad as an arbitrarily complex JSON object. The object is passed to the ad as-is with no mangling done on the names.

#### Ad Resizing

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

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

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

Example of `amp-ad` with `overflow` element:
```html
<amp-ad width=300 height=300
type="foo"
resizable>
<div overflow tabindex=0 role=button aria-label="Expand!">Expand!</div>
</amp-ad>

#### Placeholder

Optionally `amp-ad` supports a child element with the `placeholder` attribute. If supported by the ad network, this element is shown until the ad is available for viewing.
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/served/iframe.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
window.addEventListener('message', function(event) {
if (event.data && event.data.sentinel == 'amp-test') {
if (event.data.type == 'requestHeight') {
var sentinel = event.data.is3p ? 'amp-3p': 'amp';
parent./*OK*/postMessage({
sentinel: 'amp',
sentinel: sentinel,
type: 'embed-size',
height: event.data.height
}, '*');
Expand Down
73 changes: 72 additions & 1 deletion test/functional/test-amp-ad.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ describe('amp-ad', () => {
for (const key in attributes) {
a.setAttribute(key, attributes[key]);
}
if (attributes.resizable !== undefined) {
const overflowEl = iframe.doc.createElement('div');
overflowEl.setAttribute('overflow', '');
a.appendChild(overflowEl);
}
// Make document long.
a.style.marginBottom = '1000px';
if (opt_handleElement) {
Expand Down Expand Up @@ -155,6 +160,72 @@ describe('amp-ad', () => {
})).to.be.not.be.rejected;
});

describe('ad resize', () => {
it('should listen for resize events',() => {
const iframeSrc = 'http://iframe.localhost:' + location.port +
'/base/test/fixtures/served/iframe.html';
return getAd({
width: 100,
height: 100,
type: 'a9',
src: 'testsrc',
resizable: ''
}, 'https://schema.org').then(element => {
return new Promise((resolve, unusedReject) => {
impl = element.implementation_;
impl.layoutCallback();
impl.updateHeight_ = newHeight => {
expect(newHeight).to.equal(217);
resolve(impl);
};
impl.iframe_.onload = function() {
impl.iframe_.contentWindow.postMessage({
sentinel: 'amp-test',
type: 'requestHeight',
is3p: true,
height: 217
}, '*');
};
impl.iframe_.src = iframeSrc;
});
}).then(impl => {
expect(impl.iframe_.height).to.equal('217');
});
});
it('should fallback for resize with overflow element',() => {
return getAd({
width: 100,
height: 100,
type: 'a9',
src: 'testsrc',
resizable: ''
}, 'https://schema.org').then(element => {
impl = element.implementation_;
impl.attemptChangeHeight = sinon.spy();
impl.changeHeight = sinon.spy();
impl.updateHeight_(217);
expect(impl.changeHeight.callCount).to.equal(0);
expect(impl.attemptChangeHeight.callCount).to.equal(1);
expect(impl.attemptChangeHeight.firstCall.args[0]).to.equal(217);
});
});
it('should not resize a non-resizable ad',() => {
return getAd({
width: 100,
height: 100,
type: 'a9',
src: 'testsrc'
}, 'https://schema.org').then(element => {
impl = element.implementation_;
impl.attemptChangeHeight = sinon.spy();
impl.changeHeight = sinon.spy();
impl.updateHeight_(217);
expect(impl.changeHeight.callCount).to.equal(0);
expect(impl.attemptChangeHeight.callCount).to.equal(0);
});
});
});

describe('ad intersection', () => {

let ampAd;
Expand Down Expand Up @@ -286,7 +357,7 @@ describe('amp-ad', () => {
});
});

it('should collapse when equestChangeHeight succeeds', () => {
it('should collapse when attemptChangeHeight succeeds', () => {
return getAd({
width: 300,
height: 750,
Expand Down

0 comments on commit b7d7507

Please sign in to comment.