Skip to content

Commit 39b1f61

Browse files
authored
✨Add amp-iframe support for Pym.js width and height resize messages (#24917)
* Add amp-iframe support for Pym.js width and height resize messages Fixes #22714. Pym.js supports the client window sending several types of messages, including: * `height` * `width` * `parentPositionInfo` * `navigateTo` * `scrollToChildPos` The only two messages that seem relevant in an AMP context are width and height, as they map to the embed-size message that AMP is currently looking for. * Use listen event-helper instead of addEventListner directly * Rename unlisten_ to unlistenPym_ * Use this.win instead of window * Indicate event param is non-nullable MessageEvent * Use getData event helper * Warn user about unsupported Pym.js messages * Replace function binding with arrow function to fix JSC_TYPE_MISMATCH * Add test for Pym.js messages for height and width * Add missing tag argument to user().warn() * Use startsWith string helper * Replace array splice with direct references; fix format comment doc * Add missing ID from Pym message * Populate undefined variable sentinel
1 parent 7ac50af commit 39b1f61

File tree

3 files changed

+122
-6
lines changed

3 files changed

+122
-6
lines changed

extensions/amp-iframe/0.1/amp-iframe.js

+45-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import {IntersectionObserverApi} from '../../../src/intersection-observer-polyfi
1919
import {LayoutPriority, isLayoutSizeDefined} from '../../../src/layout';
2020
import {Services} from '../../../src/services';
2121
import {base64EncodeFromBytes} from '../../../src/utils/base64.js';
22-
import {createCustomEvent, getData} from '../../../src/event-helper';
22+
import {createCustomEvent, getData, listen} from '../../../src/event-helper';
2323
import {devAssert, user, userAssert} from '../../../src/log';
2424
import {dict} from '../../../src/utils/object';
25-
import {endsWith} from '../../../src/string';
25+
import {endsWith, startsWith} from '../../../src/string';
2626
import {
2727
isAdLike,
2828
isPausable,
@@ -103,6 +103,9 @@ export class AmpIframe extends AMP.BaseElement {
103103
/** @private {string} */
104104
this.sandbox_ = '';
105105

106+
/** @private {Function} */
107+
this.unlistenPym_ = null;
108+
106109
/**
107110
* The source of the iframe. May change to null for tracking iframes
108111
* to prevent them from being recreated.
@@ -470,6 +473,11 @@ export class AmpIframe extends AMP.BaseElement {
470473
/*opt_allowOpaqueOrigin*/ true
471474
);
472475

476+
// Listen for resize messages sent by Pym.js.
477+
this.unlistenPym_ = listen(this.win, 'message', event => {
478+
return this.listenForPymMessage_(/** @type {!MessageEvent} */ (event));
479+
});
480+
473481
if (this.isClickToPlay_) {
474482
listenFor(iframe, 'embed-ready', this.activateIframe_.bind(this));
475483
}
@@ -490,6 +498,37 @@ export class AmpIframe extends AMP.BaseElement {
490498
});
491499
}
492500

