-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Add tweet block for embedding tweets #754
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
Changes from 1 commit
3cd38d1
abc74cd
5fa3cb8
8c38062
09814e5
eedb7d7
ce8fc70
5fd1bb9
0aad93d
5ce13f6
a43001f
c223f18
ff12c83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,3 +8,4 @@ import './quote'; | |
| import './separator'; | ||
| import './button'; | ||
| import './pullquote'; | ||
| import './tweet'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { registerBlock, query } from '../../api'; | ||
| import Sandbox from '../../../components/sandbox'; | ||
| import Button from '../../../components/button'; | ||
| import Placeholder from '../../../components/placeholder'; | ||
|
|
||
| const { prop } = query; | ||
|
|
||
| registerBlock( 'core/tweet', { | ||
| title: wp.i18n.__( 'Tweet' ), | ||
| icon: 'twitter', | ||
|
|
||
| category: 'social', | ||
|
|
||
| attributes: { | ||
| url: prop( '*', 'innerHTML' ), // our html is just a div with the url in, for WP's oembed to process | ||
|
||
| }, | ||
|
|
||
| edit: class extends wp.element.Component { | ||
| constructor() { | ||
| super( ...arguments ); | ||
| this.fetchTweet = this.fetchTweet.bind( this ); | ||
| this.state = { | ||
| url: this.props.attributes.url, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's discouraged to create copies of props in state because it muddies the fact that the source of truth is the prop itself, leaving you responsible for maintaining when that prop changes. And evidenced by a lack of Instead, where you're currently using And depending on if we want the component to trigger another request if the URL ever changes, it'd be good to bind to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So when the button is clicked (or form submitted) I'd use a reference to the text input to set the url, instead of having it set when the text input changes?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I think I overlooked that. With that in mind, it can be fine to create a copy. Here's an archived (some outdated syntax) article which explains more of the context and highlights this exception:
Since we don't have as much control over the naming of the incoming prop, except if we were to create an intermediary component or rename the attribute (both of which don't seem ideal), I think it'd be fine to ignore the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a comment in the constructor to make it clear what we're doing here. |
||
| html: '', | ||
| error: false, | ||
| fetching: false, | ||
| }; | ||
| } | ||
| doFetch( url, setAttributes, setState ) { | ||
|
||
| setState( { fetching: true, error: false } ); | ||
| jQuery.ajax( { | ||
|
||
| type: 'GET', | ||
| dataType: 'jsonp', | ||
| data: {}, | ||
|
||
| timeout: 5000, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the default
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately we do need the timeout. JSONP requests are horrible. If they fail, they don't call the error callback, so the only way to do it is to specify a large-ish timeout and wait for it to fail.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But is the issue of wanting to avoid waiting a long time for requests specific to this block? I'm curious if and what the default timeout is, or if this should be applied globally through More a consistency question than anything.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to impose a timeout, or the error handling simply does not work. It shouldn't be applied globally though ajaxSetup because this is specific to JSONP requests. From http://api.jquery.com/jQuery.ajax/
There doesn't seem to be a default timeout value. The other way of dealing with errors for cross domain requests is a jQuery plugin, https://github.com/jaubourg/jquery-jsonp , but it doesn't seem to be maintained. |
||
| url: 'https://publish.twitter.com/oembed?url=' + encodeURI( url ), | ||
| error: function() { | ||
| setState( { fetching: false, error: true } ); | ||
|
||
| }, | ||
| success: function( msg ) { | ||
| setAttributes( { url: url } ); | ||
|
||
| setState( { fetching: false, error: false, html: msg.html } ); | ||
| }, | ||
| } ); | ||
| } | ||
| componentDidMount() { | ||
| if ( this.state.url ) { | ||
| this.doFetch( this.state.url, this.props.setAttributes, this.setState.bind( this ) ); | ||
| } | ||
| } | ||
| fetchTweet() { | ||
| const { url } = this.state; | ||
| this.doFetch( url, this.props.setAttributes, this.setState.bind( this ) ); | ||
| } | ||
| render() { | ||
| const { html, url, error, fetching } = this.state; | ||
|
|
||
| if ( ! html ) { | ||
|
||
| return ( | ||
| <Placeholder instructions={ wp.i18n.__( 'Please paste the URL of the tweet here!' ) } icon="twitter" label={ wp.i18n.__( 'Tweet' ) } className="blocks-tweet"> | ||
| <div> | ||
| <input | ||
| type="text" | ||
| value={ url } | ||
| onChange={ ( event ) => this.setState( { url: event.target.value } ) } /> | ||
| { ! fetching ? | ||
| ( | ||
| <Button | ||
| isLarge | ||
| onClick={ this.fetchTweet }> | ||
| { wp.i18n.__( 'Get Tweet' ) } | ||
| </Button> | ||
| ) : ( | ||
| <span className="spinner is-active" /> | ||
| ) | ||
| } | ||
| </div> | ||
| { error && ( <div>{ wp.i18n.__( 'Sorry, we couldn\'t fetch that tweet.' ) }</div> ) } | ||
| </Placeholder> | ||
| ); | ||
| } | ||
| return ( | ||
| <Sandbox html={ html } /> | ||
| ); | ||
| } | ||
| }, | ||
|
|
||
| save( { attributes } ) { | ||
| const { url } = attributes; | ||
| return ( | ||
| <div>{ url }</div> | ||
| ); | ||
| } | ||
| } ); | ||
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| /** | ||
| * Imported from Calypso | ||
| */ | ||
|
|
||
| /** | ||
| * External dependencies | ||
| */ | ||
| import React from 'react'; | ||
|
||
| import debugFactory from 'debug'; | ||
|
||
| import { omit } from 'lodash'; | ||
| import uuid from 'uuid/v4'; | ||
|
|
||
| /** | ||
| * Globals | ||
| */ | ||
| const debug = debugFactory( 'gutenburg:resizable-iframe' ), | ||
| noop = () => {}; | ||
|
|
||
| export default React.createClass( { | ||
|
||
| displayName: 'ResizableIframe', | ||
|
|
||
| propTypes: { | ||
| src: React.PropTypes.string, | ||
| width: React.PropTypes.oneOfType( [ | ||
| React.PropTypes.string, | ||
| React.PropTypes.number | ||
| ] ), | ||
| height: React.PropTypes.oneOfType( [ | ||
| React.PropTypes.string, | ||
| React.PropTypes.number | ||
| ] ), | ||
| onLoad: React.PropTypes.func, | ||
| onResize: React.PropTypes.func, | ||
| title: React.PropTypes.string | ||
| }, | ||
|
|
||
| getInitialState: function() { | ||
| return { width: 0, height: 0 }; | ||
| }, | ||
|
|
||
| getDefaultProps: function() { | ||
| return { | ||
| onLoad: noop, | ||
| onResize: noop, | ||
| title: uuid() | ||
|
||
| }; | ||
| }, | ||
|
|
||
| componentWillMount: function() { | ||
| debug( 'Mounting ' + this.constructor.displayName + ' React component.' ); | ||
| }, | ||
|
|
||
| componentDidMount: function() { | ||
| window.addEventListener( 'message', this.checkMessageForResize, false ); | ||
| this.maybeConnect(); | ||
| }, | ||
|
|
||
| componentDidUpdate: function() { | ||
| this.maybeConnect(); | ||
| }, | ||
|
|
||
| componentWillUnmount: function() { | ||
| window.removeEventListener( 'message', this.checkMessageForResize ); | ||
| }, | ||
|
|
||
| getFrameBody: function() { | ||
| return this.iframe.contentDocument.body; | ||
| }, | ||
|
|
||
| maybeConnect: function() { | ||
| 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: function() { | ||
| try { | ||
| return !! this.getFrameBody(); | ||
| } catch ( e ) { | ||
| return false; | ||
| } | ||
| }, | ||
|
|
||
| checkMessageForResize: function( 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; | ||
| } | ||
|
|
||
| debug( 'Received message: %o', data ); | ||
|
|
||
| // 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: function( event ) { | ||
| this.maybeConnect(); | ||
| this.props.onLoad( event ); | ||
| }, | ||
|
|
||
| render: function() { | ||
| 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 } /> | ||
| ); | ||
| } | ||
| } ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the components directory, the idea becomes that we treat these more like external modules, but unlike npm modules, they're first-party in the sense that they'd be WordPress-maintained.
So two things:
components/sandboxWordPress dependenciesdocblock above this oneThere's some more context documented here, which I'm sensing could probably be moved somewhere more obvious:
https://github.com/WordPress/gutenberg/blob/master/docs/coding-guidelines.md#javascript
Also: #716