-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Embed block refactor and tidy #10958
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b8e02f4
Edit using separate components for each state
notnownikki ddf0f98
Change maybe to upgrade
notnownikki 71a2af3
WordPress embeds do their own iframe thing, don't make them responsive
notnownikki ececfe9
Embed edit components
notnownikki 0733d41
Docs explaining why we need attributes passed in separately
notnownikki f34d9ea
Merge remote-tracking branch 'origin/master' into update/embed-block-…
notnownikki c0eeaf2
Moved class name calculations into a util function
notnownikki c7fdf5c
Tests for class name calculation
notnownikki 3d87a41
Components in separate files, better variable naming for support
notnownikki a6d0d15
Removed components file, removed unneeded editingUrl state change
notnownikki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,31 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { findBlock, isFromWordPress } from './util'; | ||
import { HOSTS_NO_PREVIEWS, ASPECT_RATIOS, DEFAULT_EMBED_BLOCK, WORDPRESS_EMBED_BLOCK } from './constants'; | ||
import { isFromWordPress, createUpgradedEmbedBlock, getClassNames } from './util'; | ||
import EmbedControls from './embed-controls'; | ||
import EmbedLoading from './embed-loading'; | ||
import EmbedPlaceholder from './embed-placeholder'; | ||
import EmbedPreview from './embed-preview'; | ||
|
||
/** | ||
* External dependencies | ||
*/ | ||
import { parse } from 'url'; | ||
import { includes, kebabCase, toLower } from 'lodash'; | ||
import classnames from 'classnames/dedupe'; | ||
import { kebabCase, toLower } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __, _x, sprintf } from '@wordpress/i18n'; | ||
import { Component, renderToString, Fragment } from '@wordpress/element'; | ||
import { | ||
Button, | ||
Placeholder, | ||
Spinner, | ||
SandBox, | ||
IconButton, | ||
Toolbar, | ||
PanelBody, | ||
ToggleControl, | ||
} from '@wordpress/components'; | ||
import { createBlock } from '@wordpress/blocks'; | ||
import { RichText, BlockControls, BlockIcon, InspectorControls } from '@wordpress/editor'; | ||
import { __, sprintf } from '@wordpress/i18n'; | ||
import { Component, Fragment } from '@wordpress/element'; | ||
|
||
export function getEmbedEditComponent( title, icon, responsive = true ) { | ||
return class extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.switchBackToURLInput = this.switchBackToURLInput.bind( this ); | ||
this.setUrl = this.setUrl.bind( this ); | ||
this.maybeSwitchBlock = this.maybeSwitchBlock.bind( this ); | ||
this.getAttributesFromPreview = this.getAttributesFromPreview.bind( this ); | ||
this.setAttributesFromPreview = this.setAttributesFromPreview.bind( this ); | ||
this.setAspectRatioClassNames = this.setAspectRatioClassNames.bind( this ); | ||
this.getResponsiveHelp = this.getResponsiveHelp.bind( this ); | ||
this.toggleResponsive = this.toggleResponsive.bind( this ); | ||
this.handleIncomingPreview = this.handleIncomingPreview.bind( this ); | ||
|
@@ -53,8 +41,15 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { | |
} | ||
|
||
handleIncomingPreview() { | ||
const { allowResponsive } = this.props.attributes; | ||
this.setAttributesFromPreview(); | ||
this.maybeSwitchBlock(); | ||
const upgradedBlock = createUpgradedEmbedBlock( | ||
this.props, | ||
this.getAttributesFromPreview( this.props.preview, allowResponsive ) | ||
); | ||
if ( upgradedBlock ) { | ||
this.props.onReplace( upgradedBlock ); | ||
} | ||
} | ||
|
||
componentDidUpdate( prevProps ) { | ||
|
@@ -63,29 +58,15 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { | |
const switchedPreview = this.props.preview && this.props.attributes.url !== prevProps.attributes.url; | ||
const switchedURL = this.props.attributes.url !== prevProps.attributes.url; | ||
|
||
if ( ( switchedURL || ( hasPreview && ! hadPreview ) ) && this.maybeSwitchBlock() ) { | ||
// Dont do anything if we are going to switch to a different block, | ||
// and we've just changed the URL, or we've just received a preview. | ||
return; | ||
} | ||
|
||
if ( ( hasPreview && ! hadPreview ) || switchedPreview ) { | ||
if ( ( hasPreview && ! hadPreview ) || switchedPreview || switchedURL ) { | ||
if ( this.props.cannotEmbed ) { | ||
// Can't embed this URL, and we've just received or switched the preview. | ||
this.setState( { editingURL: true } ); | ||
return; | ||
} | ||
this.handleIncomingPreview(); | ||
} | ||
} | ||
|
||
getPhotoHtml( photo ) { | ||
// 100% width for the preview so it fits nicely into the document, some "thumbnails" are | ||
// acually the full size photo. | ||
const photoPreview = <p><img src={ photo.thumbnail_url } alt={ photo.title } width="100%" /></p>; | ||
return renderToString( photoPreview ); | ||
} | ||
|
||
setUrl( event ) { | ||
if ( event ) { | ||
event.preventDefault(); | ||
|
@@ -96,114 +77,6 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { | |
setAttributes( { url } ); | ||
} | ||
|
||
/*** | ||
* Switches to a different embed block type, based on the URL | ||
* and the HTML in the preview, if the preview or URL match a different block. | ||
* | ||
* @return {boolean} Whether the block was switched. | ||
*/ | ||
maybeSwitchBlock() { | ||
const { preview } = this.props; | ||
const { url } = this.props.attributes; | ||
|
||
if ( ! url ) { | ||
return false; | ||
} | ||
|
||
const matchingBlock = findBlock( url ); | ||
|
||
// WordPress blocks can work on multiple sites, and so don't have patterns, | ||
// so if we're in a WordPress block, assume the user has chosen it for a WordPress URL. | ||
if ( WORDPRESS_EMBED_BLOCK !== this.props.name && DEFAULT_EMBED_BLOCK !== matchingBlock ) { | ||
// At this point, we have discovered a more suitable block for this url, so transform it. | ||
if ( this.props.name !== matchingBlock ) { | ||
this.props.onReplace( createBlock( matchingBlock, { url } ) ); | ||
return true; | ||
} | ||
} | ||
|
||
if ( preview ) { | ||
const { html } = preview; | ||
|
||
// We can't match the URL for WordPress embeds, we have to check the HTML instead. | ||
if ( isFromWordPress( html ) ) { | ||
// If this is not the WordPress embed block, transform it into one. | ||
if ( WORDPRESS_EMBED_BLOCK !== this.props.name ) { | ||
this.props.onReplace( | ||
createBlock( | ||
WORDPRESS_EMBED_BLOCK, | ||
{ | ||
url, | ||
// By now we have the preview, but when the new block first renders, it | ||
// won't have had all the attributes set, and so won't get the correct | ||
// type and it won't render correctly. So, we work out the attributes | ||
// here so that the initial render works when we switch to the WordPress | ||
// block. This only affects the WordPress block because it can't be | ||
// rendered in the usual Sandbox (it has a sandbox of its own) and it | ||
// relies on the preview to set the correct render type. | ||
...this.getAttributesFromPreview( | ||
this.props.preview, this.props.attributes.allowResponsive | ||
), | ||
} | ||
) | ||
); | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Gets the appropriate CSS class names to enforce an aspect ratio when the embed is resized | ||
* if the HTML has an iframe with width and height set. | ||
* | ||
* @param {string} html The preview HTML that possibly contains an iframe with width and height set. | ||
* @param {boolean} allowResponsive If the classes should be added, or removed. | ||
* @return {Object} Object with classnames set for use with `classnames`. | ||
*/ | ||
getAspectRatioClassNames( html, allowResponsive = true ) { | ||
const previewDocument = document.implementation.createHTMLDocument( '' ); | ||
previewDocument.body.innerHTML = html; | ||
const iframe = previewDocument.body.querySelector( 'iframe' ); | ||
|
||
// If we have a fixed aspect iframe, and it's a responsive embed block. | ||
if ( responsive && iframe && iframe.height && iframe.width ) { | ||
const aspectRatio = ( iframe.width / iframe.height ).toFixed( 2 ); | ||
// Given the actual aspect ratio, find the widest ratio to support it. | ||
for ( let ratioIndex = 0; ratioIndex < ASPECT_RATIOS.length; ratioIndex++ ) { | ||
const potentialRatio = ASPECT_RATIOS[ ratioIndex ]; | ||
if ( aspectRatio >= potentialRatio.ratio ) { | ||
return { | ||
[ potentialRatio.className ]: allowResponsive, | ||
'wp-has-aspect-ratio': allowResponsive, | ||
}; | ||
} | ||
} | ||
} | ||
|
||
return this.props.attributes.className; | ||
} | ||
|
||
/** | ||
* Sets the aspect ratio related class names returned by `getAspectRatioClassNames` | ||
* if `allowResponsive` is truthy. | ||
* | ||
* @param {string} html The preview HTML. | ||
*/ | ||
setAspectRatioClassNames( html ) { | ||
const { allowResponsive } = this.props.attributes; | ||
if ( ! allowResponsive ) { | ||
return; | ||
} | ||
const className = classnames( | ||
this.props.attributes.className, | ||
this.getAspectRatioClassNames( html ) | ||
); | ||
this.props.setAttributes( { className } ); | ||
} | ||
|
||
/*** | ||
* Gets block attributes based on the preview and responsive state. | ||
* | ||
|
@@ -229,10 +102,7 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { | |
attributes.providerNameSlug = providerNameSlug; | ||
} | ||
|
||
attributes.className = classnames( | ||
this.props.attributes.className, | ||
this.getAspectRatioClassNames( html, allowResponsive ) | ||
); | ||
attributes.className = getClassNames( html, this.props.attributes.className, responsive && allowResponsive ); | ||
|
||
return attributes; | ||
} | ||
|
@@ -257,55 +127,24 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { | |
toggleResponsive() { | ||
const { allowResponsive, className } = this.props.attributes; | ||
const { html } = this.props.preview; | ||
const responsiveClassNames = this.getAspectRatioClassNames( html, ! allowResponsive ); | ||
const newAllowResponsive = ! allowResponsive; | ||
|
||
this.props.setAttributes( | ||
{ | ||
allowResponsive: ! allowResponsive, | ||
className: classnames( className, responsiveClassNames ), | ||
allowResponsive: newAllowResponsive, | ||
className: getClassNames( html, className, responsive && newAllowResponsive ), | ||
} | ||
); | ||
} | ||
|
||
render() { | ||
const { url, editingURL } = this.state; | ||
const { caption, type, allowResponsive } = this.props.attributes; | ||
const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, supportsResponsive } = this.props; | ||
const controls = ( | ||
<Fragment> | ||
<BlockControls> | ||
<Toolbar> | ||
{ preview && ! cannotEmbed && ( | ||
<IconButton | ||
className="components-toolbar__control" | ||
label={ __( 'Edit URL' ) } | ||
icon="edit" | ||
onClick={ this.switchBackToURLInput } | ||
/> | ||
) } | ||
</Toolbar> | ||
</BlockControls> | ||
{ supportsResponsive && ( | ||
<InspectorControls> | ||
<PanelBody title={ __( 'Media Settings' ) } className="blocks-responsive"> | ||
<ToggleControl | ||
label={ __( 'Resize for smaller devices' ) } | ||
checked={ allowResponsive } | ||
help={ this.getResponsiveHelp } | ||
onChange={ this.toggleResponsive } | ||
/> | ||
</PanelBody> | ||
</InspectorControls> | ||
) } | ||
</Fragment> | ||
); | ||
const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, themeSupportsResponsive } = this.props; | ||
|
||
if ( fetching ) { | ||
return ( | ||
<div className="wp-block-embed is-loading"> | ||
<Spinner /> | ||
<p>{ __( 'Embedding…' ) }</p> | ||
</div> | ||
<EmbedLoading /> | ||
); | ||
} | ||
|
||
|
@@ -315,68 +154,40 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { | |
// No preview, or we can't embed the current URL, or we've clicked the edit button. | ||
if ( ! preview || cannotEmbed || editingURL ) { | ||
return ( | ||
<Placeholder icon={ <BlockIcon icon={ icon } showColors /> } label={ label } className="wp-block-embed"> | ||
<form onSubmit={ this.setUrl }> | ||
<input | ||
type="url" | ||
value={ url || '' } | ||
className="components-placeholder__input" | ||
aria-label={ label } | ||
placeholder={ __( 'Enter URL to embed here…' ) } | ||
onChange={ ( event ) => this.setState( { url: event.target.value } ) } /> | ||
<Button | ||
isLarge | ||
type="submit"> | ||
{ _x( 'Embed', 'button label' ) } | ||
</Button> | ||
{ cannotEmbed && <p className="components-placeholder__error">{ __( 'Sorry, we could not embed that content.' ) }</p> } | ||
</form> | ||
</Placeholder> | ||
<EmbedPlaceholder | ||
icon={ icon } | ||
label={ label } | ||
onSubmit={ this.setUrl } | ||
value={ url } | ||
cannotEmbed={ cannotEmbed } | ||
onChange={ ( event ) => this.setState( { url: event.target.value } ) } | ||
/> | ||
); | ||
} | ||
|
||
const html = 'photo' === type ? this.getPhotoHtml( preview ) : preview.html; | ||
const { scripts } = preview; | ||
const parsedUrl = parse( url ); | ||
const cannotPreview = includes( HOSTS_NO_PREVIEWS, parsedUrl.host.replace( /^www\./, '' ) ); | ||
// translators: %s: host providing embed content e.g: www.youtube.com | ||
const iframeTitle = sprintf( __( 'Embedded content from %s' ), parsedUrl.host ); | ||
const sandboxClassnames = classnames( type, className ); | ||
const embedWrapper = 'wp-embed' === type ? ( | ||
<div | ||
className="wp-block-embed__wrapper" | ||
dangerouslySetInnerHTML={ { __html: html } } | ||
/> | ||
) : ( | ||
<div className="wp-block-embed__wrapper"> | ||
<SandBox | ||
html={ html } | ||
scripts={ scripts } | ||
title={ iframeTitle } | ||
type={ sandboxClassnames } | ||
/> | ||
</div> | ||
); | ||
|
||
return ( | ||
<figure className={ classnames( className, 'wp-block-embed', { 'is-type-video': 'video' === type } ) }> | ||
{ controls } | ||
{ ( cannotPreview ) ? ( | ||
<Placeholder icon={ <BlockIcon icon={ icon } showColors /> } label={ label }> | ||
<p className="components-placeholder__error"><a href={ url }>{ url }</a></p> | ||
<p className="components-placeholder__error">{ __( 'Previews for this are unavailable in the editor, sorry!' ) }</p> | ||
</Placeholder> | ||
) : embedWrapper } | ||
{ ( ! RichText.isEmpty( caption ) || isSelected ) && ( | ||
<RichText | ||
tagName="figcaption" | ||
placeholder={ __( 'Write caption…' ) } | ||
value={ caption } | ||
onChange={ ( value ) => setAttributes( { caption: value } ) } | ||
inlineToolbar | ||
/> | ||
) } | ||
</figure> | ||
<Fragment> | ||
<EmbedControls | ||
showEditButton={ preview && ! cannotEmbed } | ||
themeSupportsResponsive={ themeSupportsResponsive } | ||
blockSupportsResponsive={ responsive } | ||
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 like these new names 👍 |
||
allowResponsive={ allowResponsive } | ||
getResponsiveHelp={ this.getResponsiveHelp } | ||
toggleResponsive={ this.toggleResponsive } | ||
switchBackToURLInput={ this.switchBackToURLInput } | ||
/> | ||
<EmbedPreview | ||
preview={ preview } | ||
className={ className } | ||
url={ url } | ||
type={ type } | ||
caption={ caption } | ||
onCaptionChange={ ( value ) => setAttributes( { caption: value } ) } | ||
isSelected={ isSelected } | ||
icon={ icon } | ||
label={ label } | ||
/> | ||
</Fragment> | ||
); | ||
} | ||
}; | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
❤️ that these are now separate components, makes the rendering logic so much more understandable.