diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index dd938ee6860d6..2118143b19777 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -2,54 +2,35 @@
* External dependencies
*/
import classnames from 'classnames';
-import { get, filter, map, last, omit, pick, includes } from 'lodash';
+import { get, omit, pick } from 'lodash';
/**
* WordPress dependencies
*/
import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
-import {
- ExternalLink,
- PanelBody,
- ResizableBox,
- Spinner,
- TextareaControl,
- TextControl,
- ToolbarGroup,
- withNotices,
-} from '@wordpress/components';
-import { useViewportMatch } from '@wordpress/compose';
-import { useSelect, useDispatch } from '@wordpress/data';
+import { withNotices } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
import {
BlockAlignmentToolbar,
BlockControls,
BlockIcon,
- InspectorControls,
- InspectorAdvancedControls,
MediaPlaceholder,
MediaReplaceFlow,
- RichText,
__experimentalBlock as Block,
- __experimentalImageSizeControl as ImageSizeControl,
- __experimentalImageURLInputUI as ImageURLInputUI,
} from '@wordpress/block-editor';
-import { useEffect, useState, useRef } from '@wordpress/element';
-import { __, sprintf } from '@wordpress/i18n';
-import { getPath } from '@wordpress/url';
+import { useEffect, useRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
import { image as icon } from '@wordpress/icons';
-import { createBlock } from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import { createUpgradedEmbedBlock } from '../embed/util';
-import useImageSize from './image-size';
+import Image from './image';
/**
* Module constants
*/
import {
- MIN_SIZE,
LINK_DESTINATION_MEDIA,
LINK_DESTINATION_ATTACHMENT,
ALLOWED_MEDIA_TYPES,
@@ -87,67 +68,32 @@ const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
*/
const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url );
-function getFilename( url ) {
- const path = getPath( url );
- if ( path ) {
- return last( path.split( '/' ) );
- }
-}
-
export function ImageEdit( {
- attributes: {
+ attributes,
+ setAttributes,
+ isSelected,
+ className,
+ noticeUI,
+ insertBlocksAfter,
+ noticeOperations,
+ onReplace,
+} ) {
+ const {
url = '',
alt,
caption,
align,
id,
- href,
- rel,
- linkClass,
linkDestination,
- title,
width,
height,
- linkTarget,
sizeSlug,
- },
- setAttributes,
- isSelected,
- className,
- noticeUI,
- insertBlocksAfter,
- noticeOperations,
- onReplace,
-} ) {
+ } = attributes;
const ref = useRef();
- 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 );
- }
-
- function onResizeStop() {
- toggleSelection( true );
- }
+ const mediaUpload = useSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+ return getSettings().mediaUpload;
+ } );
function onUploadError( message ) {
noticeOperations.removeAllNotices();
@@ -224,40 +170,6 @@ export function ImageEdit( {
}
}
- function onImageError() {
- // Check if there's an embed block that handles this URL.
- const embedBlock = createUpgradedEmbedBlock( { attributes: { url } } );
- if ( undefined !== embedBlock ) {
- onReplace( embedBlock );
- }
- }
-
- function onSetHref( props ) {
- setAttributes( props );
- }
-
- function onSetTitle( value ) {
- // This is the HTML title attribute, separate from the media object
- // title.
- setAttributes( { title: value } );
- }
-
- function onFocusCaption() {
- if ( ! captionFocused ) {
- setCaptionFocused( true );
- }
- }
-
- function onImageClick() {
- if ( captionFocused ) {
- setCaptionFocused( false );
- }
- }
-
- function updateAlt( newAlt ) {
- setAttributes( { alt: newAlt } );
- }
-
function updateAlignment( nextAlign ) {
const extraUpdatedAttributes = [ 'wide', 'full' ].includes( nextAlign )
? { width: undefined, height: undefined }
@@ -268,34 +180,6 @@ export function ImageEdit( {
} );
}
- function updateImage( newSizeSlug ) {
- const newUrl = get( image, [
- 'media_details',
- 'sizes',
- newSizeSlug,
- 'source_url',
- ] );
- if ( ! newUrl ) {
- return null;
- }
-
- setAttributes( {
- url,
- width: undefined,
- height: undefined,
- sizeSlug: newSizeSlug,
- } );
- }
-
- function getImageSizeOptions() {
- return map(
- filter( imageSizes, ( { slug } ) =>
- get( image, [ 'media_details', 'sizes', slug, 'source_url' ] )
- ),
- ( { name, slug } ) => ( { value: slug, label: name } )
- );
- }
-
const isTemp = isTemporaryImage( id, url );
// Upload a temporary image on mount.
@@ -332,12 +216,6 @@ export function ImageEdit( {
};
}, [ isTemp ] );
- useEffect( () => {
- if ( ! isSelected ) {
- setCaptionFocused( false );
- }
- }, [ isSelected ] );
-
const isExternal = isExternalImage( id, url );
const controls = (
@@ -356,20 +234,6 @@ export function ImageEdit( {
onError={ onUploadError }
/>
) }
- { url && (
-
-
-
- ) }
);
const src = isExternal ? url : undefined;
@@ -397,22 +261,6 @@ export function ImageEdit( {
/>
);
- const {
- imageWidthWithinContainer,
- imageHeightWithinContainer,
- imageWidth,
- imageHeight,
- } = useImageSize( ref, url, [ align ] );
-
- if ( ! url ) {
- return (
- <>
- { controls }
- { mediaPlaceholder }
- >
- );
- }
-
const classes = classnames( className, {
'is-transient': isBlobURL( url ),
'is-resized': !! width || !! height,
@@ -420,196 +268,22 @@ export function ImageEdit( {
[ `size-${ sizeSlug }` ]: sizeSlug,
} );
- const isResizable = ! isWideAligned && isLargeViewport;
- const imageSizeOptions = getImageSizeOptions();
-
- const inspectorControls = (
- <>
-
-
-
-
- { __(
- '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 }
- />
-
-
-
-
- { __(
- 'Describe the role of this image on the page.'
- ) }
-
- { __(
- '(Note: many devices and browsers do not display this text.)'
- ) }
-
- >
- }
- />
-
- >
- );
-
- 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' );
- }
-
- let 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, jsx-a11y/click-events-have-key-events */
- <>
- { inspectorControls }
- onImageError() }
- />
- { isBlobURL( url ) && }
- >
- /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
- );
-
- if ( ! isResizable || ! imageWidthWithinContainer ) {
- img =
{ img }
;
- } else {
- 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 */
-
- img = (
- {
- onResizeStop();
- setAttributes( {
- width: parseInt( currentWidth + delta.width, 10 ),
- height: parseInt( currentHeight + delta.height, 10 ),
- } );
- } }
- >
- { img }
-
- );
- }
+ // Focussing the image caption after inserting an image relies on the
+ // component remounting. This needs to be fixed.
+ const key = !! url;
return (
<>
{ controls }
-
- { img }
- { ( ! RichText.isEmpty( caption ) || isSelected ) && (
-
- setAttributes( { caption: value } )
- }
- isSelected={ captionFocused }
- inlineToolbar
- __unstableOnSplitAtEnd={ () =>
- insertBlocksAfter( createBlock( 'core/paragraph' ) )
- }
+
+ { url && (
+
) }
{ mediaPlaceholder }
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
new file mode 100644
index 0000000000000..1681762027483
--- /dev/null
+++ b/packages/block-library/src/image/image.js
@@ -0,0 +1,376 @@
+/**
+ * External dependencies
+ */
+import { get, filter, map, last, pick, includes } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { isBlobURL } from '@wordpress/blob';
+import {
+ ExternalLink,
+ PanelBody,
+ ResizableBox,
+ Spinner,
+ TextareaControl,
+ TextControl,
+ ToolbarGroup,
+} from '@wordpress/components';
+import { useViewportMatch } from '@wordpress/compose';
+import { useSelect, useDispatch } from '@wordpress/data';
+import {
+ BlockControls,
+ InspectorControls,
+ InspectorAdvancedControls,
+ RichText,
+ __experimentalImageSizeControl as ImageSizeControl,
+ __experimentalImageURLInputUI as ImageURLInputUI,
+} from '@wordpress/block-editor';
+import { useEffect, useState } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+import { getPath } from '@wordpress/url';
+import { createBlock } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { createUpgradedEmbedBlock } from '../embed/util';
+import useImageSize from './image-size';
+
+/**
+ * Module constants
+ */
+import { MIN_SIZE } from './constants';
+
+function getFilename( url ) {
+ const path = getPath( url );
+ if ( path ) {
+ return last( path.split( '/' ) );
+ }
+}
+
+export default function Image( {
+ attributes: {
+ url = '',
+ alt,
+ caption,
+ align,
+ id,
+ href,
+ rel,
+ linkClass,
+ linkDestination,
+ title,
+ width,
+ height,
+ linkTarget,
+ sizeSlug,
+ },
+ setAttributes,
+ isSelected,
+ insertBlocksAfter,
+ onReplace,
+ containerRef,
+} ) {
+ const image = useSelect(
+ ( select ) => {
+ const { getMedia } = select( 'core' );
+ return id && isSelected ? getMedia( id ) : null;
+ },
+ [ id, isSelected ]
+ );
+ const { maxWidth, isRTL, imageSizes } = useSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+ return pick( getSettings(), [ 'imageSizes', 'isRTL', 'maxWidth' ] );
+ } );
+ const { toggleSelection } = useDispatch( 'core/block-editor' );
+ const isLargeViewport = useViewportMatch( 'medium' );
+ const [ captionFocused, setCaptionFocused ] = useState( false );
+ const isWideAligned = includes( [ 'wide', 'full' ], align );
+ const {
+ imageWidthWithinContainer,
+ imageHeightWithinContainer,
+ imageWidth,
+ imageHeight,
+ } = useImageSize( containerRef, url, [ align ] );
+ const isResizable = ! isWideAligned && isLargeViewport;
+ const imageSizeOptions = map(
+ filter( imageSizes, ( { slug } ) =>
+ get( image, [ 'media_details', 'sizes', slug, 'source_url' ] )
+ ),
+ ( { name, slug } ) => ( { value: slug, label: name } )
+ );
+
+ useEffect( () => {
+ if ( ! isSelected ) {
+ setCaptionFocused( false );
+ }
+ }, [ isSelected ] );
+
+ function onResizeStart() {
+ toggleSelection( false );
+ }
+
+ function onResizeStop() {
+ toggleSelection( true );
+ }
+
+ function onImageError() {
+ // Check if there's an embed block that handles this URL.
+ const embedBlock = createUpgradedEmbedBlock( { attributes: { url } } );
+ if ( undefined !== embedBlock ) {
+ onReplace( embedBlock );
+ }
+ }
+
+ function onSetHref( props ) {
+ setAttributes( props );
+ }
+
+ function onSetTitle( value ) {
+ // This is the HTML title attribute, separate from the media object
+ // title.
+ setAttributes( { title: value } );
+ }
+
+ function onFocusCaption() {
+ if ( ! captionFocused ) {
+ setCaptionFocused( true );
+ }
+ }
+
+ function onImageClick() {
+ if ( captionFocused ) {
+ setCaptionFocused( false );
+ }
+ }
+
+ function updateAlt( newAlt ) {
+ setAttributes( { alt: newAlt } );
+ }
+
+ function updateImage( newSizeSlug ) {
+ const newUrl = get( image, [
+ 'media_details',
+ 'sizes',
+ newSizeSlug,
+ 'source_url',
+ ] );
+ if ( ! newUrl ) {
+ return null;
+ }
+
+ setAttributes( {
+ url,
+ width: undefined,
+ height: undefined,
+ sizeSlug: newSizeSlug,
+ } );
+ }
+
+ const controls = (
+ <>
+
+ { url && (
+
+
+
+ ) }
+
+
+
+
+
+ { __(
+ '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 }
+ />
+
+
+
+
+ { __(
+ 'Describe the role of this image on the page.'
+ ) }
+
+ { __(
+ '(Note: many devices and browsers do not display this text.)'
+ ) }
+
+ >
+ }
+ />
+
+ >
+ );
+
+ 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' );
+ }
+
+ let 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, jsx-a11y/click-events-have-key-events */
+ <>
+ { controls }
+ onImageError() }
+ />
+ { isBlobURL( url ) && }
+ >
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
+ );
+
+ if ( ! isResizable || ! imageWidthWithinContainer ) {
+ img = { img }
;
+ } else {
+ 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 */
+
+ img = (
+ {
+ onResizeStop();
+ setAttributes( {
+ width: parseInt( currentWidth + delta.width, 10 ),
+ height: parseInt( currentHeight + delta.height, 10 ),
+ } );
+ } }
+ >
+ { img }
+
+ );
+ }
+
+ return (
+ <>
+ { img }
+ { ( ! RichText.isEmpty( caption ) || isSelected ) && (
+
+ setAttributes( { caption: value } )
+ }
+ isSelected={ captionFocused }
+ inlineToolbar
+ __unstableOnSplitAtEnd={ () =>
+ insertBlocksAfter( createBlock( 'core/paragraph' ) )
+ }
+ />
+ ) }
+ >
+ );
+}