@wordpress/block-library
Version:
Block library for the WordPress editor.
401 lines (369 loc) • 10.1 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
import { map, filter } from 'lodash';
/**
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { useState, useRef } from '@wordpress/element';
import {
BlockControls,
BlockVerticalAlignmentControl,
useInnerBlocksProps,
InspectorControls,
useBlockProps,
__experimentalImageURLInputUI as ImageURLInputUI,
__experimentalImageSizeControl as ImageSizeControl,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
PanelBody,
RangeControl,
TextareaControl,
ToggleControl,
ToolbarButton,
ExternalLink,
FocalPointPicker,
} from '@wordpress/components';
import { isBlobURL, getBlobTypeByURL } from '@wordpress/blob';
import { pullLeft, pullRight } from '@wordpress/icons';
import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import MediaContainer from './media-container';
import { DEFAULT_MEDIA_SIZE_SLUG } from './constants';
/**
* Constants
*/
const TEMPLATE = [
[
'core/paragraph',
{
placeholder: _x( 'Content…', 'content placeholder' ),
},
],
];
// this limits the resize to a safe zone to avoid making broken layouts
const WIDTH_CONSTRAINT_PERCENTAGE = 15;
const applyWidthConstraints = ( width ) =>
Math.max(
WIDTH_CONSTRAINT_PERCENTAGE,
Math.min( width, 100 - WIDTH_CONSTRAINT_PERCENTAGE )
);
const LINK_DESTINATION_MEDIA = 'media';
const LINK_DESTINATION_ATTACHMENT = 'attachment';
function getImageSourceUrlBySizeSlug( image, slug ) {
// eslint-disable-next-line camelcase
return image?.media_details?.sizes?.[ slug ]?.source_url;
}
function attributesFromMedia( {
attributes: { linkDestination, href },
setAttributes,
} ) {
return ( media ) => {
if ( ! media || ! media.url ) {
setAttributes( {
mediaAlt: undefined,
mediaId: undefined,
mediaType: undefined,
mediaUrl: undefined,
mediaLink: undefined,
href: undefined,
focalPoint: undefined,
} );
return;
}
if ( isBlobURL( media.url ) ) {
media.type = getBlobTypeByURL( media.url );
}
let mediaType;
let src;
// For media selections originated from a file upload.
if ( media.media_type ) {
if ( media.media_type === 'image' ) {
mediaType = 'image';
} else {
// only images and videos are accepted so if the media_type is not an image we can assume it is a video.
// video contain the media type of 'file' in the object returned from the rest api.
mediaType = 'video';
}
} else {
// For media selections originated from existing files in the media library.
mediaType = media.type;
}
if ( mediaType === 'image' ) {
// Try the "large" size URL, falling back to the "full" size URL below.
src =
media.sizes?.large?.url ||
// eslint-disable-next-line camelcase
media.media_details?.sizes?.large?.source_url;
}
let newHref = href;
if ( linkDestination === LINK_DESTINATION_MEDIA ) {
// Update the media link.
newHref = media.url;
}
// Check if the image is linked to the attachment page.
if ( linkDestination === LINK_DESTINATION_ATTACHMENT ) {
// Update the media link.
newHref = media.link;
}
setAttributes( {
mediaAlt: media.alt,
mediaId: media.id,
mediaType,
mediaUrl: src || media.url,
mediaLink: media.link || undefined,
href: newHref,
focalPoint: undefined,
} );
};
}
function MediaTextEdit( { attributes, isSelected, setAttributes, clientId } ) {
const {
focalPoint,
href,
imageFill,
isStackedOnMobile,
linkClass,
linkDestination,
linkTarget,
mediaAlt,
mediaId,
mediaPosition,
mediaType,
mediaUrl,
mediaWidth,
rel,
verticalAlignment,
} = attributes;
const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG;
const { imageSizes, image, isContentLocked } = useSelect(
( select ) => {
const { __unstableGetContentLockingParent, getSettings } =
select( blockEditorStore );
return {
isContentLocked:
!! __unstableGetContentLockingParent( clientId ),
image:
mediaId && isSelected
? select( coreStore ).getMedia( mediaId, {
context: 'view',
} )
: null,
imageSizes: getSettings()?.imageSizes,
};
},
[ isSelected, mediaId, clientId ]
);
const refMediaContainer = useRef();
const imperativeFocalPointPreview = ( value ) => {
const { style } = refMediaContainer.current.resizable;
const { x, y } = value;
style.backgroundPosition = `${ x * 100 }% ${ y * 100 }%`;
};
const [ temporaryMediaWidth, setTemporaryMediaWidth ] = useState( null );
const onSelectMedia = attributesFromMedia( { attributes, setAttributes } );
const onSetHref = ( props ) => {
setAttributes( props );
};
const onWidthChange = ( width ) => {
setTemporaryMediaWidth( applyWidthConstraints( width ) );
};
const commitWidthChange = ( width ) => {
setAttributes( {
mediaWidth: applyWidthConstraints( width ),
} );
setTemporaryMediaWidth( applyWidthConstraints( width ) );
};
const classNames = classnames( {
'has-media-on-the-right': 'right' === mediaPosition,
'is-selected': isSelected,
'is-stacked-on-mobile': isStackedOnMobile,
[ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment,
'is-image-fill': imageFill,
} );
const widthString = `${ temporaryMediaWidth || mediaWidth }%`;
const gridTemplateColumns =
'right' === mediaPosition
? `1fr ${ widthString }`
: `${ widthString } 1fr`;
const style = {
gridTemplateColumns,
msGridColumns: gridTemplateColumns,
};
const onMediaAltChange = ( newMediaAlt ) => {
setAttributes( { mediaAlt: newMediaAlt } );
};
const onVerticalAlignmentChange = ( alignment ) => {
setAttributes( { verticalAlignment: alignment } );
};
const imageSizeOptions = map(
filter( imageSizes, ( { slug } ) =>
getImageSourceUrlBySizeSlug( image, slug )
),
( { name, slug } ) => ( { value: slug, label: name } )
);
const updateImage = ( newMediaSizeSlug ) => {
const newUrl = getImageSourceUrlBySizeSlug( image, newMediaSizeSlug );
if ( ! newUrl ) {
return null;
}
setAttributes( {
mediaUrl: newUrl,
mediaSizeSlug: newMediaSizeSlug,
} );
};
const mediaTextGeneralSettings = (
<PanelBody title={ __( 'Settings' ) }>
<ToggleControl
label={ __( 'Stack on mobile' ) }
checked={ isStackedOnMobile }
onChange={ () =>
setAttributes( {
isStackedOnMobile: ! isStackedOnMobile,
} )
}
/>
{ mediaType === 'image' && (
<ToggleControl
label={ __( 'Crop image to fill entire column' ) }
checked={ imageFill }
onChange={ () =>
setAttributes( {
imageFill: ! imageFill,
} )
}
/>
) }
{ imageFill && mediaUrl && mediaType === 'image' && (
<FocalPointPicker
label={ __( 'Focal point picker' ) }
url={ mediaUrl }
value={ focalPoint }
onChange={ ( value ) =>
setAttributes( { focalPoint: value } )
}
onDragStart={ imperativeFocalPointPreview }
onDrag={ imperativeFocalPointPreview }
/>
) }
{ mediaType === 'image' && (
<TextareaControl
label={ __( 'Alt text (alternative text)' ) }
value={ mediaAlt }
onChange={ onMediaAltChange }
help={
<>
<ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
{ __( 'Describe the purpose of the image' ) }
</ExternalLink>
{ __(
'Leave empty if the image is purely decorative.'
) }
</>
}
/>
) }
{ mediaType === 'image' && (
<ImageSizeControl
onChangeImage={ updateImage }
slug={ mediaSizeSlug }
imageSizeOptions={ imageSizeOptions }
isResizable={ false }
imageSizeHelp={ __( 'Select which image size to load.' ) }
/>
) }
{ mediaUrl && (
<RangeControl
label={ __( 'Media width' ) }
value={ temporaryMediaWidth || mediaWidth }
onChange={ commitWidthChange }
min={ WIDTH_CONSTRAINT_PERCENTAGE }
max={ 100 - WIDTH_CONSTRAINT_PERCENTAGE }
/>
) }
</PanelBody>
);
const blockProps = useBlockProps( {
className: classNames,
style,
} );
const innerBlocksProps = useInnerBlocksProps(
{ className: 'wp-block-media-text__content' },
{ template: TEMPLATE }
);
return (
<>
<InspectorControls>{ mediaTextGeneralSettings }</InspectorControls>
<BlockControls group="block">
{ ! isContentLocked && (
<>
<BlockVerticalAlignmentControl
onChange={ onVerticalAlignmentChange }
value={ verticalAlignment }
/>
<ToolbarButton
icon={ pullLeft }
title={ __( 'Show media on left' ) }
isActive={ mediaPosition === 'left' }
onClick={ () =>
setAttributes( { mediaPosition: 'left' } )
}
/>
<ToolbarButton
icon={ pullRight }
title={ __( 'Show media on right' ) }
isActive={ mediaPosition === 'right' }
onClick={ () =>
setAttributes( { mediaPosition: 'right' } )
}
/>
</>
) }
{ mediaType === 'image' && (
<ImageURLInputUI
url={ href || '' }
onChangeUrl={ onSetHref }
linkDestination={ linkDestination }
mediaType={ mediaType }
mediaUrl={ image && image.source_url }
mediaLink={ image && image.link }
linkTarget={ linkTarget }
linkClass={ linkClass }
rel={ rel }
/>
) }
</BlockControls>
<div { ...blockProps }>
{ mediaPosition === 'right' && <div { ...innerBlocksProps } /> }
<MediaContainer
className="wp-block-media-text__media"
onSelectMedia={ onSelectMedia }
onWidthChange={ onWidthChange }
commitWidthChange={ commitWidthChange }
ref={ refMediaContainer }
{ ...{
focalPoint,
imageFill,
isSelected,
isStackedOnMobile,
mediaAlt,
mediaId,
mediaPosition,
mediaType,
mediaUrl,
mediaWidth,
isContentLocked,
} }
/>
{ mediaPosition !== 'right' && <div { ...innerBlocksProps } /> }
</div>
</>
);
}
export default MediaTextEdit;