diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index c947b7c1a6244..cb151819f4c3a 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
-import { get, filter, map, last, omit, pick } from 'lodash';
+import { get, filter, map, last, omit, pick, includes } from 'lodash';
/**
* WordPress dependencies
@@ -18,8 +18,8 @@ import {
ToolbarGroup,
withNotices,
} from '@wordpress/components';
-import { compose } from '@wordpress/compose';
-import { withSelect, withDispatch } from '@wordpress/data';
+import { useViewportMatch } from '@wordpress/compose';
+import { useSelect, useDispatch } from '@wordpress/data';
import {
BlockAlignmentToolbar,
BlockControls,
@@ -33,10 +33,9 @@ import {
__experimentalImageSizeControl as ImageSizeControl,
__experimentalImageURLInputUI as ImageURLInputUI,
} from '@wordpress/block-editor';
-import { Component } from '@wordpress/element';
+import { useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { getPath } from '@wordpress/url';
-import { withViewportMatch } from '@wordpress/viewport';
import { image as icon } from '@wordpress/icons';
import { createBlock } from '@wordpress/blocks';
@@ -45,6 +44,7 @@ import { createBlock } from '@wordpress/blocks';
*/
import { createUpgradedEmbedBlock } from '../embed/util';
import ImageSize from './image-size';
+
/**
* Module constants
*/
@@ -87,80 +87,75 @@ const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
*/
const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url );
-export class ImageEdit extends Component {
- constructor() {
- super( ...arguments );
- this.updateAlt = this.updateAlt.bind( this );
- this.updateAlignment = this.updateAlignment.bind( this );
- this.onFocusCaption = this.onFocusCaption.bind( this );
- this.onImageClick = this.onImageClick.bind( this );
- this.onSelectImage = this.onSelectImage.bind( this );
- this.onSelectURL = this.onSelectURL.bind( this );
- this.updateImage = this.updateImage.bind( this );
- this.onSetHref = this.onSetHref.bind( this );
- this.onSetTitle = this.onSetTitle.bind( this );
- this.getFilename = this.getFilename.bind( this );
- this.onUploadError = this.onUploadError.bind( this );
- this.onImageError = this.onImageError.bind( this );
-
- this.state = {
- captionFocused: false,
- };
+function getFilename( url ) {
+ const path = getPath( url );
+ if ( path ) {
+ return last( path.split( '/' ) );
}
+}
- componentDidMount() {
- const { attributes, mediaUpload, noticeOperations } = this.props;
- const { id, url = '' } = attributes;
-
- if ( isTemporaryImage( id, url ) ) {
- const file = getBlobByURL( url );
-
- if ( file ) {
- mediaUpload( {
- filesList: [ file ],
- onFileChange: ( [ image ] ) => {
- this.onSelectImage( image );
- },
- allowedTypes: ALLOWED_MEDIA_TYPES,
- onError: ( message ) => {
- noticeOperations.createErrorNotice( message );
- },
- } );
- }
- }
+export function ImageEdit( {
+ attributes: {
+ url = '',
+ alt,
+ caption,
+ align,
+ id,
+ href,
+ rel,
+ linkClass,
+ linkDestination,
+ title,
+ width,
+ height,
+ linkTarget,
+ sizeSlug,
+ },
+ setAttributes,
+ isSelected,
+ className,
+ noticeUI,
+ insertBlocksAfter,
+ noticeOperations,
+ onReplace,
+} ) {
+ const { image, maxWidth, isRTL, imageSizes, mediaUpload } = useSelect(
+ ( select ) => {
+ const { getMedia } = select( 'core' );
+ const { getSettings } = select( 'core/block-editor' );
+ return {
+ ...pick( getSettings(), [
+ 'mediaUpload',
+ 'imageSizes',
+ 'isRTL',
+ 'maxWidth',
+ ] ),
+ image: id && isSelected ? getMedia( id ) : null,
+ };
+ },
+ [ id, isSelected ]
+ );
+ const { toggleSelection } = useDispatch( 'core/block-editor' );
+ const isLargeViewport = useViewportMatch( 'medium' );
+ const [ captionFocused, setCaptionFocused ] = useState( false );
+ const isWideAligned = includes( [ 'wide', 'full' ], align );
+
+ function onResizeStart() {
+ toggleSelection( false );
}
- componentDidUpdate( prevProps ) {
- const { id: prevID, url: prevURL = '' } = prevProps.attributes;
- const { id, url = '' } = this.props.attributes;
-
- if (
- isTemporaryImage( prevID, prevURL ) &&
- ! isTemporaryImage( id, url )
- ) {
- revokeBlobURL( url );
- }
-
- if (
- ! this.props.isSelected &&
- prevProps.isSelected &&
- this.state.captionFocused
- ) {
- this.setState( {
- captionFocused: false,
- } );
- }
+ function onResizeStop() {
+ toggleSelection( true );
}
- onUploadError( message ) {
- const { noticeOperations } = this.props;
+ function onUploadError( message ) {
noticeOperations.removeAllNotices();
noticeOperations.createErrorNotice( message );
}
- onSelectImage( media ) {
+ function onSelectImage( media ) {
if ( ! media || ! media.url ) {
- this.props.setAttributes( {
+ setAttributes( {
url: undefined,
alt: undefined,
id: undefined,
@@ -170,14 +165,6 @@ export class ImageEdit extends Component {
return;
}
- const {
- id,
- url,
- alt,
- caption,
- linkDestination,
- } = this.props.attributes;
-
let mediaAttributes = pickRelevantMediaFiles( media );
// If the current image is temporary but an alt text was meanwhile written by the user,
@@ -189,7 +176,7 @@ export class ImageEdit extends Component {
}
// If a caption text was meanwhile written by the user,
- // make sure the text is not overwritten by empty captions
+ // make sure the text is not overwritten by empty captions.
if ( caption && ! get( mediaAttributes, [ 'caption' ] ) ) {
mediaAttributes = omit( mediaAttributes, [ 'caption' ] );
}
@@ -203,7 +190,8 @@ export class ImageEdit extends Component {
sizeSlug: DEFAULT_SIZE_SLUG,
};
} else {
- // Keep the same url when selecting the same file, so "Image Size" option is not changed.
+ // Keep the same url when selecting the same file, so "Image Size"
+ // option is not changed.
additionalAttributes = { url };
}
@@ -219,17 +207,15 @@ export class ImageEdit extends Component {
mediaAttributes.href = media.link;
}
- this.props.setAttributes( {
+ setAttributes( {
...mediaAttributes,
...additionalAttributes,
} );
}
- onSelectURL( newURL ) {
- const { url } = this.props.attributes;
-
+ function onSelectURL( newURL ) {
if ( newURL !== url ) {
- this.props.setAttributes( {
+ setAttributes( {
url: newURL,
id: undefined,
sizeSlug: DEFAULT_SIZE_SLUG,
@@ -237,84 +223,70 @@ export class ImageEdit extends Component {
}
}
- onImageError( url ) {
+ function onImageError() {
// Check if there's an embed block that handles this URL.
const embedBlock = createUpgradedEmbedBlock( { attributes: { url } } );
if ( undefined !== embedBlock ) {
- this.props.onReplace( embedBlock );
+ onReplace( embedBlock );
}
}
- onSetHref( props ) {
- this.props.setAttributes( props );
+ function onSetHref( props ) {
+ setAttributes( props );
}
- onSetTitle( value ) {
- // This is the HTML title attribute, separate from the media object title
- this.props.setAttributes( { title: value } );
+ function onSetTitle( value ) {
+ // This is the HTML title attribute, separate from the media object
+ // title.
+ setAttributes( { title: value } );
}
- onFocusCaption() {
- if ( ! this.state.captionFocused ) {
- this.setState( {
- captionFocused: true,
- } );
+ function onFocusCaption() {
+ if ( ! captionFocused ) {
+ setCaptionFocused( true );
}
}
- onImageClick() {
- if ( this.state.captionFocused ) {
- this.setState( {
- captionFocused: false,
- } );
+ function onImageClick() {
+ if ( captionFocused ) {
+ setCaptionFocused( false );
}
}
- updateAlt( newAlt ) {
- this.props.setAttributes( { alt: newAlt } );
+ function updateAlt( newAlt ) {
+ setAttributes( { alt: newAlt } );
}
- updateAlignment( nextAlign ) {
- const extraUpdatedAttributes =
- [ 'wide', 'full' ].indexOf( nextAlign ) !== -1
- ? { width: undefined, height: undefined }
- : {};
- this.props.setAttributes( {
+ function updateAlignment( nextAlign ) {
+ const extraUpdatedAttributes = isWideAligned
+ ? { width: undefined, height: undefined }
+ : {};
+ setAttributes( {
...extraUpdatedAttributes,
align: nextAlign,
} );
}
- updateImage( sizeSlug ) {
- const { image } = this.props;
-
- const url = get( image, [
+ function updateImage( newSizeSlug ) {
+ const newUrl = get( image, [
'media_details',
'sizes',
- sizeSlug,
+ newSizeSlug,
'source_url',
] );
- if ( ! url ) {
+ if ( ! newUrl ) {
return null;
}
- this.props.setAttributes( {
+ setAttributes( {
url,
width: undefined,
height: undefined,
- sizeSlug,
+ sizeSlug: newSizeSlug,
} );
}
- getFilename( url ) {
- const path = getPath( url );
- if ( path ) {
- return last( path.split( '/' ) );
- }
- }
-
- getImageSizeOptions() {
- const { imageSizes, image } = this.props;
+ function getImageSizeOptions() {
return map(
filter( imageSizes, ( { slug } ) =>
get( image, [ 'media_details', 'sizes', slug, 'source_url' ] )
@@ -323,399 +295,366 @@ export class ImageEdit extends Component {
);
}
- render() {
- const {
- attributes,
- setAttributes,
- isLargeViewport,
- isSelected,
- className,
- maxWidth,
- noticeUI,
- isRTL,
- onResizeStart,
- onResizeStop,
- insertBlocksAfter,
- } = this.props;
- const {
- url,
- alt,
- caption,
- align,
- id,
- href,
- rel,
- linkClass,
- linkDestination,
- title,
- width,
- height,
- linkTarget,
- sizeSlug,
- } = attributes;
-
- const isExternal = isExternalImage( id, url );
- const controls = (
-
-
- { url && (
-
- ) }
- { url && (
-
-
-
- ) }
-
- );
- const src = isExternal ? url : undefined;
- const mediaPreview = !! url && (
-
- );
-
- const mediaPlaceholder = (
- }
- onSelect={ this.onSelectImage }
- onSelectURL={ this.onSelectURL }
- notices={ noticeUI }
- onError={ this.onUploadError }
- accept="image/*"
- allowedTypes={ ALLOWED_MEDIA_TYPES }
- value={ { id, src } }
- mediaPreview={ mediaPreview }
- disableMediaButtons={ url }
- />
- );
+ const isTemp = isTemporaryImage( id, url );
- if ( ! url ) {
- return (
- <>
- { controls }
- { mediaPlaceholder }
- >
- );
+ // Upload a temporary image on mount.
+ useEffect( () => {
+ if ( ! isTemp ) {
+ return;
}
- const classes = classnames( className, {
- 'is-transient': isBlobURL( url ),
- 'is-resized': !! width || !! height,
- 'is-focused': isSelected,
- [ `size-${ sizeSlug }` ]: sizeSlug,
- } );
+ const file = getBlobByURL( url );
+
+ if ( file ) {
+ mediaUpload( {
+ filesList: [ file ],
+ onFileChange: ( [ img ] ) => {
+ onSelectImage( img );
+ },
+ allowedTypes: ALLOWED_MEDIA_TYPES,
+ onError: ( message ) => {
+ noticeOperations.createErrorNotice( message );
+ },
+ } );
+ }
+ }, [] );
- const isResizable =
- [ 'wide', 'full' ].indexOf( align ) === -1 && isLargeViewport;
+ // If an image is temporary, revoke the Blob url when it is uploaded (and is
+ // no longer temporary).
+ useEffect( () => {
+ if ( ! isTemp ) {
+ return;
+ }
- const imageSizeOptions = this.getImageSizeOptions();
+ return () => {
+ revokeBlobURL( url );
+ };
+ }, [ isTemp ] );
- const getInspectorControls = ( imageWidth, imageHeight ) => (
+ useEffect( () => {
+ if ( ! isSelected ) {
+ setCaptionFocused( false );
+ }
+ }, [ isSelected ] );
+
+ const isExternal = isExternalImage( id, url );
+ const controls = (
+
+
+ { url && (
+
+ ) }
+ { url && (
+
+
+
+ ) }
+
+ );
+ const src = isExternal ? url : undefined;
+ const mediaPreview = !! url && (
+
+ );
+
+ const mediaPlaceholder = (
+ }
+ onSelect={ onSelectImage }
+ onSelectURL={ onSelectURL }
+ notices={ noticeUI }
+ onError={ onUploadError }
+ accept="image/*"
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
+ value={ { id, src } }
+ mediaPreview={ mediaPreview }
+ disableMediaButtons={ url }
+ />
+ );
+
+ if ( ! url ) {
+ return (
<>
-
-
-
-
- { __(
- 'Describe the purpose of the image'
- ) }
-
- { __(
- 'Leave empty if the image is purely decorative.'
- ) }
- >
- }
- />
- setAttributes( value ) }
- slug={ sizeSlug }
- width={ width }
- height={ height }
- imageSizeOptions={ imageSizeOptions }
- isResizable={ isResizable }
- imageWidth={ imageWidth }
- imageHeight={ imageHeight }
- />
-
-
-
- { mediaPlaceholder }
+ >
+ );
+ }
+
+ const classes = classnames( className, {
+ 'is-transient': isBlobURL( url ),
+ 'is-resized': !! width || !! height,
+ 'is-focused': isSelected,
+ [ `size-${ sizeSlug }` ]: sizeSlug,
+ } );
+
+ const isResizable = ! isWideAligned && isLargeViewport;
+ const imageSizeOptions = getImageSizeOptions();
+
+ const getInspectorControls = ( imageWidth, imageHeight ) => (
+ <>
+
+
+
- { __(
- 'Describe the role of this image on the page.'
- ) }
-
+
{ __(
- '(Note: many devices and browsers do not display this text.)'
+ 'Describe the purpose of the image'
) }
+ { __(
+ 'Leave empty if the image is purely decorative.'
+ ) }
>
}
/>
-
- >
- );
-
- // Disable reason: Each block can be selected by clicking on it
- /* eslint-disable jsx-a11y/click-events-have-key-events */
- return (
- <>
- { controls }
-
-
- { ( sizes ) => {
- const {
- imageWidthWithinContainer,
- imageHeightWithinContainer,
- imageWidth,
- imageHeight,
- } = sizes;
-
- const filename = this.getFilename( url );
- let defaultedAlt;
- if ( alt ) {
- defaultedAlt = alt;
- } else if ( filename ) {
- defaultedAlt = sprintf(
- /* translators: %s: file name */
- __(
- 'This image has an empty alt attribute; its file name is %s'
- ),
- filename
- );
- } else {
- defaultedAlt = __(
- 'This image has an empty alt attribute'
- );
- }
-
- const img = (
- // Disable reason: Image itself is not meant to be interactive, but
- // should direct focus to block.
- /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
- <>
-
- this.onImageError( url )
- }
- />
- { isBlobURL( url ) && }
- >
- /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
+ setAttributes( value ) }
+ slug={ sizeSlug }
+ width={ width }
+ height={ height }
+ imageSizeOptions={ imageSizeOptions }
+ isResizable={ isResizable }
+ imageWidth={ imageWidth }
+ imageHeight={ imageHeight }
+ />
+
+
+
+
+ { __(
+ 'Describe the role of this image on the page.'
+ ) }
+
+ { __(
+ '(Note: many devices and browsers do not display this text.)'
+ ) }
+
+ >
+ }
+ />
+
+ >
+ );
+
+ // Disable reason: Each block can be selected by clicking on it.
+ /* eslint-disable jsx-a11y/click-events-have-key-events */
+ return (
+ <>
+ { controls }
+
+
+ { ( sizes ) => {
+ const {
+ imageWidthWithinContainer,
+ imageHeightWithinContainer,
+ imageWidth,
+ imageHeight,
+ } = sizes;
+
+ const filename = getFilename( url );
+ let defaultedAlt;
+ if ( alt ) {
+ defaultedAlt = alt;
+ } else if ( filename ) {
+ defaultedAlt = sprintf(
+ /* translators: %s: file name */
+ __(
+ 'This image has an empty alt attribute; its file name is %s'
+ ),
+ filename
);
+ } else {
+ defaultedAlt = __(
+ 'This image has an empty alt attribute'
+ );
+ }
- if (
- ! isResizable ||
- ! imageWidthWithinContainer
- ) {
- return (
- <>
- { getInspectorControls(
- imageWidth,
- imageHeight
- ) }
-
- { img }
-
- >
- );
- }
-
- const currentWidth =
- width || imageWidthWithinContainer;
- const currentHeight =
- height || imageHeightWithinContainer;
-
- const ratio = imageWidth / imageHeight;
- const minWidth =
- imageWidth < imageHeight
- ? MIN_SIZE
- : MIN_SIZE * ratio;
- const minHeight =
- imageHeight < imageWidth
- ? MIN_SIZE
- : MIN_SIZE / ratio;
-
- // With the current implementation of ResizableBox, an image needs an explicit pixel value for the max-width.
- // In absence of being able to set the content-width, this max-width is currently dictated by the vanilla editor style.
- // The following variable adds a buffer to this vanilla style, so 3rd party themes have some wiggleroom.
- // This does, in most cases, allow you to scale the image beyond the width of the main column, though not infinitely.
- // @todo It would be good to revisit this once a content-width variable becomes available.
- const maxWidthBuffer = maxWidth * 2.5;
-
- let showRightHandle = false;
- let showLeftHandle = false;
-
- /* eslint-disable no-lonely-if */
- // See https://github.com/WordPress/gutenberg/issues/7584.
- if ( align === 'center' ) {
- // When the image is centered, show both handles.
- showRightHandle = true;
- showLeftHandle = true;
- } else if ( isRTL ) {
- // In RTL mode the image is on the right by default.
- // Show the right handle and hide the left handle only when it is aligned left.
- // Otherwise always show the left handle.
- if ( align === 'left' ) {
- showRightHandle = true;
- } else {
- showLeftHandle = true;
- }
- } else {
- // Show the left handle and hide the right handle only when the image is aligned right.
- // Otherwise always show the right handle.
- if ( align === 'right' ) {
- showLeftHandle = true;
- } else {
- showRightHandle = true;
- }
- }
- /* eslint-enable no-lonely-if */
+ const img = (
+ // Disable reason: Image itself is not meant to be interactive, but
+ // should direct focus to block.
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+ <>
+ onImageError() }
+ />
+ { isBlobURL( url ) && }
+ >
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
+ );
+ if ( ! isResizable || ! imageWidthWithinContainer ) {
return (
<>
{ getInspectorControls(
imageWidth,
imageHeight
) }
- {
- onResizeStop();
- setAttributes( {
- width: parseInt(
- currentWidth + delta.width,
- 10
- ),
- height: parseInt(
- currentHeight +
- delta.height,
- 10
- ),
- } );
- } }
- >
+
{ img }
-
+
>
);
- } }
-
- { ( ! RichText.isEmpty( caption ) || isSelected ) && (
-
- setAttributes( { caption: value } )
+ }
+
+ const currentWidth = width || imageWidthWithinContainer;
+ const currentHeight =
+ height || imageHeightWithinContainer;
+
+ const ratio = imageWidth / imageHeight;
+ const minWidth =
+ imageWidth < imageHeight
+ ? MIN_SIZE
+ : MIN_SIZE * ratio;
+ const minHeight =
+ imageHeight < imageWidth
+ ? MIN_SIZE
+ : MIN_SIZE / ratio;
+
+ // With the current implementation of ResizableBox, an image needs an explicit pixel value for the max-width.
+ // In absence of being able to set the content-width, this max-width is currently dictated by the vanilla editor style.
+ // The following variable adds a buffer to this vanilla style, so 3rd party themes have some wiggleroom.
+ // This does, in most cases, allow you to scale the image beyond the width of the main column, though not infinitely.
+ // @todo It would be good to revisit this once a content-width variable becomes available.
+ const maxWidthBuffer = maxWidth * 2.5;
+
+ let showRightHandle = false;
+ let showLeftHandle = false;
+
+ /* eslint-disable no-lonely-if */
+ // See https://github.com/WordPress/gutenberg/issues/7584.
+ if ( align === 'center' ) {
+ // When the image is centered, show both handles.
+ showRightHandle = true;
+ showLeftHandle = true;
+ } else if ( isRTL ) {
+ // In RTL mode the image is on the right by default.
+ // Show the right handle and hide the left handle only when it is aligned left.
+ // Otherwise always show the left handle.
+ if ( align === 'left' ) {
+ showRightHandle = true;
+ } else {
+ showLeftHandle = true;
}
- isSelected={ this.state.captionFocused }
- inlineToolbar
- __unstableOnSplitAtEnd={ () =>
- insertBlocksAfter(
- createBlock( 'core/paragraph' )
- )
+ } else {
+ // Show the left handle and hide the right handle only when the image is aligned right.
+ // Otherwise always show the right handle.
+ if ( align === 'right' ) {
+ showLeftHandle = true;
+ } else {
+ showRightHandle = true;
}
- />
- ) }
+ }
+ /* eslint-enable no-lonely-if */
- { mediaPlaceholder }
-
- >
- );
- /* eslint-enable jsx-a11y/click-events-have-key-events */
- }
-}
+ return (
+ <>
+ { getInspectorControls(
+ imageWidth,
+ imageHeight
+ ) }
+ {
+ onResizeStop();
+ setAttributes( {
+ width: parseInt(
+ currentWidth + delta.width,
+ 10
+ ),
+ height: parseInt(
+ currentHeight + delta.height,
+ 10
+ ),
+ } );
+ } }
+ >
+ { img }
+
+ >
+ );
+ } }
+
+ { ( ! RichText.isEmpty( caption ) || isSelected ) && (
+
+ setAttributes( { caption: value } )
+ }
+ isSelected={ captionFocused }
+ inlineToolbar
+ __unstableOnSplitAtEnd={ () =>
+ insertBlocksAfter( createBlock( 'core/paragraph' ) )
+ }
+ />
+ ) }
-export default compose( [
- withDispatch( ( dispatch ) => {
- const { toggleSelection } = dispatch( 'core/block-editor' );
+ { mediaPlaceholder }
+
+ >
+ );
+ /* eslint-enable jsx-a11y/click-events-have-key-events */
+}
- return {
- onResizeStart: () => toggleSelection( false ),
- onResizeStop: () => toggleSelection( true ),
- };
- } ),
- withSelect( ( select, props ) => {
- const { getMedia } = select( 'core' );
- const { getSettings } = select( 'core/block-editor' );
- const {
- attributes: { id },
- isSelected,
- } = props;
- const { mediaUpload, imageSizes, isRTL, maxWidth } = getSettings();
-
- return {
- image: id && isSelected ? getMedia( id ) : null,
- maxWidth,
- isRTL,
- imageSizes,
- mediaUpload,
- };
- } ),
- withViewportMatch( { isLargeViewport: 'medium' } ),
- withNotices,
-] )( ImageEdit );
+export default withNotices( ImageEdit );