diff --git a/.gitignore b/.gitignore index 7ceecfeecd5e..44399320200d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ examples.build examples.min node_modules npm-debug.log +.idea diff --git a/3p/facebook.js b/3p/facebook.js new file mode 100644 index 000000000000..154906154857 --- /dev/null +++ b/3p/facebook.js @@ -0,0 +1,67 @@ +/** + * Copyright 2015 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 {loadScript} from '../src/3p'; +import {assert} from '../src/asserts'; + + +/** + * Produces the Facebook SDK object for the passed in callback. + * + * Note: Facebook SDK fails to render multiple posts when the SDK is only loaded + * in one frame. To Allow the SDK to render them correctly we load the script + * per iframe. + * + * @param {!Window} global + * @param {function(!Object)} cb + */ +function getFacebookSdk(global, cb) { + loadScript(global, 'https://connect.facebook.net/en_US/sdk.js', () => { + cb(global.FB); + }); +} + +/** + * @param {!Window} global + * @param {!Object} data + */ +export function facebook(global, data) { + const embedAs = data.embedAs || 'post'; + assert(['post', 'video'].indexOf(embedAs) !== -1, + 'Attribute data-embed-as for value is wrong, should be' + + ' "post" or "video" was: %s', embedAs); + const fbPost = document.createElement('div'); + fbPost.className = 'fb-' + embedAs; + fbPost.setAttribute('data-href', data.href); + global.document.getElementById('c').appendChild(fbPost); + getFacebookSdk(global, FB => { + // Dimensions are given by the parent frame. + delete data.width; + delete data.height; + + // Only need to listen to post resizing as FB videos have a fixed ratio + // and can automatically resize correctly given the initial width/height. + if (embedAs === 'post') { + FB.Event.subscribe('xfbml.resize', event => { + context.updateDimensions( + parseInt(event.width, 10), + parseInt(event.height, 10) + /* margins */ 20); + }); + } + FB.init({xfbml: true, version: 'v2.5'}); + }); + +} diff --git a/3p/integration.js b/3p/integration.js index 4bbeaa6481fb..eb97c944ad2d 100644 --- a/3p/integration.js +++ b/3p/integration.js @@ -28,6 +28,7 @@ import {adreactor} from '../ads/adreactor'; import {adsense} from '../ads/adsense'; import {adtech} from '../ads/adtech'; import {doubleclick} from '../ads/doubleclick'; +import {facebook} from './facebook'; import {twitter} from './twitter'; import {register, run} from '../src/3p'; import {parseUrl} from '../src/url'; @@ -42,6 +43,7 @@ register('_ping_', function(win, data) { win.document.getElementById('c').textContent = data.ping; }); register('twitter', twitter); +register('facebook', facebook); /** * Visible for testing. @@ -112,12 +114,13 @@ window.draw3p = function(opt_configCallback) { window.context.isMaster = window.context.master == window; window.context.data = data; window.context.noContentAvailable = triggerNoContentAvailable; - if (data.type == 'twitter') { - // Only make this available to Twitter for now while - // https://github.com/ampproject/amphtml/issues/728 - // is being implemented. + + if (data.type === 'facebook' || data.type === 'twitter') { + // Only make this available to selected embeds until the generic solution is + // available. window.context.updateDimensions = triggerDimensions; } + // This only actually works for ads. window.context.observeIntersection = observeIntersection; window.context.reportRenderedEntityIdentifier = diff --git a/DEVELOPING.md b/DEVELOPING.md index 54a942e4bc2d..4609644648d8 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -46,7 +46,7 @@ If you have any questions, feel free to ask on the issue or join us on [Slack](h | `gulp dist` | Builds production binaries. | | `gulp lint` | Validates against Google Closure Linter. | | `gulp lint --watch` | Watches for changes in files, Validates against Google Closure Linter.| -| `gulp lint-fix` | Fixes simple lint warnings/errors automatically. | +| `gulp lint --fix` | Fixes simple lint warnings/errors automatically. | | `gulp build` | Builds the AMP library. | | `gulp clean` | Removes build output. | | `gulp css` | Recompile css to build directory. | diff --git a/examples/facebook.amp.html b/examples/facebook.amp.html new file mode 100644 index 000000000000..35561c716ddf --- /dev/null +++ b/examples/facebook.amp.html @@ -0,0 +1,48 @@ + + + + + Facebook examples + + + + + + + + + + + +

Facebook

+ + + + +

More Posts

+ + + + + + + + + + + + + + + diff --git a/extensions/amp-facebook/0.1/amp-facebook.js b/extensions/amp-facebook/0.1/amp-facebook.js new file mode 100644 index 000000000000..f6282419c112 --- /dev/null +++ b/extensions/amp-facebook/0.1/amp-facebook.js @@ -0,0 +1,56 @@ +/** + * Copyright 2015 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 {getIframe, listen, prefetchBootstrap} from '../../../src/3p-frame'; +import {isLayoutSizeDefined} from '../../../src/layout'; +import {loadPromise} from '../../../src/event-helper'; + + +class AmpFacebook extends AMP.BaseElement { + /** @override */ + preconnectCallback(onLayout) { + this.preconnect.url('https://facebook.com', onLayout); + // Hosts the facebook SDK. + this.preconnect.prefetch('https://connect.facebook.net/en_US/sdk.js'); + prefetchBootstrap(this.getWin()); + } + + /** @override */ + isLayoutSupported(layout) { + return isLayoutSizeDefined(layout); + } + + /** @override */ + layoutCallback() { + const iframe = getIframe(this.element.ownerDocument.defaultView, + this.element, 'facebook'); + this.applyFillContent(iframe); + this.element.appendChild(iframe); + // Triggered by context.updateDimensions() inside the iframe. + listen(iframe, 'embed-size', data => { + iframe.height = data.height; + iframe.width = data.width; + const amp = iframe.parentElement; + amp.setAttribute('height', data.height); + amp.setAttribute('width', data.width); + this./*OK*/changeHeight(data.height); + }); + return loadPromise(iframe); + } +}; + +AMP.registerElement('amp-facebook', AmpFacebook); diff --git a/extensions/amp-facebook/0.1/test/test-amp-facebook.js b/extensions/amp-facebook/0.1/test/test-amp-facebook.js new file mode 100644 index 000000000000..b67c0fbe0283 --- /dev/null +++ b/extensions/amp-facebook/0.1/test/test-amp-facebook.js @@ -0,0 +1,69 @@ +/** + * Copyright 2015 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 {createIframePromise} from '../../../../testing/iframe'; +require('../amp-facebook'); +import {adopt} from '../../../../src/runtime'; + +adopt(window); + +describe('amp-facebook', () => { + + function getFBPost(href, opt_embedAs) { + return createIframePromise().then(iframe => { + const link = document.createElement('link'); + link.setAttribute('rel', 'canonical'); + link.setAttribute('href', 'https://foo.bar/baz'); + iframe.addElement(link); + + const ampFB = iframe.doc.createElement('amp-facebook'); + ampFB.setAttribute('data-href', href); + ampFB.setAttribute('width', '111'); + ampFB.setAttribute('height', '222'); + if (opt_embedAs) { + ampFB.setAttribute('data-embed-as', opt_embedAs); + } + return iframe.addElement(ampFB); + }); + } + + it('renders fb-post', () => { + return getFBPost('https://www.facebook.com/zuck/posts/10102593740125791').then(ampFB => { + const iframe = ampFB.firstChild; + expect(iframe).to.not.be.null; + expect(iframe.tagName).to.equal('IFRAME'); + expect(iframe.getAttribute('width')).to.equal('111'); + expect(iframe.getAttribute('height')).to.equal('222'); + + const fbPost = iframe.getElementsByClassName('fb-post')[0]; + expect(fbPost).not.to.be.null; + }); + }); + + it('renders fb-post', () => { + return getFBPost('https://www.facebook.com/zuck/videos/10102509264909801/', 'video').then(ampFB => { + const iframe = ampFB.firstChild; + expect(iframe).to.not.be.null; + expect(iframe.tagName).to.equal('IFRAME'); + expect(iframe.getAttribute('width')).to.equal('111'); + expect(iframe.getAttribute('height')).to.equal('222'); + + const fbVideo = iframe.getElementsByClassName('fb-video')[0]; + expect(fbVideo).not.to.be.null; + }); + }); + +}); diff --git a/extensions/amp-facebook/amp-facebook.md b/extensions/amp-facebook/amp-facebook.md new file mode 100644 index 000000000000..c008789699ab --- /dev/null +++ b/extensions/amp-facebook/amp-facebook.md @@ -0,0 +1,51 @@ + + +### `amp-facebook` + +Displays a Facebook Post or Video. + +Example - Embedding a post: +```html + + +``` + +Example - Embedding a video: +```html + + +``` + + +#### Attributes + +**data-href** + +The URL of the facebook post/video. For example: https://www.facebook.com/zuck/posts/10102593740125791. + +**data-embed-as** +_Optional_ +Either `post` or `video` (default: `post`). + +Both posts and videos can be embedded as a post. Setting `data-embed-as="video"` for Facebook videos only embed the player of the video ignoring the accompanying post card with it. This is recommended if you'd like a better aspect ratio management for the video to be responsive. + +Checkout the documentation for differences between [post embeds](https://developers.facebook.com/docs/plugins/embedded-posts) and [video embeds](https://developers.facebook.com/docs/plugins/embedded-video-player). diff --git a/gulpfile.js b/gulpfile.js index d49e8d39dac7..057cd5a34b82 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -76,6 +76,7 @@ function buildExtensions(options) { buildExtension('amp-brightcove', '0.1', false, options); buildExtension('amp-carousel', '0.1', true, options); buildExtension('amp-dynamic-css-classes', '0.1', false, options); + buildExtension('amp-facebook', '0.1', false, options); buildExtension('amp-fit-text', '0.1', true, options); buildExtension('amp-font', '0.1', false, options); buildExtension('amp-iframe', '0.1', false, options); @@ -319,6 +320,7 @@ function buildExamples(watch) { buildExample('metadata-examples/video-microdata.amp.html'); buildExample('everything.amp.html'); buildExample('font.amp.html'); + buildExample('facebook.amp.html'); buildExample('instagram.amp.html'); buildExample('pinterest.amp.html'); buildExample('released.amp.html');