From 682d08c7ebab3c6906968bc1390358b79b669314 Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Thu, 11 Jun 2020 18:40:53 +0300 Subject: [PATCH 1/2] Image block: split huge component --- packages/block-library/src/image/edit.js | 388 ++-------------------- packages/block-library/src/image/image.js | 376 +++++++++++++++++++++ 2 files changed, 405 insertions(+), 359 deletions(-) create mode 100644 packages/block-library/src/image/image.js diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index dd938ee6860d6..3879dbf327cf0 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,18 @@ 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 } - - ); - } - 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' ) ) + } + /> + ) } + + ); +} From 69df279372b95d9754c5ae84ae5434b2f3e36439 Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Thu, 11 Jun 2020 20:09:39 +0300 Subject: [PATCH 2/2] Fix caption focus --- packages/block-library/src/image/edit.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 3879dbf327cf0..2118143b19777 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -268,10 +268,14 @@ export function ImageEdit( { [ `size-${ sizeSlug }` ]: sizeSlug, } ); + // Focussing the image caption after inserting an image relies on the + // component remounting. This needs to be fixed. + const key = !! url; + return ( <> { controls } - + { url && (