diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index 8e3c06159a1201..0ca4f81465c0ce 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -30,6 +30,30 @@ function gutenberg_register_background_support( $block_type ) { } } +/** + * Given a theme.json or block background styles, returns the background styles for a block. + * + * @since 6.6.0 + * + * @param array $background_styles Background style properties. + * @return array Style engine array of CSS string and style declarations. + */ +function gutenberg_get_background_support_styles( $background_styles = array() ) { + /* + * Styles backgrounds. + * Where a URL is not absolute (has no host fragment), it is assumed to be relative to the theme directory. + * Blocks, elements, and block variations are not yet supported. + */ + if ( + isset( $background_styles['backgroundImage']['url'] ) && + is_string( $background_styles['backgroundImage']['url'] ) && + ! isset( wp_parse_url( $background_styles['backgroundImage']['url'] )['host'] ) ) { + $background_styles['backgroundImage']['url'] = esc_url( get_theme_file_uri( $background_styles['backgroundImage']['url'] ) ); + } + return gutenberg_style_engine_get_styles( array( 'background' => $background_styles ) ); +} + + /** * Renders the background styles to the block wrapper. * This block support uses the `render_block` hook to ensure that @@ -66,7 +90,7 @@ function gutenberg_render_background_support( $block_content, $block ) { } } - $styles = gutenberg_style_engine_get_styles( array( 'background' => $background_styles ) ); + $styles = gutenberg_get_background_support_styles( $background_styles ); if ( ! empty( $styles['css'] ) ) { // Inject background styles to the first element, presuming it's the wrapper, if it exists. diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 77004253bea868..84b650c917929a 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -488,10 +488,10 @@ class WP_Theme_JSON_Gutenberg { */ const VALID_STYLES = array( 'background' => array( - 'backgroundImage' => 'top', - 'backgroundPosition' => 'top', - 'backgroundRepeat' => 'top', - 'backgroundSize' => 'top', + 'backgroundImage' => null, + 'backgroundPosition' => null, + 'backgroundRepeat' => null, + 'backgroundSize' => null, ), 'border' => array( 'color' => null, @@ -2198,7 +2198,7 @@ protected static function compute_style_properties( $styles, $settings = array() // Processes background styles. if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) { - $background_styles = gutenberg_style_engine_get_styles( array( 'background' => $styles['background'] ) ); + $background_styles = gutenberg_get_background_support_styles( $styles['background'] ); $value = $background_styles['declarations'][ $css_property ] ?? $value; } diff --git a/lib/compat/wordpress-6.6/rest-api.php b/lib/compat/wordpress-6.6/rest-api.php index 8526093dc99ddb..1bbbc4f3ff5ee5 100644 --- a/lib/compat/wordpress-6.6/rest-api.php +++ b/lib/compat/wordpress-6.6/rest-api.php @@ -76,3 +76,75 @@ function gutenberg_add_class_list_to_public_post_types() { } } add_action( 'rest_api_init', 'gutenberg_add_class_list_to_public_post_types' ); + +if ( ! function_exists( 'gutenberg_register_wp_rest_themes_stylesheet_directory_uri_field' ) ) { + /** + * Adds `stylesheet_uri` fields to WP_REST_Themes_Controller class. + * Core ticket: https://core.trac.wordpress.org/ticket/61021 + */ + function gutenberg_register_wp_rest_themes_stylesheet_directory_uri_field() { + register_rest_field( + 'theme', + 'stylesheet_uri', + array( + 'get_callback' => function ( $item ) { + if ( ! empty( $item['stylesheet'] ) ) { + $theme = wp_get_theme( $item['stylesheet'] ); + $current_theme = wp_get_theme(); + if ( $theme->get_stylesheet() === $current_theme->get_stylesheet() ) { + return get_stylesheet_directory_uri(); + } else { + return $theme->get_stylesheet_directory_uri(); + } + } + + return null; + }, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'The uri for the theme\'s stylesheet directory.', 'gutenberg' ), + 'format' => 'uri', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + } +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_themes_stylesheet_directory_uri_field' ); + +if ( ! function_exists( 'gutenberg_register_wp_rest_themes_template_directory_uri_field' ) ) { + /** + * Adds `template_uri` fields to WP_REST_Themes_Controller class. + * Core ticket: https://core.trac.wordpress.org/ticket/61021 + */ + function gutenberg_register_wp_rest_themes_template_directory_uri_field() { + register_rest_field( + 'theme', + 'template_uri', + array( + 'get_callback' => function ( $item ) { + if ( ! empty( $item['stylesheet'] ) ) { + $theme = wp_get_theme( $item['stylesheet'] ); + $current_theme = wp_get_theme(); + if ( $theme->get_stylesheet() === $current_theme->get_stylesheet() ) { + return get_template_directory_uri(); + } else { + return $theme->get_template_directory_uri(); + } + } + + return null; + }, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'The uri for the theme\'s template directory. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme\'s stylesheet directory.', 'gutenberg' ), + 'format' => 'uri', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + } +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_themes_template_directory_uri_field' ); diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index f45c787e45589b..76fd5b7609ac70 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -38,6 +38,7 @@ import { TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { setImmutably } from '../../utils/object'; import MediaReplaceFlow from '../media-replace-flow'; import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; const IMAGE_BACKGROUND_TYPE = 'image'; const DEFAULT_CONTROLS = { @@ -201,6 +202,23 @@ function BackgroundImageToolsPanelItem( { ...inheritedValue?.background?.backgroundImage, }; + const { backgroundImageURL } = useSelect( + ( select ) => { + const { getThemeFileURI } = unlock( select( blockEditorStore ) ); + let file = url; + if ( + !! style?.background?.backgroundImage?.url && + style?.background?.backgroundImage?.source === 'theme' + ) { + file = getThemeFileURI( style.background.backgroundImage.url ); + } + return { + backgroundImageURL: file, + }; + }, + [ url ] + ); + const replaceContainerRef = useRef(); const { createErrorNotice } = useDispatch( noticesStore ); @@ -293,7 +311,7 @@ function BackgroundImageToolsPanelItem( { > } variant="secondary" diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index e0de34cf2280e2..0ac01d08ecf574 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -368,6 +368,15 @@ export function useSettingsForBlockElement( } } ); + [ 'backgroundImage', 'backgroundSize' ].forEach( ( key ) => { + if ( ! supportedStyles.includes( key ) ) { + updatedSettings.background = { + ...updatedSettings.background, + [ key ]: false, + }; + } + } ); + updatedSettings.shadow = supportedStyles.includes( 'shadow' ) ? updatedSettings.shadow : false; diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 95de3d4007079c..ec24a8e08c1e9d 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -990,6 +990,34 @@ describe( 'global styles renderer', () => { 'letter-spacing: 2px', ] ); } ); + + it( 'should process styles and set CSS default values', () => { + const styles = { + background: { + backgroundImage: { + url: 'image.jpg', + source: 'theme', + }, + }, + }; + const editorSettings = { + themeDirURI: 'http://example.com/theme', + }; + + expect( + getStylesDeclarations( + styles, + '.wp-block', + false, + {}, + false, + editorSettings + ) + ).toEqual( [ + "background-image: url( 'http://example.com/theme/image.jpg' )", + 'background-size: cover', + ] ); + } ); } ); describe( 'processCSSNesting', () => { diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 23bb88efc9991e..637f5607196d3c 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -12,6 +12,7 @@ import { useSelect } from '@wordpress/data'; import { useContext, useMemo } from '@wordpress/element'; import { getCSSRules } from '@wordpress/style-engine'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { isURL } from '@wordpress/url'; /** * Internal dependencies @@ -35,6 +36,7 @@ import { LAYOUT_DEFINITIONS } from '../../layouts/definitions'; import { getValueFromObjectPath, setImmutably } from '../../utils/object'; import BlockContext from '../block-context'; import { unlock } from '../../lock-unlock'; +import useResolveThemeFileURIs from './use-resolve-theme-file-uris'; // List of block support features that can have their related styles // generated under their own feature level selector rather than the block's. @@ -314,6 +316,7 @@ const getFeatureDeclarations = ( selectors, styles ) => { * @param {Object} tree A theme.json tree containing layout definitions. * * @param {boolean} isTemplate Whether the entity being edited is a full template or a pattern. + * @param {Object} editorSettings Current editor settings. * @return {Array} An array of style declarations. */ export function getStylesDeclarations( @@ -947,19 +950,19 @@ export const toStyles = ( } ); } - // Process the remaining block styles (they use either normal block class or __experimentalSelector). - const declarations = getStylesDeclarations( - styles, - selector, - useRootPaddingAlign, - tree, - isTemplate - ); - if ( declarations?.length ) { - ruleset += `:where(${ selector }){${ declarations.join( - ';' - ) };}`; - } + // Process the remaining block styles (they use either normal block class or __experimentalSelector). + const declarations = getStylesDeclarations( + styles, + selector, + useRootPaddingAlign, + tree, + isTemplate + ); + if ( declarations?.length ) { + ruleset += `:where(${ selector }){${ declarations.join( + ';' + ) };}`; + } // Check for pseudo selector in `styles` and handle separately. const pseudoSelectorStyles = Object.entries( styles ).filter( @@ -1217,11 +1220,15 @@ export function processCSSNesting( css, blockSelector ) { */ export function useGlobalStylesOutputWithConfig( mergedConfig = {} ) { const [ blockGap ] = useGlobalSetting( 'spacing.blockGap' ); + const resolvedConfig = useResolveThemeFileURIs( mergedConfig ); const hasBlockGapSupport = blockGap !== null; const hasFallbackGapSupport = ! hasBlockGapSupport; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback styles support. - const disableLayoutStyles = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return !! getSettings().disableLayoutStyles; + + const { disableLayoutStyles } = useSelect( ( select ) => { + const _settings = select( blockEditorStore ).getSettings(); + return { + disableLayoutStyles: !! _settings.disableLayoutStyles, + }; } ); const blockContext = useContext( BlockContext ); @@ -1231,10 +1238,10 @@ export function useGlobalStylesOutputWithConfig( mergedConfig = {} ) { const { getBlockStyles } = useSelect( blocksStore ); return useMemo( () => { - if ( ! mergedConfig?.styles || ! mergedConfig?.settings ) { + if ( ! resolvedConfig?.styles || ! resolvedConfig?.settings ) { return []; } - const updatedConfig = updateConfigWithSeparator( mergedConfig ); + const updatedConfig = updateConfigWithSeparator( resolvedConfig ); const blockSelectors = getBlockSelectors( getBlockTypes(), @@ -1297,7 +1304,7 @@ export function useGlobalStylesOutputWithConfig( mergedConfig = {} ) { }, [ hasBlockGapSupport, hasFallbackGapSupport, - mergedConfig, + resolvedConfig, disableLayoutStyles, isTemplate, getBlockStyles, diff --git a/packages/block-editor/src/components/global-styles/use-resolve-theme-file-uris.js b/packages/block-editor/src/components/global-styles/use-resolve-theme-file-uris.js new file mode 100644 index 00000000000000..f235cf3548b709 --- /dev/null +++ b/packages/block-editor/src/components/global-styles/use-resolve-theme-file-uris.js @@ -0,0 +1,271 @@ +/** + * WordPress dependencies + */ +import { isURL, isValidPath } from '@wordpress/url'; +import { useSelect } from '@wordpress/data'; +import { useEffect, useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as blockEditorStore } from '../../store'; + +const resolvedURIsCache = new Map(); + +function isRelativePath( url ) { + return isValidPath( url ) && ! isURL( url ); +} + +function getThemeFileURIs( file, stylesheetURI, templateURI ) { + file = file.trim(); + file = file.startsWith( '/' ) ? file : `/${ file }`; + return [ `${ stylesheetURI }${ file }`, `${ templateURI }${ file }` ]; +} + +function fetchThemeFileURI( url, { onSuccess, onError, onFinally } ) { + if ( ! isURL( url ) || ! window?.fetch ) { + return null; + } + return window + .fetch( url, { method: 'HEAD', mode: 'same-origin' } ) + .then( onSuccess ) + .catch( onError ) + .finally( onFinally ); +} + +async function resolveThemeJSONFileURIs( + relativePaths, + stylesheetURI, + templateURI, + themeStylesheet +) { + const fetchPromises = []; + const unresolvedPaths = [ ...relativePaths ]; + if ( relativePaths?.length ) { + relativePaths.forEach( ( path ) => { + const cacheKey = `${ themeStylesheet }-${ path }`; + if ( ! resolvedURIsCache.has( cacheKey ) ) { + const [ activeThemeFileURL, defaultThemeFileURL ] = + getThemeFileURIs( path, stylesheetURI, templateURI ); + fetchPromises.push( + fetchThemeFileURI( activeThemeFileURL, { + onSuccess: () => { + resolvedURIsCache.set( + cacheKey, + activeThemeFileURL + ); + }, + onError: () => { + resolvedURIsCache.set( + cacheKey, + defaultThemeFileURL + ); + }, + onFinally: () => { + const index = unresolvedPaths.indexOf( path ); + unresolvedPaths.splice( index, 1 ); + }, + } ) + ); + } + } ); + } + /* + + // Top-level styles. + if ( isRelativePath( config?.styles?.background?.backgroundImage?.url ) ) { + const cacheKey = `${ themeStylesheet }-${ config.styles.background.backgroundImage.url }`; + if ( ! resolvedURIsCache.has( cacheKey ) ) { + const [ activeThemeFileURL, defaultThemeFileURL ] = + getThemeFileURIs( + config.styles.background.backgroundImage.url, + stylesheetURI, + templateURI + ); + + fetchPromises.push( + fetchThemeFileURI( activeThemeFileURL, { + onSuccess: () => { + resolvedURIsCache.set( cacheKey, activeThemeFileURL ); + }, + onError: () => { + resolvedURIsCache.set( cacheKey, defaultThemeFileURL ); + }, + } ) + ); + } + } + + // Block styles. + if ( + config?.styles?.blocks && + Object.keys( config.styles.blocks ).length + ) { + Object.entries( config.styles.blocks ).forEach( + ( [ , blockStyles ] ) => { + if ( + isRelativePath( + blockStyles?.background?.backgroundImage?.url + ) + ) { + const cacheKey = `${ themeStylesheet }-${ blockStyles?.background?.backgroundImage?.url }`; + if ( ! resolvedURIsCache.has( cacheKey ) ) { + const [ activeThemeFileURL, defaultThemeFileURL ] = + getThemeFileURIs( + blockStyles.background.backgroundImage.url, + stylesheetURI, + templateURI + ); + fetchPromises.push( + fetchThemeFileURI( activeThemeFileURL, { + onSuccess: () => { + resolvedURIsCache.set( + cacheKey, + activeThemeFileURL + ); + }, + onError: () => { + resolvedURIsCache.set( + cacheKey, + defaultThemeFileURL + ); + }, + } ) + ); + } + } + } + ); + } +*/ + + await Promise.allSettled( fetchPromises ); + return unresolvedPaths; +} + +function getUnresolvedThemeFilePaths( config, themeStylesheet ) { + const paths = []; + if ( isRelativePath( config?.styles?.background?.backgroundImage?.url ) ) { + const cacheKey = `${ themeStylesheet }-${ config.styles.background.backgroundImage.url }`; + if ( ! resolvedURIsCache.has( cacheKey ) ) { + paths.push( config.styles.background.backgroundImage.url ); + } + } + + if ( + config?.styles?.blocks && + Object.keys( config.styles.blocks ).length + ) { + Object.entries( config.styles.blocks ).forEach( + ( [ , blockStyles ] ) => { + if ( + isRelativePath( + blockStyles?.background?.backgroundImage?.url + ) + ) { + const cacheKey = `${ themeStylesheet }-${ blockStyles?.background?.backgroundImage?.url }`; + if ( ! resolvedURIsCache.has( cacheKey ) ) { + paths.push( blockStyles.background.backgroundImage.url ); + } + } + } + ); + } + + return paths; +} + +function setResolvedThemeFilePaths( config, themeStylesheet ) { + if ( isRelativePath( config?.styles?.background?.backgroundImage?.url ) ) { + const cacheKey = `${ themeStylesheet }-${ config.styles.background.backgroundImage.url }`; + if ( resolvedURIsCache.has( cacheKey ) ) { + config.styles.background.backgroundImage.url = + resolvedURIsCache.get( cacheKey ); + } + } + + if ( + config?.styles?.blocks && + Object.keys( config.styles.blocks ).length + ) { + Object.entries( config.styles.blocks ).forEach( + ( [ blockName, blockStyles ] ) => { + if ( + isRelativePath( + blockStyles?.background?.backgroundImage?.url + ) + ) { + const cacheKey = `${ themeStylesheet }-${ blockStyles?.background?.backgroundImage?.url }`; + if ( resolvedURIsCache.has( cacheKey ) ) { + config.styles.blocks[ + blockName + ].background.backgroundImage.url = + resolvedURIsCache.get( cacheKey ); + } + } + } + ); + } + + return config; +} + +/* + Ideas: + Check that this needs to be run before running it by parsing out URLs early? + Can a function returning an array be a useEffect dependency? + */ +export default function useResolveThemeFileURIs( mergedConfig ) { + const { stylesheetURI, templateURI, themeStylesheet } = useSelect( + ( select ) => { + const { + stylesheet_uri: _stylesheetURI, + template_uri: _templateURI, + stylesheet: _themeStylesheet, + } = unlock( select( blockEditorStore ) ).getCurrentTheme(); + return { + stylesheetURI: _stylesheetURI, + templateURI: _templateURI, + themeStylesheet: _themeStylesheet, + }; + } + ); + + const [ unresolvedPaths, setUnresolvedPaths ] = useState( + getUnresolvedThemeFilePaths( mergedConfig, themeStylesheet ) + ); + + useEffect( () => { + if ( + ! mergedConfig?.styles || + ! unresolvedPaths?.length || + ! stylesheetURI || + ! templateURI || + ! themeStylesheet + ) { + return; + } + + ( async () => { + try { + const _unresolvedPaths = await resolveThemeJSONFileURIs( + unresolvedPaths, + stylesheetURI, + templateURI, + themeStylesheet + ); + setUnresolvedPaths( _unresolvedPaths ); + } catch ( err ) {} + } )(); + }, [ + mergedConfig, + unresolvedPaths, + setUnresolvedPaths, + stylesheetURI, + templateURI, + themeStylesheet, + ] ); + + return setResolvedThemeFilePaths( mergedConfig, themeStylesheet ); +} diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 3e23efcbf5fc96..b3bd15b53c8dbd 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -4,6 +4,7 @@ import { getBlockSupport } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { useCallback } from '@wordpress/element'; +import { safeDecodeURI, isURL } from '@wordpress/url'; /** * Internal dependencies @@ -16,6 +17,8 @@ import { useHasBackgroundPanel, hasBackgroundImageValue, } from '../components/global-styles/background-panel'; +import { ROOT_BLOCK_SELECTOR } from '../components/global-styles/utils'; +import { unlock } from '../lock-unlock'; export const BACKGROUND_SUPPORT_KEY = 'background'; @@ -50,56 +53,6 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) { return !! support?.[ feature ]; } -export function setBackgroundStyleDefaults( backgroundStyle ) { - if ( ! backgroundStyle ) { - return; - } - - const backgroundImage = backgroundStyle?.backgroundImage; - let backgroundStylesWithDefaults; - - // Set block background defaults. - if ( !! backgroundImage?.url ) { - if ( ! backgroundStyle?.backgroundSize ) { - backgroundStylesWithDefaults = { - backgroundSize: 'cover', - }; - } - - if ( - 'contain' === backgroundStyle?.backgroundSize && - ! backgroundStyle?.backgroundPosition - ) { - backgroundStylesWithDefaults = { - backgroundPosition: 'center', - }; - } - } - - return backgroundStylesWithDefaults; -} - -function useBlockProps( { name, style } ) { - if ( - ! hasBackgroundSupport( name ) || - ! style?.background?.backgroundImage - ) { - return; - } - - const backgroundStyles = setBackgroundStyleDefaults( style?.background ); - - if ( ! backgroundStyles ) { - return; - } - - return { - style: { - ...backgroundStyles, - }, - }; -} - /** * Generates a CSS class name if an background image is set. * @@ -181,8 +134,55 @@ export function BackgroundImagePanel( { ); } +function useBlockProps( { name, style } ) { + const { backgroundImageURL } = useSelect( + ( select ) => { + const { getThemeFileURI } = unlock( select( blockEditorStore ) ); + let file = style?.background?.backgroundImage?.url; + if ( + !! style?.background?.backgroundImage?.url && ! isURL( style?.background?.backgroundImage?.url ) + ) { + file = getThemeFileURI( style.background.backgroundImage.url ); + } + return { + backgroundImageURL: file, + }; + }, + [ style?.background?.backgroundImage ] + ); + + if ( + ! hasBackgroundSupport( name, 'backgroundImage' ) || + ! backgroundImageURL + ) { + return; + } + const newBackgroundStyles = {}; + if ( ! style?.background?.backgroundSize ) { + newBackgroundStyles.backgroundSize = 'cover'; + } + + if ( + 'contain' === style?.background?.backgroundSize && + ! style?.background?.backgroundPosition + ) { + newBackgroundStyles.backgroundPosition = 'center'; + } + + return { + style: { + // @TODO this should be backgroundImage. How to do that? + // Also, maybe consider reinstating https://github.com/WordPress/gutenberg/blob/fc98542a7dbba194bb4096d49cd0bd093b63f43e/packages/block-editor/src/hooks/background.js#L82 + backgroundImage: `url( '${ encodeURI( + safeDecodeURI( backgroundImageURL ) + ) }' )`, + ...newBackgroundStyles, + }, + }; +} + export default { - useBlockProps, attributeKeys: [ 'style' ], hasSupport: hasBackgroundSupport, + useBlockProps, }; diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index ec5cf29b49c5a6..d2f4f5fb5ed1ce 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -50,7 +50,6 @@ createBlockEditFilter( createBlockListBlockFilter( [ align, textAlign, - background, style, color, dimensions, @@ -60,6 +59,7 @@ createBlockListBlockFilter( [ border, position, childLayout, + background, ] ); createBlockSaveFilter( [ align, diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index f10fcc4df2c726..b42f1390c27927 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -35,6 +35,7 @@ import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; import { selectBlockPatternsKey, reusableBlocksSelectKey, + getCurrentThemeKey, } from './store/private-keys'; import { requiresWrapperOnCopy } from './components/writing-flow/utils'; import { PrivateRichText } from './components/rich-text/'; @@ -76,4 +77,5 @@ lock( privateApis, { requiresWrapperOnCopy, PrivateRichText, reusableBlocksSelectKey, + getCurrentThemeKey, } ); diff --git a/packages/block-editor/src/store/private-keys.js b/packages/block-editor/src/store/private-keys.js index f48612e7491c9c..7a48d59753d67d 100644 --- a/packages/block-editor/src/store/private-keys.js +++ b/packages/block-editor/src/store/private-keys.js @@ -1,2 +1,3 @@ export const selectBlockPatternsKey = Symbol( 'selectBlockPatternsKey' ); export const reusableBlocksSelectKey = Symbol( 'reusableBlocksSelect' ); +export const getCurrentThemeKey = Symbol( 'getCurrentTheme' ); diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index bb7f8fed189549..71cf90c1de860c 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -27,6 +27,7 @@ import { unlock } from '../lock-unlock'; import { selectBlockPatternsKey, reusableBlocksSelectKey, + getCurrentThemeKey, } from './private-keys'; export { getBlockSettings } from './get-block-settings'; @@ -496,3 +497,13 @@ export function getTemporarilyEditingAsBlocks( state ) { export function getTemporarilyEditingFocusModeToRevert( state ) { return state.temporarilyEditingFocusModeRevert; } + +const EMPTY_OBJECT = {}; +export const getCurrentTheme = createRegistrySelector( + ( select ) => ( state, file ) => { + const getCurrentTheme = state.settings[ getCurrentThemeKey ]; + return getCurrentTheme + ? getCurrentTheme( select ) + : EMPTY_OBJECT; + } +); diff --git a/packages/core-data/src/entity-types/theme.ts b/packages/core-data/src/entity-types/theme.ts index 761c6461d4aca8..ed78906220723d 100644 --- a/packages/core-data/src/entity-types/theme.ts +++ b/packages/core-data/src/entity-types/theme.ts @@ -12,10 +12,18 @@ declare module './base-entity-records' { * The theme's stylesheet. This uniquely identifies the theme. */ stylesheet: string; + /** + * The uri for the theme's stylesheet directory + */ + stylesheet_uri: string; /** * The theme's template. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme's stylesheet. */ template: string; + /** + * The uri for the theme's template directory. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme's stylesheet directory. + */ + temnplate_uri: string; /** * The theme author. */ diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index bcd08856141831..6e16650a752956 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -101,6 +101,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { const { allowRightClickOverrides, blockTypes, + currentTheme, focusMode, hasFixedToolbar, isDistractionFree, @@ -122,7 +123,9 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { getEntityRecord, getUserPatternCategories, getBlockPatternCategories, + getCurrentTheme, } = select( coreStore ); + const _currentTheme = getCurrentTheme(); const { get } = select( preferencesStore ); const { getBlockTypes } = select( blocksStore ); const { getBlocksByName, getBlockAttributes } = @@ -155,6 +158,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { postType, postId )?._links?.hasOwnProperty( 'wp:action-unfiltered-html' ), + currentTheme: _currentTheme, focusMode: get( 'core', 'focusMode' ), hasFixedToolbar: get( 'core', 'fixedToolbar' ) || ! isLargeViewport, @@ -290,6 +294,10 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { // Check these two properties: they were not present in the site editor. __experimentalCreatePageEntity: createPageEntity, __experimentalUserCanCreatePages: userCanCreatePages, + [ unlock( privateApis ).getCurrentThemeKey ]: ( select ) => + unlock( select( coreStore ) ).getCurrentTheme( + postType + ), pageOnFront, pageForPosts, __experimentalPreferPatternsOnRoot: postType === 'wp_template', @@ -308,6 +316,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { }, [ allowedBlockTypes, allowRightClickOverrides, + currentTheme, focusMode, forceDisableFocusMode, hasFixedToolbar, diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php index 165a65204793d1..865b698f283d00 100644 --- a/phpunit/block-supports/background-test.php +++ b/phpunit/block-supports/background-test.php @@ -157,8 +157,7 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', ), 'backgroundRepeat' => 'no-repeat', 'backgroundSize' => 'contain', @@ -189,8 +188,7 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', ), ), 'expected_wrapper' => '
Content
', @@ -204,8 +202,7 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', ), ), 'expected_wrapper' => '
Content
', @@ -213,4 +210,57 @@ public function data_background_block_support() { ), ); } + + /** + * Tests generating background styles. + * + * @covers ::gutenberg_get_background_support_styles + * + * @dataProvider data_get_background_support_styles + * + * @param mixed $background_style The background styles within the block attributes. + * @param string $expected_css Expected markup for the block wrapper. + */ + public function test_get_background_support_styles( $background_style, $expected_css ) { + switch_theme( 'block-theme' ); + $actual = gutenberg_get_background_support_styles( $background_style )['css'] ?? null; + + $this->assertEquals( + $expected_css, + $actual, + 'Background CSS should be correct.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_background_support_styles() { + return array( + 'no css generated with no block styles' => array( + 'background_style' => array( + 'backgroundImage' => array(), + ), + 'expected_css' => null, + ), + 'css generated with theme background image path' => array( + 'background_style' => array( + 'backgroundImage' => array( + 'url' => '/assets/image/not_there.png', + ), + ), + 'expected_css' => "background-image:url('http://localhost:8889/wp-content/plugins/gutenberg/phpunit/data/themedir1/block-theme/assets/image/not_there.png');", + ), + 'css generated with theme background image path and no preceding slash' => array( + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'assets/image/not_there.png', + ), + ), + 'expected_css' => "background-image:url('http://localhost:8889/wp-content/plugins/gutenberg/phpunit/data/themedir1/block-theme/assets/image/not_there.png');", + ), + ); + } } diff --git a/test/emptytheme/styles/variation.json b/test/emptytheme/styles/variation.json index 06f672f6fd25d7..64cbf060755792 100644 --- a/test/emptytheme/styles/variation.json +++ b/test/emptytheme/styles/variation.json @@ -1,5 +1,6 @@ { "version": 3, + "title": "hello", "settings": { "color": { "palette": [ @@ -12,10 +13,22 @@ } }, "styles": { + "background": { + "backgroundImage": { + "url": "img/2.jpg" + }, + "backgroundSize": "contain" + }, "blocks": { - "core/post-title": { - "typography": { - "fontWeight": "700" + "core/group": { + "color": { + "text": "yellow" + }, + "background": { + "backgroundImage": { + "url": "img/3.jpg" + }, + "backgroundSize": "auto" } } } diff --git a/test/emptytheme/theme.json b/test/emptytheme/theme.json index 09f11eeeff8cfb..295ee4c8f0ff59 100644 --- a/test/emptytheme/theme.json +++ b/test/emptytheme/theme.json @@ -1,19 +1,15 @@ { - "$schema": "../../schemas/json/theme.json", - "version": 3, + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, "settings": { - "appearanceTools": true, - "layout": { - "contentSize": "840px", - "wideSize": "1100px" - } + "appearanceTools": true }, - "customTemplates": [ - { - "name": "custom-template", - "title": "Custom", - "postTypes": [ "post" ] + "styles": { + "background": { + "backgroundImage": { + "url": "img/1.jpg" + }, + "backgroundSize": "cover" } - ], - "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] + } }