Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add background image support to group block #38784

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Combine blocks into a group. ([Source](https://github.com/WordPress/gutenberg/tr
- **Name:** core/group
- **Category:** design
- **Supports:** align (full, wide), anchor, color (background, gradients, link, text), spacing (blockGap, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** tagName, templateLock
- **Attributes:** alt, backgroundType, customOverlayColor, dimRatio, focalPoint, hasParallax, id, isRepeated, overlayColor, tagName, templateLock, url

## Heading

Expand Down
38 changes: 38 additions & 0 deletions packages/block-library/src/group/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,44 @@
"templateLock": {
"type": [ "string", "boolean" ],
"enum": [ "all", "insert", false ]
},
"url": {
"type": "string"
},
"id": {
"type": "number"
},
"alt": {
"type": "string",
"source": "attribute",
"selector": "img",
"attribute": "alt",
"default": ""
},
"hasParallax": {
"type": "boolean",
"default": false
},
"isRepeated": {
"type": "boolean",
"default": false
},
"dimRatio": {
"type": "number",
"default": 100
},
"overlayColor": {
"type": "string"
},
"customOverlayColor": {
"type": "string"
},
"backgroundType": {
"type": "string",
"default": "image"
},
"focalPoint": {
"type": "object"
}
},
"supports": {
Expand Down
282 changes: 270 additions & 12 deletions packages/block-library/src/group/edit.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,103 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import FastAverageColor from 'fast-average-color';
import { colord } from 'colord';

/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { useRef, useState, useEffect } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import {
InnerBlocks,
useBlockProps,
InspectorControls,
useInnerBlocksProps,
useSetting,
withColors,
BlockControls,
MediaReplaceFlow,
store as blockEditorStore,
__experimentalPanelColorGradientSettings as PanelColorGradientSettings,
__experimentalUseGradient,
} from '@wordpress/block-editor';
import { SelectControl } from '@wordpress/components';
import {
SelectControl,
Spinner,
PanelBody,
FocalPointPicker,
RangeControl,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { compose } from '@wordpress/compose';
import { isBlobURL } from '@wordpress/blob';

/**
* Internal dependencies
*/
/* eslint-disable no-unused-vars */
import {
attributesFromMedia,
IMAGE_BACKGROUND_TYPE,
backgroundImageStyles,
dimRatioToClass,
} from './shared';
/* eslint-enable no-unused-vars */

// TODO - refactor to shared from cover block
// For now let's keep things simple and use only images.
const ALLOWED_MEDIA_TYPES = [ 'image' ];

// TODO - refactor to shared from cover block
function useCoverIsDark( url, dimRatio = 50, overlayColor, elementRef ) {
const [ isDark, setIsDark ] = useState( false );
useEffect( () => {
// If opacity is lower than 50 the dominant color is the image or video color,
// so use that color for the dark mode computation.
if ( url && dimRatio <= 50 && elementRef.current ) {
retrieveFastAverageColor().getColorAsync(
elementRef.current,
( color ) => {
setIsDark( color.isDark );
}
);
}
}, [ url, url && dimRatio <= 50 && elementRef.current, setIsDark ] );
useEffect( () => {
// If opacity is greater than 50 the dominant color is the overlay color,
// so use that color for the dark mode computation.
if ( dimRatio > 50 || ! url ) {
if ( ! overlayColor ) {
// If no overlay color exists the overlay color is black (isDark )
setIsDark( true );
return;
}
setIsDark( colord( overlayColor ).isDark() );
}
}, [ overlayColor, dimRatio > 50 || ! url, setIsDark ] );
useEffect( () => {
if ( ! url && ! overlayColor ) {
// Reset isDark
setIsDark( false );
}
}, [ ! url && ! overlayColor, setIsDark ] );
return isDark;
}

// TODO - refactor to shared from cover block
function mediaPosition( { x, y } ) {
return `${ Math.round( x * 100 ) }% ${ Math.round( y * 100 ) }%`;
}

// TODO - refactor to shared from cover block
function retrieveFastAverageColor() {
if ( ! retrieveFastAverageColor.fastAverageColor ) {
retrieveFastAverageColor.fastAverageColor = new FastAverageColor();
}
return retrieveFastAverageColor.fastAverageColor;
}

const htmlElementMessages = {
header: __(
Expand All @@ -34,7 +120,28 @@ const htmlElementMessages = {
),
};

// TODO - duplicated from Cover. Should be in shared?
const isTemporaryMedia = ( id, url ) => ! id && isBlobURL( url );

function GroupEdit( { attributes, setAttributes, clientId } ) {
const {
id,
url,
backgroundType,
alt,
focalPoint,
overlayColor,
setOverlayColor,
dimRatio,
isDark,
} = attributes;

const {
gradientClass,
gradientValue,
setGradient,
} = __experimentalUseGradient();

const { hasInnerBlocks, themeSupportsLayout } = useSelect(
( select ) => {
const { getBlock, getSettings } = select( blockEditorStore );
Expand All @@ -46,11 +153,34 @@ function GroupEdit( { attributes, setAttributes, clientId } ) {
},
[ clientId ]
);

const {
__unstableMarkNextChangeAsNotPersistent: markNextChangeAsNotPersistent,
} = useDispatch( blockEditorStore );

const ref = useRef();
const isDarkElement = useRef();
const isCoverDark = useCoverIsDark(
url,
dimRatio,
overlayColor?.color,
isDarkElement
);

useEffect( () => {
// This side-effect should not create an undo level.
markNextChangeAsNotPersistent();
setAttributes( { isDark: isCoverDark } );
}, [ isCoverDark ] );

const defaultLayout = useSetting( 'layout' ) || {};
const { tagName: TagName = 'div', templateLock, layout = {} } = attributes;
const usedLayout = !! layout && layout.inherit ? defaultLayout : layout;
const { type = 'default' } = usedLayout;
const layoutSupportEnabled = themeSupportsLayout || type !== 'default';

// TODO - temp disabled
const layoutSupportEnabled =
( false && themeSupportsLayout ) || type !== 'default';

const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps(
Expand All @@ -66,8 +196,106 @@ function GroupEdit( { attributes, setAttributes, clientId } ) {
}
);

const isImgElement = true; // ! ( hasParallax || isRepeated );
const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;

const bgStyle = { backgroundColor: overlayColor?.color };
const mediaStyle = {
objectPosition:
focalPoint && isImgElement
? mediaPosition( focalPoint )
: undefined,
};

const onSelectMedia = attributesFromMedia( setAttributes, 1 );
const isUploadingMedia = isTemporaryMedia( id, url );

const showBgImage = url && isImageBackground && isImgElement;
const showFocalPointPicker = isImageBackground;

const imperativeFocalPointPreview = ( value ) => {
const [ styleOfRef, property ] = isDarkElement.current
? [ isDarkElement.current.style, 'objectPosition' ]
: [ ref.current.style, 'backgroundPosition' ];
styleOfRef[ property ] = mediaPosition( value );
};

// TODO - make dynamic
const hasParallax = false;
const isRepeated = false;

// TODO - shared with cover block
const classes = classnames( {
'is-dark-theme': isDark,
'is-light': ! isDark,
'is-transient': isUploadingMedia,
'has-parallax': hasParallax,
'is-repeated': isRepeated,
} );

return (
<>
<BlockControls group="other">
<MediaReplaceFlow
mediaId={ id }
mediaURL={ url }
allowedTypes={ ALLOWED_MEDIA_TYPES }
accept="image/*,video/*"
onSelect={ onSelectMedia }
name={ ! url ? __( 'Add Media' ) : __( 'Replace' ) }
/>
</BlockControls>
<InspectorControls>
{ !! url && (
<PanelBody title={ __( 'Media settings' ) }>
{ showFocalPointPicker && (
<FocalPointPicker
label={ __( 'Focal point picker' ) }
url={ url }
value={ focalPoint }
onDragStart={ imperativeFocalPointPreview }
onDrag={ imperativeFocalPointPreview }
onChange={ ( newFocalPoint ) =>
setAttributes( {
focalPoint: newFocalPoint,
} )
}
/>
) }
</PanelBody>
) }

<PanelColorGradientSettings
__experimentalHasMultipleOrigins
__experimentalIsRenderedInSidebar
title={ __( 'Overlay' ) }
initialOpen={ true }
settings={ [
{
colorValue: overlayColor?.color,
gradientValue,
onColorChange: setOverlayColor,
onGradientChange: setGradient,
label: __( 'Color' ),
},
] }
>
<RangeControl
label={ __( 'Opacity' ) }
value={ dimRatio }
onChange={ ( newDimRation ) =>
setAttributes( {
dimRatio: newDimRation,
} )
}
min={ 0 }
max={ 100 }
step={ 10 }
required
/>
</PanelColorGradientSettings>
</InspectorControls>

<InspectorControls __experimentalGroup="advanced">
<SelectControl
label={ __( 'HTML element' ) }
Expand All @@ -87,16 +315,46 @@ function GroupEdit( { attributes, setAttributes, clientId } ) {
help={ htmlElementMessages[ TagName ] }
/>
</InspectorControls>
{ layoutSupportEnabled && <TagName { ...innerBlocksProps } /> }
{ /* Ideally this is not needed but it's there for backward compatibility reason
to keep this div for themes that might rely on its presence */ }
{ ! layoutSupportEnabled && (
<TagName { ...blockProps }>
<div { ...innerBlocksProps } />
</TagName>
) }

<TagName
{ ...blockProps }
className={ classnames( classes, blockProps.className ) }
>
<span
aria-hidden="true"
className={ classnames(
'wp-block-group__background',
dimRatioToClass( dimRatio ),
{
[ overlayColor?.class ]: overlayColor?.class,
'has-background-dim': dimRatio !== undefined,
// For backwards compatibility. Former versions of the Cover Block applied
// `.wp-block-group__gradient-background` in the presence of
// media, a gradient and a dim.
'wp-block-group__gradient-background':
url && gradientValue && dimRatio !== 0,
'has-background-gradient': gradientValue,
[ gradientClass ]: gradientClass,
}
) }
style={ { backgroundImage: gradientValue, ...bgStyle } }
/>
{ showBgImage && (
<img
ref={ isDarkElement }
className="wp-block-group__image-background"
alt={ alt }
src={ url }
style={ mediaStyle }
/>
) }
{ isUploadingMedia && <Spinner /> }
<div { ...innerBlocksProps } />
</TagName>
</>
);
}

export default GroupEdit;
export default compose( [
withColors( { overlayColor: 'background-color' } ),
] )( GroupEdit );