Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tweet block for embedding tweets #754

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion blocks/api/categories.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { __ } from 'i18n';
*/
const categories = [
{ slug: 'common', title: __( 'Common Blocks' ) },
{ slug: 'layout', title: __( 'Layout Blocks' ) }
{ slug: 'layout', title: __( 'Layout Blocks' ) },
{ slug: 'social', title: __( 'Social Media' ) }
];

/**
Expand Down
2 changes: 1 addition & 1 deletion blocks/library/embed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ registerBlock( 'core/embed', {
if ( ! url ) {
return (
<Placeholder icon="cloud" label={ wp.i18n.__( 'Embed URL' ) } className="blocks-embed">
<input type="url" className="placeholder__input" placeholder={ wp.i18n.__( 'Enter URL to embed here...' ) } />
<input type="url" className="components-placeholder__input" placeholder={ wp.i18n.__( 'Enter URL to embed' ) } />
<Button isLarge>
{ wp.i18n.__( 'Embed' ) }
</Button>
Expand Down
1 change: 1 addition & 0 deletions blocks/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ import './quote';
import './separator';
import './button';
import './pullquote';
import './tweet';
112 changes: 112 additions & 0 deletions blocks/library/tweet/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* External dependencies
*/
import jQuery from 'jquery';
/**
* WordPress dependencies
*/
import Sandbox from 'components/sandbox';
import Button from 'components/button';
import Placeholder from 'components/placeholder';
/**
* Internal dependencies
*/
import { registerBlock } from '../../api';

registerBlock( 'core/tweet', {
title: wp.i18n.__( 'Tweet' ),
icon: 'twitter',

category: 'social',

attributes( url ) {
return { url };
},

edit: class extends wp.element.Component {
constructor() {
super( ...arguments );
this.fetchTweet = this.fetchTweet.bind( this );
// Copies the block's url so we can edit it without having the block
// update (i.e. refetch the tweet) every time it changes in this edit component.
this.state = {
url: this.props.attributes.url,
html: '',
error: false,
fetching: false,
};
}
doFetch( url ) {
this.setState( { fetching: true, error: false } );
this.fetchXHR = jQuery.ajax( {
type: 'GET',
dataType: 'jsonp',
timeout: 5000,
url: 'https://publish.twitter.com/oembed?url=' + encodeURI( url ),
error: () => this.setState( { fetching: false, error: true } ),
success: ( msg ) => {
this.props.setAttributes( { url } );
this.setState( { fetching: false, error: false, html: msg.html } );
},
} );
}
componentDidMount() {
if ( this.state.url ) {
this.doFetch( this.state.url );
}
}
componentWillUnmount() {
if ( this.fetchXHR ) {
this.fetchXHR.abort();
delete this.fetchXHR;
}
}
fetchTweet( event ) {
const { url } = this.state;
event.preventDefault();
this.doFetch( url );
}
render() {
const { html, url, error, fetching } = this.state;

if ( html ) {
/* translators: %s: url of the tweet */
const title = wp.i18n.sprintf( wp.i18n.__( 'Embedded from %s' ), url );
return (
<Sandbox
html={ html }
title={ title } />
);
}

return (
<Placeholder icon="twitter" label={ wp.i18n.__( 'Twitter' ) } className="blocks-tweet">
<form onSubmit={ this.fetchTweet }>
<input
className="components-placeholder__input"
value={ url }
placeholder={ wp.i18n.__( 'Enter tweet URL to embed...' ) }
onChange={ ( event ) => this.setState( { url: event.target.value } ) } />
{ ! fetching ?
(
<Button
isLarge
type="submit">
{ wp.i18n.__( 'Embed' ) }
</Button>
) : (
<span className="spinner is-active" />
)
}
</form>
{ error && ( <p className="components-placeholder__error">{ wp.i18n.__( 'Sorry, we couldn\'t fetch that tweet.' ) }</p> ) }
</Placeholder>
);
}
},

save( { attributes } ) {
const { url } = attributes;
return url;
}
} );
6 changes: 6 additions & 0 deletions components/placeholder/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
.components-placeholder__fieldset {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
width: 100%;
max-width: 280px;

.components-placeholder__error {
font-family: $default-font;
font-size: $default-font-size;
}
}

