-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Enhance the embed block to support all WP_oEmbed embeds #816
Changes from 15 commits
f9d17bc
2723e86
d071080
6fea1e8
830b743
b99e42c
c55973f
540c43c
42172c0
ea6ff87
19f8477
f09a47f
a235861
d934958
316fb7d
58acca1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,8 @@ | |
} | ||
}, | ||
"globals": { | ||
"wp": true | ||
"wp": true, | ||
"wpApiSettings": true | ||
}, | ||
"plugins": [ | ||
"react", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Button, Placeholder } from 'components'; | ||
import { Button, Placeholder, HtmlEmbed, Spinner } from 'components'; | ||
|
||
/** | ||
* Internal dependencies | ||
|
@@ -34,7 +34,6 @@ registerBlock( 'core/embed', { | |
category: 'embed', | ||
|
||
attributes: { | ||
url: attr( 'iframe', 'src' ), | ||
title: attr( 'iframe', 'title' ), | ||
caption: children( 'figcaption' ), | ||
}, | ||
|
@@ -73,52 +72,130 @@ registerBlock( 'core/embed', { | |
} | ||
}, | ||
|
||
edit( { attributes, setAttributes, focus, setFocus } ) { | ||
const { url, title, caption } = attributes; | ||
edit: class extends wp.element.Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.doServerSideRender = this.doServerSideRender.bind( this ); | ||
this.state = { | ||
html: '', | ||
type: '', | ||
error: false, | ||
fetching: false, | ||
}; | ||
this.noPreview = [ | ||
'facebook.com', | ||
]; | ||
if ( this.props.attributes.url ) { | ||
// if the url is already there, we're loading a saved block, so we need to render | ||
this.doServerSideRender(); | ||
} | ||
} | ||
|
||
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...' ) } /> | ||
<Button isLarge> | ||
{ wp.i18n.__( 'Embed' ) } | ||
</Button> | ||
</Placeholder> | ||
componentWillUnmount() { | ||
// can't abort the fetch promise, so let it know we will unmount | ||
this.unmounting = true; | ||
} | ||
|
||
doServerSideRender( event ) { | ||
if ( event ) { | ||
event.preventDefault(); | ||
} | ||
const { url } = this.props.attributes; | ||
const api_url = wpApiSettings.root + 'oembed/1.0/proxy?url=' + encodeURIComponent( url ) + '&_wpnonce=' + wpApiSettings.nonce; | ||
|
||
this.setState( { error: false, fetching: true } ); | ||
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. When loading the page on
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, so this needs to be moved into 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. Maybe. I'd have to look again at where this function is called originally, and if by To a more general point, I think this is a handy reference for when side effects and state changes should occur during component lifecycle: https://gist.github.com/bvaughn/923dffb2cd9504ee440791fade8db5f9 |
||
fetch( api_url, { | ||
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. There is an oEmbed Proxy endpoint now in core which should be used, I believe. See https://core.trac.wordpress.org/ticket/40450 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. For me it fails to embed because this is not the proper path. Needs path to the WP site. https://developer.wordpress.org/reference/functions/rest_url/ 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. Great! I hardcoded the api url here because I didn't know how to discover it. I see that function is how to do it in PHP, is there a JS version too? 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. You'll need the URL and the nonce alike. They can be exported from PHP to JS as was done for Media: https://github.com/WordPress/wordpress-develop/blob/255bd917f2bc3c0d2b324ebbc979ab4147631c06/src/wp-includes/media.php#L3421-L3431 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'm not sure how to access that from JS... do you have an example of the JS side you could link me to? 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. You don't have to get a new one. In WordPress, a nonce is not strictly a number-used-once. It is more like a CSRF token that will expire after a day. You can presume that the WP-API should be responsible for keeping 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. @notnownikki Alternatively, if you need other PHP settings, you can use https://developer.wordpress.org/reference/functions/wp_localize_script/ in WordPress. 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.
Edit: I guess we're interested in the 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'm having a few problems implementing this... If I fetch the URL resulting from
...then I get back "Cookie nonce is invalid". (I've tried to set the none in a header too, but same error.) However, if I visit the URL in a browser, I don't get a nonce error, but I do get a 404, the route isn't found. Can you point me in the right direction? I've been through the REST API Handbook, and it all looks like it should work... 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. Are you running WordPress 4.8-beta2? This is a new endpoint in 4.8. |
||
credentials: 'include', | ||
} ).then( | ||
( response ) => { | ||
if ( this.unmounting ) { | ||
return; | ||
} | ||
response.json().then( ( obj ) => { | ||
const { html, type } = obj; | ||
if ( html ) { | ||
this.setState( { html, type } ); | ||
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. When we do async calls in React components, we need to ensure that the callbacks are not executed if the component has been unmounted before the end of the async process. Which generally means one of these two things:
(Aborting is better when possible) 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. Gah, sorry, I remember you saying this before and I forgot to implement. Will get this done. 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. Ok, |
||
} else { | ||
this.setState( { error: true } ); | ||
} | ||
this.setState( { fetching: false } ); | ||
} ); | ||
} | ||
); | ||
} | ||
|
||
return ( | ||
<figure className="blocks-embed"> | ||
<div className="iframe-overlay"> | ||
<iframe src={ url } title={ title } /> | ||
</div> | ||
{ ( caption && caption.length > 0 ) || !! focus ? ( | ||
<Editable | ||
tagName="figcaption" | ||
placeholder={ wp.i18n.__( 'Write caption…' ) } | ||
value={ caption } | ||
focus={ focus } | ||
onFocus={ setFocus } | ||
onChange={ ( value ) => setAttributes( { caption: value } ) } | ||
inline | ||
inlineToolbar | ||
/> | ||
) : null } | ||
</figure> | ||
); | ||
render() { | ||
const { html, type, error, fetching } = this.state; | ||
const { url, caption } = this.props.attributes; | ||
const { setAttributes, focus, setFocus } = this.props; | ||
|
||
if ( ! html ) { | ||
return ( | ||
<Placeholder icon="cloud" label={ wp.i18n.__( 'Embed URL' ) } className="blocks-embed"> | ||
<form onSubmit={ this.doServerSideRender }> | ||
<input | ||
type="url" | ||
className="components-placeholder__input" | ||
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. Why are we using 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. This was an @aduth request, if I recall correctly. 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. Yes, we are using components as prefix for the whole folder. This is so specific to blocks, though. Mmm. 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. If we are to adhere to the proposed guideline, this should be https://github.com/WordPress/gutenberg/blob/master/docs/coding-guidelines.md#naming 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. If it's truly generic to be considered a component, it should be moved to the 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. No, I agree this is not generic enough and should be |
||
placeholder={ wp.i18n.__( 'Enter URL to embed here...' ) } | ||
onChange={ ( event ) => setAttributes( { url: event.target.value } ) } /> | ||
{ ! fetching | ||
? <Button | ||
isLarge | ||
type="submit"> | ||
{ wp.i18n.__( 'Embed' ) } | ||
</Button> | ||
: <Spinner /> | ||
} | ||
{ error && <p className="components-placeholder__error">{ wp.i18n.__( 'Sorry, we could not embed that content.' ) }</p> } | ||
</form> | ||
</Placeholder> | ||
); | ||
} | ||
|
||
const domain = url.split( '/' )[ 2 ].replace( /^www\./, '' ); | ||
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. This feels a bit fragile, since it could easily throw errors if an unexpected 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. yeah, the host without www. because we want to blacklist certain embeds from previewing in the editor (facebook specifically). I'll look at the url module :) |
||
const cannotPreview = this.noPreview.includes( domain ); | ||
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.
|
||
let typeClassName = 'blocks-embed'; | ||
|
||
if ( 'video' === type ) { | ||
typeClassName = 'blocks-embed-video'; | ||
} | ||
|
||
return ( | ||
<figure className={ typeClassName }> | ||
{ ( cannotPreview ) ? ( | ||
<Placeholder icon="cloud" label={ wp.i18n.__( 'Embed URL' ) }> | ||
<p className="components-placeholder__error"><a href={ url }>{ url }</a></p> | ||
<p className="components-placeholder__error">{ wp.i18n.__( 'Previews for this are unavailable in the editor, sorry!' ) }</p> | ||
</Placeholder> | ||
) : ( | ||
<HtmlEmbed html={ html } /> | ||
) } | ||
{ ( caption && caption.length > 0 ) || !! focus ? ( | ||
<Editable | ||
tagName="figcaption" | ||
placeholder={ wp.i18n.__( 'Write caption…' ) } | ||
value={ caption } | ||
focus={ focus } | ||
onFocus={ setFocus } | ||
onChange={ ( value ) => setAttributes( { caption: value } ) } | ||
inline | ||
inlineToolbar | ||
/> | ||
) : null } | ||
</figure> | ||
); | ||
} | ||
}, | ||
|
||
save( { attributes } ) { | ||
const { url, title, caption } = attributes; | ||
const iframe = <iframe src={ url } title={ title } />; | ||
|
||
const { url, caption } = attributes; | ||
if ( ! caption || ! caption.length ) { | ||
return iframe; | ||
return url; | ||
} | ||
|
||
return ( | ||
<figure> | ||
{ iframe } | ||
{ url } | ||
<figcaption>{ caption }</figcaption> | ||
</figure> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
<!-- wp:core/embed url="https://www.youtube.com/watch?v=Nl6U7UotA-M" --> | ||
<figure><iframe src="//www.youtube.com/embed/Nl6U7UotA-M" frameborder="0" allowfullscreen></iframe><figcaption>State of the Word 2016</figcaption></figure> | ||
<!-- /wp:core/embed --> | ||
<figure>https://www.youtube.com/embed/Nl6U7UotA-M"<figcaption>State of the Word 2016</figcaption></figure> | ||
<!-- /wp:core/embed --> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/*** | ||
* When embedding HTML from the WP oEmbed proxy, we need to insert it | ||
* into a div and make sure any scripts get run. This component takes | ||
* HTML and puts it into a div element, and creates and adds new script | ||
* elements so all scripts get run as expected. | ||
*/ | ||
export default class HtmlEmbed extends wp.element.Component { | ||
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'm not sure I understand what this component is doing, could you clarify a bit? Thanks 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. Also, components need to be exported in 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. Agreed, some comments would be helpful. 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. Comment added explaining the purpose of the component 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. Thanks. Multiline comments (unless jsdoc) should use 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. Fixed! |
||
|
||
componentDidMount() { | ||
const body = this.node; | ||
const { html = '' } = this.props; | ||
|
||
body.innerHTML = html; | ||
|
||
const scripts = body.getElementsByTagName( 'script' ); | ||
const newScripts = Array.from( scripts ).map( ( script ) => { | ||
const newScript = document.createElement( 'script' ); | ||
if ( script.src ) { | ||
newScript.src = script.src; | ||
} else { | ||
newScript.innerHTML = script.innerHTML; | ||
} | ||
return newScript; | ||
} ); | ||
|
||
newScripts.forEach( ( script ) => body.appendChild( script ) ); | ||
} | ||
|
||
render() { | ||
return ( | ||
<div ref={ ( node ) => this.node = node } /> | ||
); | ||
} | ||
} |
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.
Could we extract this ServerSideRender to a HoC? Maybe something like:
Any blocker to achieve this?
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.
The only blocker is I need to read more React docs to figure out how to do it. There were async issues with having Sandbox as the containing block, this seems like a much better way, I've just never done it before.
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.
Maybe this post could help https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9