diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php new file mode 100644 index 00000000000000..ac734895663d5a --- /dev/null +++ b/lib/block-supports/background.php @@ -0,0 +1,114 @@ +attributes ) { + $block_type->attributes = array(); + } + + if ( $has_background_image_support && ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } +} + +/** + * Checks whether serialization of the current block's background image properties should + * occur. + * + * @since 5.9.0 + * @access private + * + * @param WP_Block_Type $block_type Block type. + * @return bool Whether to serialize spacing support styles & classes. + */ +function wp_skip_background_image_serialization( $block_type ) { + $background_image_support = _wp_array_get( $block_type->supports, array( '__experimentalBackgroundImage' ), false ); + + return is_array( $background_image_support ) && + array_key_exists( '__experimentalSkipSerialization', $background_image_support ) && + $background_image_support['__experimentalSkipSerialization']; +} + +/** + * Renders the background image styles to the block wrapper. + * This block support uses the `render_block` hook to ensure that + * it is applied to non-server-rendered blocks. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_background_image_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $block_attributes = $block['attrs']; + $has_background_image_support = gutenberg_block_has_support( $block_type, array( '__experimentalBackgroundImage' ), false ); + if ( ! $has_background_image_support || ! isset( $block_attributes['style']['backgroundImage'] ) ) { + return $block_content; + } + + if ( wp_skip_background_image_serialization( $block_type ) ) { + return $block_content; + } + + $styles = array(); + + $background_image_source = _wp_array_get( $block_attributes, array( 'style', 'backgroundImage', 'source' ), null ); + $background_image_url = _wp_array_get( $block_attributes, array( 'style', 'backgroundImage', 'url' ), null ); + + if ( + 'file' === $background_image_source && + $background_image_url + ) { + $styles[] = sprintf( "background-image: url('%s')", esc_url( $background_image_url ) ); + } + + $inline_style = safecss_filter_attr( implode( '; ', $styles ) ); + + // Attempt to update an existing style attribute on the wrapper element. + $injected_style = preg_replace( + '/^([^>.]+?)(' . preg_quote( 'style="', '/' ) . ')(?=.+?>)/', + '$1$2' . $inline_style . '; ', + $block_content, + 1 + ); + + // If there is no existing style attribute, add one to the wrapper element. + if ( $injected_style === $block_content ) { + $injected_style = preg_replace( + '/<([a-zA-Z0-9]+)([ >])/', + '<$1 style="' . $inline_style . '"$2', + $block_content, + 1 + ); + }; + + return $injected_style; +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'backgroundImage', + array( + 'register_attribute' => 'wp_register_background_image_support', + ) +); + +add_filter( 'render_block', 'gutenberg_render_background_image_support', 10, 2 ); diff --git a/lib/compat/wordpress-5.9/theme.json b/lib/compat/wordpress-5.9/theme.json index ec29439d7f13f2..5f437713c9ed77 100644 --- a/lib/compat/wordpress-5.9/theme.json +++ b/lib/compat/wordpress-5.9/theme.json @@ -2,6 +2,7 @@ "version": 2, "settings": { "appearanceTools": false, + "backgroundImage": true, "border": { "color": false, "radius": false, diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php b/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php index c68f6fb3d205a7..75b8ac76fe6789 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php @@ -16,6 +16,58 @@ */ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_5_9 { + /** + * The valid properties under the settings key. + * + * @var array + */ + const VALID_SETTINGS = array( + 'appearanceTools' => null, + 'backgroundImage' => null, + 'border' => array( + 'color' => null, + 'radius' => null, + 'style' => null, + 'width' => null, + ), + 'color' => array( + 'background' => null, + 'custom' => null, + 'customDuotone' => null, + 'customGradient' => null, + 'defaultGradients' => null, + 'defaultPalette' => null, + 'duotone' => null, + 'gradients' => null, + 'link' => null, + 'palette' => null, + 'text' => null, + ), + 'custom' => null, + 'layout' => array( + 'contentSize' => null, + 'wideSize' => null, + ), + 'spacing' => array( + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, + ), + 'typography' => array( + 'customFontSize' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textDecoration' => null, + 'textTransform' => null, + ), + ); + /** * The top-level keys a theme.json can have. * diff --git a/lib/load.php b/lib/load.php index 26cd50608dbdc1..74a17fd17ede17 100644 --- a/lib/load.php +++ b/lib/load.php @@ -119,6 +119,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/global-styles.php'; require __DIR__ . '/pwa.php'; +require __DIR__ . '/block-supports/background.php'; require __DIR__ . '/block-supports/elements.php'; require __DIR__ . '/block-supports/colors.php'; require __DIR__ . '/block-supports/typography.php'; diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index e3061f0004af45..61ead4907e1965 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -143,6 +143,10 @@ const BlockInspectorSingleBlock = ( { ) } + { const [ mediaURLValue, setMediaURLValue ] = useState( mediaURL ); const mediaUpload = useSelect( ( select ) => { @@ -148,6 +149,7 @@ const MediaReplaceFlow = ( { aria-haspopup="true" onClick={ onToggle } onKeyDown={ openOnArrowDown } + variant={ variant } > { name } diff --git a/packages/block-editor/src/hooks/backgroundImage.js b/packages/block-editor/src/hooks/backgroundImage.js new file mode 100644 index 00000000000000..a911ea559e8aa3 --- /dev/null +++ b/packages/block-editor/src/hooks/backgroundImage.js @@ -0,0 +1,191 @@ +/** + * WordPress dependencies + */ +import { getBlobTypeByURL, isBlobURL } from '@wordpress/blob'; +import { getBlockSupport } from '@wordpress/blocks'; +import { __experimentalToolsPanelItem as ToolsPanelItem } from '@wordpress/components'; +import { Platform } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import InspectorControls from '../components/inspector-controls'; +import MediaReplaceFlow from '../components/media-replace-flow'; + +import useSetting from '../components/use-setting'; +import { cleanEmptyObject } from './utils'; + +export const BACKGROUND_IMAGE_SUPPORT_KEY = '__experimentalBackgroundImage'; +export const IMAGE_BACKGROUND_TYPE = 'image'; + +export function BackgroundImagePanel( props ) { + const { attributes, clientId, setAttributes } = props; + + const { id, url } = attributes.style?.backgroundImage || {}; + + const onSelectMedia = ( media ) => { + if ( ! media || ! media.url ) { + setAttributes( { url: undefined, id: undefined } ); + return; + } + + if ( isBlobURL( media.url ) ) { + media.type = getBlobTypeByURL( media.url ); + } + + // For media selections originated from a file upload. + if ( media.media_type && media.media_type !== IMAGE_BACKGROUND_TYPE ) { + return; + } else if ( media.type !== IMAGE_BACKGROUND_TYPE ) { + // For media selections originated from existing files in the media library. + return; + } + + const newStyle = { + ...attributes.style, + backgroundImage: { + ...attributes.style?.backgroundImage, + ...{ + url: media.url, + id: media.id, + source: 'file', + }, + }, + }; + + const newAttributes = { + style: cleanEmptyObject( newStyle ), + }; + + setAttributes( newAttributes ); + }; + + const isBackgroundImageSupported = + useSetting( 'backgroundImage' ) && + hasBackgroundImageSupport( props.name ); + + const isDisabled = [ ! isBackgroundImageSupported ].every( Boolean ); + + if ( isDisabled ) { + return null; + } + + const createResetAllFilter = ( + backgroundImageAttributes, + topLevelAttributes = {} + ) => ( newAttributes ) => ( { + ...newAttributes, + ...topLevelAttributes, + style: removeBackgroundImageAttributes( + newAttributes.style, + backgroundImageAttributes + ), + } ); + + return ( + + { isBackgroundImageSupported && ( + hasBackgroundImageValue( props ) } + label={ __( 'Image' ) } + onDeselect={ () => resetBackgroundImage( props ) } + isShownByDefault={ true } + resetAllFilter={ createResetAllFilter( [ 'url', 'id' ] ) } + panelId={ clientId } + > + + + ) } + + ); +} + +/** + * Checks if there is a current value in the background image block support + * attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a background image value set. + */ +export function hasBackgroundImageValue( props ) { + const hasValue = + !! props.attributes.style?.backgroundImage?.id || + !! props.attributes.style?.backgroundImage?.url; + + return hasValue; +} + +/** + * Determine whether there is block support for background image. + * + * @param {string} blockName Block name. + * @param {string} feature Background image feature to check for. + * + * @return {boolean} Whether there is support. + */ +export function hasBackgroundImageSupport( blockName, feature = 'any' ) { + if ( Platform.OS !== 'web' ) { + return false; + } + + const support = getBlockSupport( blockName, BACKGROUND_IMAGE_SUPPORT_KEY ); + + if ( support === true ) { + return true; + } + + return !! support?.[ feature ]; +} + +/** + * Check whether serialization of background image classes and styles should be skipped. + * + * @param {string|Object} blockType Block name or block type object. + * + * @return {boolean} Whether serialization of border properties should occur. + */ +export function shouldSkipSerialization( blockType ) { + const support = getBlockSupport( blockType, BACKGROUND_IMAGE_SUPPORT_KEY ); + + return support?.__experimentalSkipSerialization; +} + +export function resetBackgroundImage( { attributes = {}, setAttributes } ) { + const { style } = attributes; + setAttributes( { + style: removeBackgroundImageAttributes( style, [ 'url', 'id' ] ), + } ); +} + +/** + * Returns a new style object where the specified border attribute has been + * removed. + * + * @param {Object} style Styles from block attributes. + * @param {string} attributes The background image style attributes to clear. + * + * @return {Object} Style object with the specified attribute removed. + */ +export function removeBackgroundImageAttributes( style, attributes ) { + const clearedAttributes = {}; + attributes?.forEach( + ( attribute ) => ( clearedAttributes[ attribute ] = undefined ) + ); + return cleanEmptyObject( { + ...style, + backgroundImage: { + ...style?.backgroundImage, + ...clearedAttributes, + }, + } ); +} diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index a5f30d5c1e1333..2a6570bfa91620 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -33,6 +33,10 @@ import { getCSSRules } from '@wordpress/style-engine'; * Internal dependencies */ import BlockList from '../components/block-list'; +import { + BACKGROUND_IMAGE_SUPPORT_KEY, + BackgroundImagePanel, +} from './backgroundImage'; import { BORDER_SUPPORT_KEY, BorderPanel } from './border'; import { COLOR_SUPPORT_KEY, ColorEdit } from './color'; import { @@ -45,6 +49,7 @@ import useDisplayBlockControls from '../components/use-display-block-controls'; const styleSupportKeys = [ ...TYPOGRAPHY_SUPPORT_KEYS, + BACKGROUND_IMAGE_SUPPORT_KEY, BORDER_SUPPORT_KEY, COLOR_SUPPORT_KEY, SPACING_SUPPORT_KEY, @@ -197,6 +202,7 @@ const skipSerializationPathsEdit = { */ const skipSerializationPathsSave = { ...skipSerializationPathsEdit, + [ `${ BACKGROUND_IMAGE_SUPPORT_KEY }` ]: [ 'backgroundImage' ], [ `${ SPACING_SUPPORT_KEY }` ]: [ 'spacing.blockGap' ], }; @@ -283,6 +289,7 @@ export const withBlockControls = createHigherOrderComponent( <> { shouldDisplayControls && ( <> + diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index bd79422311dbb3..1203119aa62466 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -29,6 +29,7 @@ "text": true } }, + "__experimentalBackgroundImage": true, "spacing": { "margin": [ "top", "bottom" ], "padding": true, diff --git a/packages/style-engine/src/styles/backgroundImage.ts b/packages/style-engine/src/styles/backgroundImage.ts new file mode 100644 index 00000000000000..70601e7bf50f40 --- /dev/null +++ b/packages/style-engine/src/styles/backgroundImage.ts @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import type { GeneratedCSSRule, Style, StyleOptions } from '../types'; + +const padding = { + name: 'backgroundImage', + generate: ( style: Style, options: StyleOptions ) => { + const backgroundImage = style?.backgroundImage; + + const styleRules: GeneratedCSSRule[] = []; + + if ( ! backgroundImage ) { + return styleRules; + } + + if ( backgroundImage?.source === 'file' && backgroundImage?.url ) { + styleRules.push( { + selector: options.selector, + key: 'backgroundImage', + value: `url( '${ backgroundImage.url }' )`, + } ); + } + + return styleRules; + }, +}; + +export default padding; diff --git a/packages/style-engine/src/styles/index.ts b/packages/style-engine/src/styles/index.ts index 2f09e428176937..c77b29916cc589 100644 --- a/packages/style-engine/src/styles/index.ts +++ b/packages/style-engine/src/styles/index.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import backgroundImage from './backgroundImage'; import padding from './padding'; -export const styleDefinitions = [ padding ]; +export const styleDefinitions = [ backgroundImage, padding ]; diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 48bd9a0d8e5c6e..0b13bfd11a372f 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -12,6 +12,10 @@ export type Box< T extends BoxVariants = undefined > = { }; export interface Style { + backgroundImage?: { + url?: CSSProperties[ 'backgroundImage' ]; + source?: string; + }; spacing?: { margin?: CSSProperties[ 'margin' ] | Box< 'margin' >; padding?: CSSProperties[ 'padding' ] | Box< 'padding' >;