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

Embed block refactor and tidy #10958

Merged
merged 10 commits into from
Nov 1, 2018
297 changes: 54 additions & 243 deletions packages/block-library/src/embed/edit.js
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 );
Expand All @@ -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 ) {
Expand All @@ -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();
Expand All @@ -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.
*
Expand All @@ -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;
}
Expand All @@ -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 />
Copy link
Contributor

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.

);
}

Expand All @@ -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 }
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
}
};
Expand Down
Loading