@wordpress/block-library
Version:
Block library for the WordPress editor.
387 lines (350 loc) • 9.6 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
import { get, isEmpty, pick } from 'lodash';
/**
* WordPress dependencies
*/
import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
import { Placeholder } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import {
BlockAlignmentControl,
BlockControls,
BlockIcon,
MediaPlaceholder,
useBlockProps,
store as blockEditorStore,
__experimentalUseBorderProps as useBorderProps,
} from '@wordpress/block-editor';
import { useEffect, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { image as icon } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
import Image from './image';
// Much of this description is duplicated from MediaPlaceholder.
const placeholder = ( content ) => {
return (
<Placeholder
className="block-editor-media-placeholder"
withIllustration={ true }
icon={ icon }
label={ __( 'Image' ) }
instructions={ __(
'Upload an image file, pick one from your media library, or add one with a URL.'
) }
>
{ content }
</Placeholder>
);
};
/**
* Module constants
*/
import {
LINK_DESTINATION_ATTACHMENT,
LINK_DESTINATION_CUSTOM,
LINK_DESTINATION_MEDIA,
LINK_DESTINATION_NONE,
ALLOWED_MEDIA_TYPES,
} from './constants';
export const pickRelevantMediaFiles = ( image, size ) => {
const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] );
imageProps.url =
get( image, [ 'sizes', size, 'url' ] ) ||
get( image, [ 'media_details', 'sizes', size, 'source_url' ] ) ||
image.url;
return imageProps;
};
/**
* Is the URL a temporary blob URL? A blob URL is one that is used temporarily
* while the image is being uploaded and will not have an id yet allocated.
*
* @param {number=} id The id of the image.
* @param {string=} url The url of the image.
*
* @return {boolean} Is the URL a Blob URL
*/
const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
/**
* Is the url for the image hosted externally. An externally hosted image has no
* id and is not a blob url.
*
* @param {number=} id The id of the image.
* @param {string=} url The url of the image.
*
* @return {boolean} Is the url an externally hosted url?
*/
export const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url );
/**
* Checks if WP generated default image size. Size generation is skipped
* when the image is smaller than the said size.
*
* @param {Object} image
* @param {string} defaultSize
*
* @return {boolean} Whether or not it has default image size.
*/
function hasDefaultSize( image, defaultSize ) {
return (
'url' in ( image?.sizes?.[ defaultSize ] ?? {} ) ||
'source_url' in ( image?.media_details?.sizes?.[ defaultSize ] ?? {} )
);
}
export function ImageEdit( {
attributes,
setAttributes,
isSelected,
className,
insertBlocksAfter,
onReplace,
context,
clientId,
} ) {
const {
url = '',
alt,
caption,
align,
id,
width,
height,
sizeSlug,
} = attributes;
const [ temporaryURL, setTemporaryURL ] = useState();
const altRef = useRef();
useEffect( () => {
altRef.current = alt;
}, [ alt ] );
const captionRef = useRef();
useEffect( () => {
captionRef.current = caption;
}, [ caption ] );
const ref = useRef();
const { imageDefaultSize, mediaUpload, isContentLocked } = useSelect(
( select ) => {
const { getSettings, __unstableGetContentLockingParent } =
select( blockEditorStore );
const settings = getSettings();
return {
imageDefaultSize: settings.imageDefaultSize,
mediaUpload: settings.mediaUpload,
isContentLocked:
!! __unstableGetContentLockingParent( clientId ),
};
},
[]
);
const { createErrorNotice } = useDispatch( noticesStore );
function onUploadError( message ) {
createErrorNotice( message, { type: 'snackbar' } );
setAttributes( {
src: undefined,
id: undefined,
url: undefined,
} );
setTemporaryURL( undefined );
}
function onSelectImage( media ) {
if ( ! media || ! media.url ) {
setAttributes( {
url: undefined,
alt: undefined,
id: undefined,
title: undefined,
caption: undefined,
} );
return;
}
if ( isBlobURL( media.url ) ) {
setTemporaryURL( media.url );
return;
}
setTemporaryURL();
let mediaAttributes = pickRelevantMediaFiles( media, imageDefaultSize );
// If a caption text was meanwhile written by the user,
// make sure the text is not overwritten by empty captions.
if ( captionRef.current && ! get( mediaAttributes, [ 'caption' ] ) ) {
const { caption: omittedCaption, ...restMediaAttributes } =
mediaAttributes;
mediaAttributes = restMediaAttributes;
}
let additionalAttributes;
// Reset the dimension attributes if changing to a different image.
if ( ! media.id || media.id !== id ) {
additionalAttributes = {
width: undefined,
height: undefined,
// Fallback to size "full" if there's no default image size.
// It means the image is smaller, and the block will use a full-size URL.
sizeSlug: hasDefaultSize( media, imageDefaultSize )
? imageDefaultSize
: 'full',
};
} else {
// Keep the same url when selecting the same file, so "Image Size"
// option is not changed.
additionalAttributes = { url };
}
// Check if default link setting should be used.
let linkDestination = attributes.linkDestination;
if ( ! linkDestination ) {
// Use the WordPress option to determine the proper default.
// The constants used in Gutenberg do not match WP options so a little more complicated than ideal.
// TODO: fix this in a follow up PR, requires updating media-text and ui component.
switch (
window?.wp?.media?.view?.settings?.defaultProps?.link ||
LINK_DESTINATION_NONE
) {
case 'file':
case LINK_DESTINATION_MEDIA:
linkDestination = LINK_DESTINATION_MEDIA;
break;
case 'post':
case LINK_DESTINATION_ATTACHMENT:
linkDestination = LINK_DESTINATION_ATTACHMENT;
break;
case LINK_DESTINATION_CUSTOM:
linkDestination = LINK_DESTINATION_CUSTOM;
break;
case LINK_DESTINATION_NONE:
linkDestination = LINK_DESTINATION_NONE;
break;
}
}
// Check if the image is linked to it's media.
let href;
switch ( linkDestination ) {
case LINK_DESTINATION_MEDIA:
href = media.url;
break;
case LINK_DESTINATION_ATTACHMENT:
href = media.link;
break;
}
mediaAttributes.href = href;
setAttributes( {
...mediaAttributes,
...additionalAttributes,
linkDestination,
} );
}
function onSelectURL( newURL ) {
if ( newURL !== url ) {
setAttributes( {
url: newURL,
id: undefined,
width: undefined,
height: undefined,
sizeSlug: imageDefaultSize,
} );
}
}
function updateAlignment( nextAlign ) {
const extraUpdatedAttributes = [ 'wide', 'full' ].includes( nextAlign )
? { width: undefined, height: undefined }
: {};
setAttributes( {
...extraUpdatedAttributes,
align: nextAlign,
} );
}
let isTemp = isTemporaryImage( id, url );
// Upload a temporary image on mount.
useEffect( () => {
if ( ! isTemp ) {
return;
}
const file = getBlobByURL( url );
if ( file ) {
mediaUpload( {
filesList: [ file ],
onFileChange: ( [ img ] ) => {
onSelectImage( img );
},
allowedTypes: ALLOWED_MEDIA_TYPES,
onError: ( message ) => {
isTemp = false;
onUploadError( message );
},
} );
}
}, [] );
// If an image is temporary, revoke the Blob url when it is uploaded (and is
// no longer temporary).
useEffect( () => {
if ( isTemp ) {
setTemporaryURL( url );
return;
}
revokeBlobURL( temporaryURL );
}, [ isTemp, url ] );
const isExternal = isExternalImage( id, url );
const src = isExternal ? url : undefined;
const mediaPreview = !! url && (
<img
alt={ __( 'Edit image' ) }
title={ __( 'Edit image' ) }
className={ 'edit-image-preview' }
src={ url }
/>
);
const borderProps = useBorderProps( attributes );
const classes = classnames( className, {
'is-transient': temporaryURL,
'is-resized': !! width || !! height,
[ `size-${ sizeSlug }` ]: sizeSlug,
'has-custom-border':
!! borderProps.className || ! isEmpty( borderProps.style ),
} );
const blockProps = useBlockProps( {
ref,
className: classes,
} );
return (
<figure { ...blockProps }>
{ ( temporaryURL || url ) && (
<Image
temporaryURL={ temporaryURL }
attributes={ attributes }
setAttributes={ setAttributes }
isSelected={ isSelected }
insertBlocksAfter={ insertBlocksAfter }
onReplace={ onReplace }
onSelectImage={ onSelectImage }
onSelectURL={ onSelectURL }
onUploadError={ onUploadError }
containerRef={ ref }
context={ context }
clientId={ clientId }
isContentLocked={ isContentLocked }
/>
) }
{ ! url && ! isContentLocked && (
<BlockControls group="block">
<BlockAlignmentControl
value={ align }
onChange={ updateAlignment }
/>
</BlockControls>
) }
<MediaPlaceholder
icon={ <BlockIcon icon={ icon } /> }
onSelect={ onSelectImage }
onSelectURL={ onSelectURL }
onError={ onUploadError }
placeholder={ placeholder }
accept="image/*"
allowedTypes={ ALLOWED_MEDIA_TYPES }
value={ { id, src } }
mediaPreview={ mediaPreview }
disableMediaButtons={ temporaryURL || url }
/>
</figure>
);
}
export default ImageEdit;