501+
/**
502+
* Listen for Pym.js messages for 'height' and 'width'.
503+
*
504+
* @see http://blog.apps.npr.org/pym.js/
505+
* @param {!MessageEvent} event
506+
* @private
507+
*/
508+
listenForPymMessage_(event) {
509+
if (!this.iframe_ || event.source !== this.iframe_.contentWindow) {
510+
return;
511+
}
512+
const data = getData(event);
513+
if (typeof data !== 'string' || !startsWith(data, 'pym')) {
514+
return;
515+
}
516+
517+
// The format of the message takes the form of `pymxPYMx${id}xPYMx${type}xPYMx${message}`.
518+
// The id is unnecessary for integration with amp-iframe; the possible types include
519+
// 'height', 'width', 'parentPositionInfo', 'navigateTo', and 'scrollToChildPos'.
520+
// Only the 'height' and 'width' messages are currently supported.
521+
// See <https://github.com/nprapps/pym.js/blob/57feb68/src/pym.js#L85-L102>
522+
const args = data.split(/xPYMx/);
523+
if ('height' === args[2]) {
524+
this.updateSize_(parseInt(args[3], 10), undefined);
525+
} else if ('width' === args[2]) {
526+
this.updateSize_(undefined, parseInt(args[3], 10));
527+
} else {
528+
user().warn(TAG_, `Unsupported Pym.js message: ${data}`);
529+
}
530+
}
531+
493532
/** @override */
494533
unlayoutOnPause() {
495534
return !this.isPausable_();
@@ -528,6 +567,10 @@ export class AmpIframe extends AMP.BaseElement {
528567
* @override
529568
**/
530569
unlayoutCallback() {
570+
if (this.unlistenPym_) {
571+
this.unlistenPym_();
572+
this.unlistenPym_ = null;
573+
}
531574
if (this.iframe_) {
532575
removeElement(this.iframe_);
533576
if (this.placeholder_) {

extensions/amp-iframe/0.1/test/test-amp-iframe.js

+58
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,64 @@ describes.realWin(
10591059
ActionTrust.HIGH
10601060
);
10611061
});
1062+
1063+
it('should listen for Pym.js height event', function*() {
1064+
const ampIframe = createAmpIframe(env, {
1065+
src: iframeSrc,
1066+
sandbox: 'allow-scripts allow-same-origin',
1067+
width: 200,
1068+
height: 200,
1069+
resizable: '',
1070+
});
1071+
yield waitForAmpIframeLayoutPromise(doc, ampIframe);
1072+
const impl = ampIframe.implementation_;
1073+
return new Promise((resolve, unusedReject) => {
1074+
impl.updateSize_ = (height, width) => {
1075+
resolve({height, width});
1076+
};
1077+
const iframe = ampIframe.querySelector('iframe');
1078+
iframe.contentWindow.postMessage(
1079+
{
1080+
sentinel: 'amp-test',
1081+
type: 'requestPymjsHeight',
1082+
height: 234,
1083+
},
1084+
'*'
1085+
);
1086+
}).then(res => {
1087+
expect(res.height).to.equal(234);
1088+
expect(res.width).to.be.an('undefined');
1089+
});
1090+
});
1091+
1092+
it('should listen for Pym.js width event', function*() {
1093+
const ampIframe = createAmpIframe(env, {
1094+
src: iframeSrc,
1095+
sandbox: 'allow-scripts allow-same-origin',
1096+
width: 200,
1097+
height: 200,
1098+
resizable: '',
1099+
});
1100+
yield waitForAmpIframeLayoutPromise(doc, ampIframe);
1101+
const impl = ampIframe.implementation_;
1102+
return new Promise((resolve, unusedReject) => {
1103+
impl.updateSize_ = (height, width) => {
1104+
resolve({height, width});
1105+
};
1106+
const iframe = ampIframe.querySelector('iframe');
1107+
iframe.contentWindow.postMessage(
1108+
{
1109+
sentinel: 'amp-test',
1110+
type: 'requestPymjsWidth',
1111+
width: 345,
1112+
},
1113+
'*'
1114+
);
1115+
}).then(res => {
1116+
expect(res.width).to.equal(345);
1117+
expect(res.height).to.be.an('undefined');
1118+
});
1119+
});
10621120
});
10631121

10641122
describe('pause/resume', () => {

test/fixtures/served/iframe.html

+19-4
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
}
2626

2727
window.addEventListener('message', function(event) {
28+
var sentinel, msg;
2829
if (event.data) {
29-
if (event.data.type == 'requestHeight') {
30-
var sentinel;
30+
if (event.data.type === 'requestHeight') {
3131
if (event.data.is3p) {
3232
sentinel = event.data.sentinel;
3333
} else {
@@ -40,8 +40,7 @@
4040
height: event.data.height,
4141
width: event.data.width,
4242
}, '*');
43-
} else if (event.data.type == 'subscribeToEmbedState') {
44-
var sentinel;
43+
} else if (event.data.type === 'subscribeToEmbedState') {
4544
if (event.data.is3p) {
4645
sentinel = event.data.sentinel;
4746
} else {
@@ -52,6 +51,22 @@
5251
sentinel: sentinel,
5352
type: 'send-embed-state',
5453
}, '*');
54+
} else if (event.data.type === 'requestPymjsHeight') {
55+
msg = [ 'pym', 'example', 'height', event.data.height ].join( 'xPYMx' );
56+
if (event.data.is3p) {
57+
sentinel = event.data.sentinel;
58+
} else {
59+
sentinel = 'amp';
60+
}
61+
getAmpWindow(sentinel)./*OK*/postMessage( msg, '*');
62+
} else if (event.data.type === 'requestPymjsWidth' || 1) {
63+
msg = [ 'pym', 'example', 'width', event.data.width ].join( 'xPYMx' );
64+
if (event.data.is3p) {
65+
sentinel = event.data.sentinel;
66+
} else {
67+
sentinel = 'amp';
68+
}
69+
getAmpWindow(sentinel)./*OK*/postMessage( msg, '*');
5570
}
5671
}
5772
});

0 commit comments

Comments
 (0)