.components-placeholder__input {
Expand Down
46 changes: 46 additions & 0 deletions components/resizable-iframe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Resizable Iframe
================

Resizable Iframe is a React component for rendering an `<iframe>` element which can dynamically update its own dimensions using [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage). This is useful in cases where an inline frame of an unknown size is to be displayed on the page.

## Example

The `ResizableIframe` component can be used in much the same way that you would use an `<iframe>` DOM element. Props are automatically transferred to the rendered `<iframe>`, in case you need to specify additional properties or styles.

```html
<ResizableIframe src={ myFrameUrl } frameBorder={ 0 } />
```

## Usage

To allow for resizing of the element, a `ResizableIframe` element listens for `message` events on the `window` object. If the rendered frame is not sandboxed, a script is injected in the frame to manage this behavior on your behalf. If the frame is sandboxed, any page you reference as the `src` URL is responsible for invoking this event using [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage). The message should be a JSON string with an `action` value of "resize" and a numeric `width` and `height` to define the new pixel dimensions of the element.

For example, a page can trigger a resize using the following code snippet:

```javascript
if ( window.parent ) {
window.parent.postMessage( JSON.stringify( {
action: 'resize',
width: document.body.clientWidth,
height: document.body.clientHeight
} ), '*' );
}
```

## Props

### `src`

Treated as the `src` URL to be used in the rendered `<iframe>` DOM element.

### `width`

An optional fixed width value, if you don't want this to be the responsibility of the child window.

### `height`

An optional fixed height value, if you don't want this to be the responsibility of the child window.

### `onResize`

An optional function to trigger when the rendered frame has been resized.
171 changes: 171 additions & 0 deletions components/resizable-iframe/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Imported from Calypso, with some lint fixes and gutenburg specific changes.
*/

/**
* External dependencies
*/
import { omit } from 'lodash';

export default class ResizableIframe extends wp.element.Component {

constructor() {
super( ...arguments );
this.state = {
width: 0,
height: 0
};
this.getFrameBody = this.getFrameBody.bind( this );
this.maybeConnect = this.maybeConnect.bind( this );
this.isFrameAccessible = this.isFrameAccessible.bind( this );
this.checkMessageForResize = this.checkMessageForResize.bind( this );
}

static get defaultProps() {
return {
onLoad: () => {},
onResize: () => {},
title: ''
};
}

componentDidMount() {
window.addEventListener( 'message', this.checkMessageForResize, false );
this.maybeConnect();
}

componentDidUpdate() {
this.maybeConnect();
}

componentWillUnmount() {
window.removeEventListener( 'message', this.checkMessageForResize );
}

getFrameBody() {
return this.iframe.contentDocument.body;
}

maybeConnect() {
if ( ! this.isFrameAccessible() ) {
return;
}

const body = this.getFrameBody();
if ( null !== body.getAttribute( 'data-resizable-iframe-connected' ) ) {
return;
}

const script = document.createElement( 'script' );
script.innerHTML = `
( function() {
var observer;

if ( ! window.MutationObserver || ! document.body || ! window.top ) {
return;
}

function sendResize() {
window.top.postMessage( {
action: 'resize',
width: document.body.offsetWidth,
height: document.body.offsetHeight
}, '*' );
}

observer = new MutationObserver( sendResize );
observer.observe( document.body, {
attributes: true,
attributeOldValue: false,
characterData: true,
characterDataOldValue: false,
childList: true,
subtree: true
} );

window.addEventListener( 'load', sendResize, true );

// Hack: Remove viewport unit styles, as these are relative
// the iframe root and interfere with our mechanism for
// determining the unconstrained page bounds.
function removeViewportStyles( ruleOrNode ) {
[ 'width', 'height', 'minHeight', 'maxHeight' ].forEach( function( style ) {
if ( /^\\d+(vmin|vmax|vh|vw)$/.test( ruleOrNode.style[ style ] ) ) {
ruleOrNode.style[ style ] = '';
}
} );
}

Array.prototype.forEach.call( document.querySelectorAll( '[style]' ), removeViewportStyles );
Array.prototype.forEach.call( document.styleSheets, function( stylesheet ) {
Array.prototype.forEach.call( stylesheet.cssRules || stylesheet.rules, removeViewportStyles );
} );

document.body.style.position = 'absolute';
document.body.setAttribute( 'data-resizable-iframe-connected', '' );

sendResize();
} )();
`;
body.appendChild( script );
}

isFrameAccessible() {
try {
return !! this.getFrameBody();
} catch ( e ) {
return false;
}
}

checkMessageForResize( event ) {
const iframe = this.iframe;

// Attempt to parse the message data as JSON if passed as string
let data = event.data || {};
if ( 'string' === typeof data ) {
try {
data = JSON.parse( data );
} catch ( e ) {} // eslint-disable-line no-empty
}

// Verify that the mounted element is the source of the message
if ( ! iframe || iframe.contentWindow !== event.source ) {
return;
}

// Update the state only if the message is formatted as we expect, i.e.
// as an object with a 'resize' action, width, and height
const { action, width, height } = data;
const { width: oldWidth, height: oldHeight } = this.state;

if ( 'resize' === action && ( oldWidth !== width || oldHeight !== height ) ) {
this.setState( { width, height } );
this.props.onResize();
}
}

onLoad( event ) {
this.maybeConnect();
this.props.onLoad( event );
}

render() {
const omitProps = [ 'onResize' ];

if ( ! this.props.src ) {
omitProps.push( 'src' );
}
return (
<iframe
ref={ ( node ) => this.iframe = node }
title={ this.props.title }
seamless="seamless"
scrolling="no"
{ ...omit( this.props, omitProps ) }
onLoad={ this.onLoad }
width={ this.props.width || this.state.width }
height={ this.props.height || this.state.height } />
);
}
}
Loading