@wordpress/block-editor
Version:
311 lines (293 loc) • 8.19 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import {
Tooltip,
DropdownMenu,
MenuGroup,
MenuItem,
Spinner,
Modal,
Flex,
FlexItem,
Button,
Composite,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useMemo, useCallback, useState } from '@wordpress/element';
import { cloneBlock } from '@wordpress/blocks';
import { moreVertical, external } from '@wordpress/icons';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { isBlobURL } from '@wordpress/blob';
import { getFilename } from '@wordpress/url';
/**
* Internal dependencies
*/
import InserterDraggableBlocks from '../../inserter-draggable-blocks';
import { getBlockAndPreviewFromMedia } from './utils';
import { store as blockEditorStore } from '../../../store';
const ALLOWED_MEDIA_TYPES = [ 'image' ];
const MEDIA_OPTIONS_POPOVER_PROPS = {
placement: 'bottom-end',
className:
'block-editor-inserter__media-list__item-preview-options__popover',
};
function MediaPreviewOptions( { category, media } ) {
if ( ! category.getReportUrl ) {
return null;
}
const reportUrl = category.getReportUrl( media );
return (
<DropdownMenu
className="block-editor-inserter__media-list__item-preview-options"
label={ __( 'Options' ) }
popoverProps={ MEDIA_OPTIONS_POPOVER_PROPS }
icon={ moreVertical }
>
{ () => (
<MenuGroup>
<MenuItem
onClick={ () =>
window.open( reportUrl, '_blank' ).focus()
}
icon={ external }
>
{ sprintf(
/* translators: %s: The media type to report e.g: "image", "video", "audio" */
__( 'Report %s' ),
category.mediaType
) }
</MenuItem>
</MenuGroup>
) }
</DropdownMenu>
);
}
function InsertExternalImageModal( { onClose, onSubmit } ) {
return (
<Modal
title={ __( 'Insert external image' ) }
onRequestClose={ onClose }
className="block-editor-inserter-media-tab-media-preview-inserter-external-image-modal"
>
<VStack spacing={ 3 }>
<p>
{ __(
'This image cannot be uploaded to your Media Library, but it can still be inserted as an external image.'
) }
</p>
<p>
{ __(
'External images can be removed by the external provider without warning and could even have legal compliance issues related to privacy legislation.'
) }
</p>
</VStack>
<Flex
className="block-editor-block-lock-modal__actions"
justify="flex-end"
expanded={ false }
>
<FlexItem>
<Button
__next40pxDefaultSize
variant="tertiary"
onClick={ onClose }
>
{ __( 'Cancel' ) }
</Button>
</FlexItem>
<FlexItem>
<Button
__next40pxDefaultSize
variant="primary"
onClick={ onSubmit }
>
{ __( 'Insert' ) }
</Button>
</FlexItem>
</Flex>
</Modal>
);
}
export function MediaPreview( { media, onClick, category } ) {
const [ showExternalUploadModal, setShowExternalUploadModal ] =
useState( false );
const [ isHovered, setIsHovered ] = useState( false );
const [ isInserting, setIsInserting ] = useState( false );
const [ block, preview ] = useMemo(
() => getBlockAndPreviewFromMedia( media, category.mediaType ),
[ media, category.mediaType ]
);
const { createErrorNotice, createSuccessNotice } =
useDispatch( noticesStore );
const { getSettings, getBlock } = useSelect( blockEditorStore );
const { updateBlockAttributes } = useDispatch( blockEditorStore );
const onMediaInsert = useCallback(
( previewBlock ) => {
// Prevent multiple uploads when we're in the process of inserting.
if ( isInserting ) {
return;
}
const settings = getSettings();
const clonedBlock = cloneBlock( previewBlock );
const { id, url, caption } = clonedBlock.attributes;
// User has no permission to upload media.
if ( ! id && ! settings.mediaUpload ) {
setShowExternalUploadModal( true );
return;
}
// Media item already exists in library, so just insert it.
if ( !! id ) {
onClick( clonedBlock );
return;
}
setIsInserting( true );
// Media item does not exist in library, so try to upload it.
// Fist fetch the image data. This may fail if the image host
// doesn't allow CORS with the domain.
// If this happens, we insert the image block using the external
// URL and let the user know about the possible implications.
window
.fetch( url )
.then( ( response ) => response.blob() )
.then( ( blob ) => {
const fileName = getFilename( url ) || 'image.jpg';
const file = new File( [ blob ], fileName, {
type: blob.type,
} );
settings.mediaUpload( {
filesList: [ file ],
additionalData: { caption },
onFileChange( [ img ] ) {
if ( isBlobURL( img.url ) ) {
return;
}
if ( ! getBlock( clonedBlock.clientId ) ) {
// Ensure the block is only inserted once.
onClick( {
...clonedBlock,
attributes: {
...clonedBlock.attributes,
id: img.id,
url: img.url,
},
} );
createSuccessNotice(
__( 'Image uploaded and inserted.' ),
{ type: 'snackbar', id: 'inserter-notice' }
);
} else {
// For subsequent calls, update the existing block.
updateBlockAttributes( clonedBlock.clientId, {
...clonedBlock.attributes,
id: img.id,
url: img.url,
} );
}
setIsInserting( false );
},
allowedTypes: ALLOWED_MEDIA_TYPES,
onError( message ) {
createErrorNotice( message, {
type: 'snackbar',
id: 'inserter-notice',
} );
setIsInserting( false );
},
} );
} )
.catch( () => {
setShowExternalUploadModal( true );
setIsInserting( false );
} );
},
[
isInserting,
getSettings,
onClick,
createSuccessNotice,
updateBlockAttributes,
createErrorNotice,
getBlock,
]
);
const title =
typeof media.title === 'string'
? media.title
: media.title?.rendered || __( 'no title' );
const onMouseEnter = useCallback( () => setIsHovered( true ), [] );
const onMouseLeave = useCallback( () => setIsHovered( false ), [] );
return (
<>
<InserterDraggableBlocks isEnabled blocks={ [ block ] }>
{ ( { draggable, onDragStart, onDragEnd } ) => (
<div
className={ clsx(
'block-editor-inserter__media-list__list-item',
{
'is-hovered': isHovered,
}
) }
draggable={ draggable }
onDragStart={ onDragStart }
onDragEnd={ onDragEnd }
>
{ /* Adding `is-hovered` class to the wrapper element is needed
because the options Popover is rendered outside of this node. */ }
<div
onMouseEnter={ onMouseEnter }
onMouseLeave={ onMouseLeave }
>
<Tooltip text={ title }>
<Composite.Item
render={
<div
aria-label={ title }
role="option"
className="block-editor-inserter__media-list__item"
/>
}
onClick={ () => onMediaInsert( block ) }
>
<div className="block-editor-inserter__media-list__item-preview">
{ preview }
{ isInserting && (
<div className="block-editor-inserter__media-list__item-preview-spinner">
<Spinner />
</div>
) }
</div>
</Composite.Item>
</Tooltip>
{ ! isInserting && (
<MediaPreviewOptions
category={ category }
media={ media }
/>
) }
</div>
</div>
) }
</InserterDraggableBlocks>
{ showExternalUploadModal && (
<InsertExternalImageModal
onClose={ () => setShowExternalUploadModal( false ) }
onSubmit={ () => {
onClick( cloneBlock( block ) );
createSuccessNotice( __( 'Image inserted.' ), {
type: 'snackbar',
id: 'inserter-notice',
} );
setShowExternalUploadModal( false );
} }
/>
) }
</>
);
}