UNPKG

@wordpress/block-library

Version:
638 lines (568 loc) 16.7 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import { useEffect, useMemo, useRef } from '@wordpress/element'; import { Placeholder, Spinner } from '@wordpress/components'; import { compose, useResizeObserver } from '@wordpress/compose'; import { withColors, ColorPalette, useBlockProps, useSettings, useInnerBlocksProps, __experimentalUseGradient, store as blockEditorStore, useBlockEditingMode, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { isBlobURL } from '@wordpress/blob'; import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ import { attributesFromMedia, IMAGE_BACKGROUND_TYPE, VIDEO_BACKGROUND_TYPE, dimRatioToClass, isContentPositionCenter, getPositionClassName, mediaPosition, } from '../shared'; import CoverInspectorControls from './inspector-controls'; import CoverBlockControls from './block-controls'; import CoverPlaceholder from './cover-placeholder'; import ResizableCoverPopover from './resizable-cover-popover'; import { getMediaColor, compositeIsDark, DEFAULT_BACKGROUND_COLOR, DEFAULT_OVERLAY_COLOR, } from './color-utils'; import { DEFAULT_MEDIA_SIZE_SLUG } from '../constants'; function getInnerBlocksTemplate( attributes ) { return [ [ 'core/paragraph', { align: 'center', placeholder: __( 'Write title…' ), ...attributes, }, ], ]; } /** * Is the URL a temporary blob URL? A blob URL is one that is used temporarily while * the media (image or video) is being uploaded and will not have an id allocated yet. * * @param {number} id The id of the media. * @param {string} url The url of the media. * * @return {boolean} Is the URL a Blob URL. */ const isTemporaryMedia = ( id, url ) => ! id && isBlobURL( url ); function CoverEdit( { attributes, clientId, isSelected, overlayColor, setAttributes, setOverlayColor, toggleSelection, context: { postId, postType }, } ) { const { contentPosition, id, url: originalUrl, backgroundType: originalBackgroundType, useFeaturedImage, dimRatio, focalPoint, hasParallax, isDark, isRepeated, minHeight, minHeightUnit, alt, allowedBlocks, templateLock, tagName: TagName = 'div', isUserOverlayColor, sizeSlug, } = attributes; const [ featuredImage ] = useEntityProp( 'postType', postType, 'featured_media', postId ); const { getSettings } = useSelect( blockEditorStore ); const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); const { media } = useSelect( ( select ) => { return { media: featuredImage && useFeaturedImage ? select( coreStore ).getMedia( featuredImage, { context: 'view', } ) : undefined, }; }, [ featuredImage, useFeaturedImage ] ); const mediaUrl = media?.media_details?.sizes?.[ sizeSlug ]?.source_url ?? media?.source_url; // User can change the featured image outside of the block, but we still // need to update the block when that happens. This effect should only // run when the featured image changes in that case. All other cases are // handled in their respective callbacks. useEffect( () => { ( async () => { if ( ! useFeaturedImage ) { return; } const averageBackgroundColor = await getMediaColor( mediaUrl ); let newOverlayColor = overlayColor.color; if ( ! isUserOverlayColor ) { newOverlayColor = averageBackgroundColor; __unstableMarkNextChangeAsNotPersistent(); setOverlayColor( newOverlayColor ); } const newIsDark = compositeIsDark( dimRatio, newOverlayColor, averageBackgroundColor ); __unstableMarkNextChangeAsNotPersistent(); setAttributes( { isDark: newIsDark, isUserOverlayColor: isUserOverlayColor || false, } ); } )(); // Update the block only when the featured image changes. }, [ mediaUrl ] ); // instead of destructuring the attributes // we define the url and background type // depending on the value of the useFeaturedImage flag // to preview in edit the dynamic featured image const url = useFeaturedImage ? mediaUrl : // Ensure the url is not malformed due to sanitization through `wp_kses`. originalUrl?.replaceAll( '&amp;', '&' ); const backgroundType = useFeaturedImage ? IMAGE_BACKGROUND_TYPE : originalBackgroundType; const { createErrorNotice } = useDispatch( noticesStore ); const { gradientClass, gradientValue } = __experimentalUseGradient(); const onSelectMedia = async ( newMedia ) => { const mediaAttributes = attributesFromMedia( newMedia ); const isImage = [ newMedia?.type, newMedia?.media_type ].includes( IMAGE_BACKGROUND_TYPE ); const averageBackgroundColor = await getMediaColor( isImage ? newMedia?.url : undefined ); let newOverlayColor = overlayColor.color; if ( ! isUserOverlayColor ) { newOverlayColor = averageBackgroundColor; setOverlayColor( newOverlayColor ); // Make undo revert the next setAttributes and the previous setOverlayColor. __unstableMarkNextChangeAsNotPersistent(); } // Only set a new dimRatio if there was no previous media selected // to avoid resetting to 50 if it has been explicitly set to 100. // See issue #52835 for context. const newDimRatio = originalUrl === undefined && dimRatio === 100 ? 50 : dimRatio; const newIsDark = compositeIsDark( newDimRatio, newOverlayColor, averageBackgroundColor ); if ( backgroundType === IMAGE_BACKGROUND_TYPE && mediaAttributes?.id ) { const { imageDefaultSize } = getSettings(); // Try to use the previous selected image size if it's available // otherwise try the default image size or fallback to full size. if ( sizeSlug && ( newMedia?.sizes?.[ sizeSlug ] || newMedia?.media_details?.sizes?.[ sizeSlug ] ) ) { mediaAttributes.sizeSlug = sizeSlug; mediaAttributes.url = newMedia?.sizes?.[ sizeSlug ]?.url || newMedia?.media_details?.sizes?.[ sizeSlug ]?.source_url; } else if ( newMedia?.sizes?.[ imageDefaultSize ] || newMedia?.media_details?.sizes?.[ imageDefaultSize ] ) { mediaAttributes.sizeSlug = imageDefaultSize; mediaAttributes.url = newMedia?.sizes?.[ imageDefaultSize ]?.url || newMedia?.media_details?.sizes?.[ imageDefaultSize ] ?.source_url; } else { mediaAttributes.sizeSlug = DEFAULT_MEDIA_SIZE_SLUG; } } setAttributes( { ...mediaAttributes, focalPoint: undefined, useFeaturedImage: undefined, dimRatio: newDimRatio, isDark: newIsDark, isUserOverlayColor: isUserOverlayColor || false, } ); }; const onClearMedia = () => { let newOverlayColor = overlayColor.color; if ( ! isUserOverlayColor ) { newOverlayColor = DEFAULT_OVERLAY_COLOR; setOverlayColor( undefined ); // Make undo revert the next setAttributes and the previous setOverlayColor. __unstableMarkNextChangeAsNotPersistent(); } const newIsDark = compositeIsDark( dimRatio, newOverlayColor, DEFAULT_BACKGROUND_COLOR ); setAttributes( { url: undefined, id: undefined, backgroundType: undefined, focalPoint: undefined, hasParallax: undefined, isRepeated: undefined, useFeaturedImage: undefined, isDark: newIsDark, } ); }; const onSetOverlayColor = async ( newOverlayColor ) => { const averageBackgroundColor = await getMediaColor( url ); const newIsDark = compositeIsDark( dimRatio, newOverlayColor, averageBackgroundColor ); setOverlayColor( newOverlayColor ); // Make undo revert the next setAttributes and the previous setOverlayColor. __unstableMarkNextChangeAsNotPersistent(); setAttributes( { isUserOverlayColor: true, isDark: newIsDark, } ); }; const onUpdateDimRatio = async ( newDimRatio ) => { const averageBackgroundColor = await getMediaColor( url ); const newIsDark = compositeIsDark( newDimRatio, overlayColor.color, averageBackgroundColor ); setAttributes( { dimRatio: newDimRatio, isDark: newIsDark, } ); }; const onUploadError = ( message ) => { createErrorNotice( message, { type: 'snackbar' } ); }; const isUploadingMedia = isTemporaryMedia( id, url ); const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType; const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType; const blockEditingMode = useBlockEditingMode(); const hasNonContentControls = blockEditingMode === 'default'; const [ resizeListener, { height, width } ] = useResizeObserver(); const resizableBoxDimensions = useMemo( () => { return { height: minHeightUnit === 'px' ? minHeight : 'auto', width: 'auto', }; }, [ minHeight, minHeightUnit ] ); const minHeightWithUnit = minHeight && minHeightUnit ? `${ minHeight }${ minHeightUnit }` : minHeight; const isImgElement = ! ( hasParallax || isRepeated ); const style = { minHeight: minHeightWithUnit || undefined, }; const backgroundImage = url ? `url(${ url })` : undefined; const backgroundPosition = mediaPosition( focalPoint ); const bgStyle = { backgroundColor: overlayColor.color }; const mediaStyle = { objectPosition: focalPoint && isImgElement ? mediaPosition( focalPoint ) : undefined, }; const hasBackground = !! ( url || overlayColor.color || gradientValue ); const hasInnerBlocks = useSelect( ( select ) => select( blockEditorStore ).getBlock( clientId ).innerBlocks.length > 0, [ clientId ] ); const ref = useRef(); const blockProps = useBlockProps( { ref } ); // Check for fontSize support before we pass a fontSize attribute to the innerBlocks. const [ fontSizes ] = useSettings( 'typography.fontSizes' ); const hasFontSizes = fontSizes?.length > 0; const innerBlocksTemplate = getInnerBlocksTemplate( { fontSize: hasFontSizes ? 'large' : undefined, } ); const innerBlocksProps = useInnerBlocksProps( { className: 'wp-block-cover__inner-container', }, { // Avoid template sync when the `templateLock` value is `all` or `contentOnly`. // See: https://github.com/WordPress/gutenberg/pull/45632 template: ! hasInnerBlocks ? innerBlocksTemplate : undefined, templateInsertUpdatesSelection: true, allowedBlocks, templateLock, dropZoneElement: ref.current, } ); const mediaElement = useRef(); const currentSettings = { isVideoBackground, isImageBackground, mediaElement, hasInnerBlocks, url, isImgElement, overlayColor, }; const toggleUseFeaturedImage = async () => { const newUseFeaturedImage = ! useFeaturedImage; const averageBackgroundColor = newUseFeaturedImage ? await getMediaColor( mediaUrl ) : DEFAULT_BACKGROUND_COLOR; const newOverlayColor = ! isUserOverlayColor ? averageBackgroundColor : overlayColor.color; if ( ! isUserOverlayColor ) { if ( newUseFeaturedImage ) { setOverlayColor( newOverlayColor ); } else { setOverlayColor( undefined ); } // Make undo revert the next setAttributes and the previous setOverlayColor. __unstableMarkNextChangeAsNotPersistent(); } const newDimRatio = dimRatio === 100 ? 50 : dimRatio; const newIsDark = compositeIsDark( newDimRatio, newOverlayColor, averageBackgroundColor ); setAttributes( { id: undefined, url: undefined, useFeaturedImage: newUseFeaturedImage, dimRatio: newDimRatio, backgroundType: useFeaturedImage ? IMAGE_BACKGROUND_TYPE : undefined, isDark: newIsDark, } ); }; const blockControls = ( <CoverBlockControls attributes={ attributes } setAttributes={ setAttributes } onSelectMedia={ onSelectMedia } currentSettings={ currentSettings } toggleUseFeaturedImage={ toggleUseFeaturedImage } onClearMedia={ onClearMedia } /> ); const inspectorControls = ( <CoverInspectorControls attributes={ attributes } setAttributes={ setAttributes } clientId={ clientId } setOverlayColor={ onSetOverlayColor } coverRef={ ref } currentSettings={ currentSettings } toggleUseFeaturedImage={ toggleUseFeaturedImage } updateDimRatio={ onUpdateDimRatio } onClearMedia={ onClearMedia } featuredImage={ media } /> ); const resizableCoverProps = { className: 'block-library-cover__resize-container', clientId, height, minHeight: minHeightWithUnit, onResizeStart: () => { setAttributes( { minHeightUnit: 'px' } ); toggleSelection( false ); }, onResize: ( value ) => { setAttributes( { minHeight: value } ); }, onResizeStop: ( newMinHeight ) => { toggleSelection( true ); setAttributes( { minHeight: newMinHeight } ); }, // Hide the resize handle if an aspect ratio is set, as the aspect ratio takes precedence. showHandle: ! attributes.style?.dimensions?.aspectRatio, size: resizableBoxDimensions, width, }; if ( ! useFeaturedImage && ! hasInnerBlocks && ! hasBackground ) { return ( <> { blockControls } { inspectorControls } { hasNonContentControls && isSelected && ( <ResizableCoverPopover { ...resizableCoverProps } /> ) } <TagName { ...blockProps } className={ clsx( 'is-placeholder', blockProps.className ) } style={ { ...blockProps.style, minHeight: minHeightWithUnit || undefined, } } > { resizeListener } <CoverPlaceholder onSelectMedia={ onSelectMedia } onError={ onUploadError } toggleUseFeaturedImage={ toggleUseFeaturedImage } > <div className="wp-block-cover__placeholder-background-options"> <ColorPalette disableCustomColors value={ overlayColor.color } onChange={ onSetOverlayColor } clearable={ false } asButtons aria-label={ __( 'Overlay color' ) } /> </div> </CoverPlaceholder> </TagName> </> ); } const classes = clsx( { 'is-dark-theme': isDark, 'is-light': ! isDark, 'is-transient': isUploadingMedia, 'has-parallax': hasParallax, 'is-repeated': isRepeated, 'has-custom-content-position': ! isContentPositionCenter( contentPosition ), }, getPositionClassName( contentPosition ) ); const showOverlay = url || ! useFeaturedImage || ( useFeaturedImage && ! url ); return ( <> { blockControls } { inspectorControls } <TagName { ...blockProps } className={ clsx( classes, blockProps.className ) } style={ { ...style, ...blockProps.style } } data-url={ url } > { resizeListener } { ! url && useFeaturedImage && ( <Placeholder className="wp-block-cover__image--placeholder-image" withIllustration /> ) } { url && isImageBackground && ( isImgElement ? ( <img ref={ mediaElement } className="wp-block-cover__image-background" alt={ alt } src={ url } style={ mediaStyle } /> ) : ( <div ref={ mediaElement } role={ alt ? 'img' : undefined } aria-label={ alt ? alt : undefined } className={ clsx( classes, 'wp-block-cover__image-background' ) } style={ { backgroundImage, backgroundPosition } } /> ) ) } { url && isVideoBackground && ( <video ref={ mediaElement } className="wp-block-cover__video-background" autoPlay muted loop src={ url } style={ mediaStyle } /> ) } { showOverlay && ( <span aria-hidden="true" className={ clsx( 'wp-block-cover__background', dimRatioToClass( dimRatio ), { [ overlayColor.class ]: overlayColor.class, 'has-background-dim': dimRatio !== undefined, // For backwards compatibility. Former versions of the Cover Block applied // `.wp-block-cover__gradient-background` in the presence of // media, a gradient and a dim. 'wp-block-cover__gradient-background': url && gradientValue && dimRatio !== 0, 'has-background-gradient': gradientValue, [ gradientClass ]: gradientClass, } ) } style={ { backgroundImage: gradientValue, ...bgStyle } } /> ) } { isUploadingMedia && <Spinner /> } <CoverPlaceholder disableMediaButtons onSelectMedia={ onSelectMedia } onError={ onUploadError } toggleUseFeaturedImage={ toggleUseFeaturedImage } /> <div { ...innerBlocksProps } /> </TagName> { hasNonContentControls && isSelected && ( <ResizableCoverPopover { ...resizableCoverProps } /> ) } </> ); } export default compose( [ withColors( { overlayColor: 'background-color' } ), ] )( CoverEdit );