diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 84f220d22b8c3..d5ef390bfa34b 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -1,32 +1,22 @@ /** * 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 { @@ -34,10 +24,8 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { 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 =

{

; - 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,12 +127,12 @@ 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 ), } ); } @@ -270,42 +140,11 @@ export function getEmbedEditComponent( title, icon, responsive = true ) { 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 = ( - - - - { preview && ! cannotEmbed && ( - - ) } - - - { supportsResponsive && ( - - - - - - ) } - - ); + const { fetching, setAttributes, isSelected, className, preview, cannotEmbed, themeSupportsResponsive } = this.props; if ( fetching ) { return ( -
- -

{ __( 'Embedding…' ) }

-
+ ); } @@ -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 ( - } label={ label } className="wp-block-embed"> -
- this.setState( { url: event.target.value } ) } /> - - { cannotEmbed &&

{ __( 'Sorry, we could not embed that content.' ) }

} -
-
+ 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 ? ( -
- ) : ( -
- -
- ); - return ( -
- { controls } - { ( cannotPreview ) ? ( - } label={ label }> -

{ url }

-

{ __( 'Previews for this are unavailable in the editor, sorry!' ) }

-
- ) : embedWrapper } - { ( ! RichText.isEmpty( caption ) || isSelected ) && ( - setAttributes( { caption: value } ) } - inlineToolbar - /> - ) } -
+ + + setAttributes( { caption: value } ) } + isSelected={ isSelected } + icon={ icon } + label={ label } + /> + ); } }; diff --git a/packages/block-library/src/embed/embed-controls.js b/packages/block-library/src/embed/embed-controls.js new file mode 100644 index 0000000000000..e16b91ba7552f --- /dev/null +++ b/packages/block-library/src/embed/embed-controls.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { IconButton, Toolbar, PanelBody, ToggleControl } from '@wordpress/components'; +import { BlockControls, InspectorControls } from '@wordpress/editor'; + +const EmbedControls = ( props ) => { + const { + blockSupportsResponsive, + showEditButton, + themeSupportsResponsive, + allowResponsive, + getResponsiveHelp, + toggleResponsive, + switchBackToURLInput, + } = props; + return ( + + + + { showEditButton && ( + + ) } + + + { themeSupportsResponsive && blockSupportsResponsive && ( + + + + + + ) } + + ); +}; + +export default EmbedControls; diff --git a/packages/block-library/src/embed/embed-loading.js b/packages/block-library/src/embed/embed-loading.js new file mode 100644 index 0000000000000..f643cdc9c1dc1 --- /dev/null +++ b/packages/block-library/src/embed/embed-loading.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Spinner } from '@wordpress/components'; + +const EmbedLoading = () => ( +
+ +

{ __( 'Embedding…' ) }

+
+); + +export default EmbedLoading; diff --git a/packages/block-library/src/embed/embed-placeholder.js b/packages/block-library/src/embed/embed-placeholder.js new file mode 100644 index 0000000000000..1d54279c9027f --- /dev/null +++ b/packages/block-library/src/embed/embed-placeholder.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { Button, Placeholder } from '@wordpress/components'; +import { BlockIcon } from '@wordpress/editor'; + +const EmbedPlaceholder = ( props ) => { + const { icon, label, value, onSubmit, onChange, cannotEmbed } = props; + return ( + } label={ label } className="wp-block-embed"> +
+ + + { cannotEmbed &&

{ __( 'Sorry, we could not embed that content.' ) }

} +
+
+ ); +}; + +export default EmbedPlaceholder; diff --git a/packages/block-library/src/embed/embed-preview.js b/packages/block-library/src/embed/embed-preview.js new file mode 100644 index 0000000000000..b3da26ce75752 --- /dev/null +++ b/packages/block-library/src/embed/embed-preview.js @@ -0,0 +1,69 @@ +/** + * Internal dependencies + */ +import { HOSTS_NO_PREVIEWS } from './constants'; +import { getPhotoHtml } from './util'; + +/** + * External dependencies + */ +import { parse } from 'url'; +import { includes } from 'lodash'; +import classnames from 'classnames/dedupe'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Placeholder, SandBox } from '@wordpress/components'; +import { RichText, BlockIcon } from '@wordpress/editor'; + +const EmbedPreview = ( props ) => { + const { preview, url, type, caption, onCaptionChange, isSelected, className, icon, label } = props; + const { scripts } = preview; + + const html = 'photo' === type ? getPhotoHtml( preview ) : preview.html; + 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, 'wp-block-embed__wrapper' ); + + const embedWrapper = 'wp-embed' === type ? ( +
+ ) : ( +
+ +
+ ); + + return ( +
+ { ( cannotPreview ) ? ( + } label={ label }> +

{ url }

+

{ __( 'Previews for this are unavailable in the editor, sorry!' ) }

+
+ ) : embedWrapper } + { ( ! RichText.isEmpty( caption ) || isSelected ) && ( + + ) } +
+ ); +}; + +export default EmbedPreview; diff --git a/packages/block-library/src/embed/settings.js b/packages/block-library/src/embed/settings.js index 78d8397a71859..35d9b5ce6c4d1 100644 --- a/packages/block-library/src/embed/settings.js +++ b/packages/block-library/src/embed/settings.js @@ -75,7 +75,7 @@ export function getEmbedBlockSettings( { title, description, icon, category = 'e return { preview: validPreview ? preview : undefined, fetching, - supportsResponsive: themeSupports[ 'responsive-embeds' ], + themeSupportsResponsive: themeSupports[ 'responsive-embeds' ], cannotEmbed, }; } ) diff --git a/packages/block-library/src/embed/test/index.js b/packages/block-library/src/embed/test/index.js index ff625b01ae1b4..3931ae7b581e1 100644 --- a/packages/block-library/src/embed/test/index.js +++ b/packages/block-library/src/embed/test/index.js @@ -7,7 +7,7 @@ import { render } from 'enzyme'; * Internal dependencies */ import { getEmbedEditComponent } from '../edit'; -import { findBlock } from '../util'; +import { findBlock, getClassNames } from '../util'; describe( 'core/embed', () => { test( 'block edit matches snapshot', () => { @@ -26,4 +26,22 @@ describe( 'core/embed', () => { expect( findBlock( youtubeURL ) ).toEqual( 'core-embed/youtube' ); expect( findBlock( unknownURL ) ).toEqual( 'core/embed' ); } ); + + test( 'getClassNames returns aspect ratio class names for iframes with width and height', () => { + const html = ''; + const expected = 'wp-embed-aspect-16-9 wp-has-aspect-ratio'; + expect( getClassNames( html ) ).toEqual( expected ); + } ); + + test( 'getClassNames does not return aspect ratio class names if we do not allow responsive', () => { + const html = ''; + const expected = ''; + expect( getClassNames( html, '', false ) ).toEqual( expected ); + } ); + + test( 'getClassNames preserves exsiting class names when removing responsive classes', () => { + const html = ''; + const expected = 'lovely'; + expect( getClassNames( html, 'lovely wp-embed-aspect-16-9 wp-has-aspect-ratio', false ) ).toEqual( expected ); + } ); } ); diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index afb32f81ca79a..58b07b0a62774 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -2,18 +2,25 @@ * Internal dependencies */ import { common, others } from './core-embeds'; -import { DEFAULT_EMBED_BLOCK } from './constants'; +import { DEFAULT_EMBED_BLOCK, WORDPRESS_EMBED_BLOCK, ASPECT_RATIOS } from './constants'; /** * External dependencies */ import { includes } from 'lodash'; +import classnames from 'classnames/dedupe'; + +/** + * WordPress dependencies + */ +import { renderToString } from '@wordpress/element'; +import { createBlock } from '@wordpress/blocks'; /** * Returns true if any of the regular expressions match the URL. * - * @param {string} url The URL to test. - * @param {Array} patterns The list of regular expressions to test agains. + * @param {string} url The URL to test. + * @param {Array} patterns The list of regular expressions to test agains. * @return {boolean} True if any of the regular expressions match the URL. */ export const matchesPatterns = ( url, patterns = [] ) => { @@ -26,7 +33,7 @@ export const matchesPatterns = ( url, patterns = [] ) => { * Finds the block name that should be used for the URL, based on the * structure of the URL. * - * @param {string} url The URL to test. + * @param {string} url The URL to test. * @return {string} The name of the block that should be used for this URL, e.g. core-embed/twitter */ export const findBlock = ( url ) => { @@ -41,3 +48,118 @@ export const findBlock = ( url ) => { export const isFromWordPress = ( html ) => { return includes( html, 'class="wp-embedded-content" data-secret' ); }; + +export const getPhotoHtml = ( photo ) => { + // 100% width for the preview so it fits nicely into the document, some "thumbnails" are + // acually the full size photo. + const photoPreview =

{

; + return renderToString( photoPreview ); +}; + +/*** + * Creates a more suitable embed block based on the passed in props + * and attributes generated from an embed block's preview. + * + * We require `attributesFromPreview` to be generated from the latest attributes + * and preview, and because of the way the react lifecycle operates, we can't + * guarantee that the attributes contained in the block's props are the latest + * versions, so we require that these are generated separately. + * See `getAttributesFromPreview` in the generated embed edit component. + * + * @param {Object} props The block's props. + * @param {Object} attributesFromPreview Attributes generated from the block's most up to date preview. + * @return {Object|undefined} A more suitable embed block if one exists. + */ +export const createUpgradedEmbedBlock = ( props, attributesFromPreview ) => { + const { preview, name } = props; + const { url } = props.attributes; + + if ( ! url ) { + return; + } + + 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 !== name && DEFAULT_EMBED_BLOCK !== matchingBlock ) { + // At this point, we have discovered a more suitable block for this url, so transform it. + if ( name !== matchingBlock ) { + return createBlock( matchingBlock, { url } ); + } + } + + 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 !== name ) { + return 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 pass through the current 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. + ...attributesFromPreview, + } + ); + } + } + } +}; + +/** + * Returns class names with any relevant responsive aspect ratio names. + * + * @param {string} html The preview HTML that possibly contains an iframe with width and height set. + * @param {string} existingClassNames Any existing class names. + * @param {boolean} allowResponsive If the responsive class names should be added, or removed. + * @return {string} Deduped class names. + */ +export function getClassNames( html, existingClassNames = '', allowResponsive = true ) { + if ( ! allowResponsive ) { + // Remove all of the aspect ratio related class names. + const aspectRatioClassNames = { + 'wp-has-aspect-ratio': false, + }; + for ( let ratioIndex = 0; ratioIndex < ASPECT_RATIOS.length; ratioIndex++ ) { + const aspectRatioToRemove = ASPECT_RATIOS[ ratioIndex ]; + aspectRatioClassNames[ aspectRatioToRemove.className ] = false; + } + return classnames( + existingClassNames, + aspectRatioClassNames + ); + } + + 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 ( 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 classnames( + existingClassNames, + { + [ potentialRatio.className ]: allowResponsive, + 'wp-has-aspect-ratio': allowResponsive, + } + ); + } + } + } + + return existingClassNames; +}