Repository navigation
Add custom CSS support for individual block instances #73959
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
b47ea62
75fdc6f
bfa879c
5b58d33
d4aa543
f4a67ab
ade3aaa
84dafba
2f0256b
218155f
06712df
6fa349b
82884e4
c9e5cbe
f14260c
82dde70
ad1730a
ac76315
af22573
24cab8d
139af04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| <?php | ||
| /** | ||
| * Custom CSS block support. | ||
| * | ||
| * @package gutenberg | ||
| */ | ||
|
|
||
| /** | ||
| * Render the custom CSS stylesheet and add class name to block as required. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param array $parsed_block The parsed block. | ||
| * @return array The same parsed block with custom CSS class name added if appropriate. | ||
| */ | ||
| function gutenberg_render_custom_css_support_styles( $parsed_block ) { | ||
| $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] ); | ||
|
|
||
| if ( ! block_has_support( $block_type, 'customCSS', true ) ) { | ||
| return $parsed_block; | ||
| } | ||
|
|
||
| $custom_css = $parsed_block['attrs']['style']['css'] ?? null; | ||
|
|
||
| if ( empty( $custom_css ) ) { | ||
| return $parsed_block; | ||
| } | ||
|
|
||
| // Generate a unique class name for this block instance. | ||
| $class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' ); | ||
| $updated_class_name = isset( $parsed_block['attrs']['className'] ) | ||
| ? $parsed_block['attrs']['className'] . " $class_name" | ||
| : $class_name; | ||
|
|
||
| _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); | ||
|
|
||
| // Process the custom CSS using the same method as global styles. | ||
| $selector = '.' . $class_name; | ||
| $processed_css = WP_Theme_JSON_Gutenberg::process_blocks_custom_css( $custom_css, $selector ); | ||
|
|
||
| if ( ! empty( $processed_css ) ) { | ||
| /* | ||
| * Register and add inline style for block custom CSS. | ||
| * The style depends on global-styles to ensure custom CSS loads after | ||
| * and can override global styles. | ||
| */ | ||
| wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); | ||
| wp_add_inline_style( 'wp-block-custom-css', $processed_css ); | ||
| } | ||
|
|
||
| return $parsed_block; | ||
| } | ||
|
|
||
| /** | ||
| * Enqueues the block custom CSS styles. | ||
| * | ||
| * @since 7.0.0 | ||
| */ | ||
| function gutenberg_enqueue_block_custom_css() { | ||
| wp_enqueue_style( 'wp-block-custom-css' ); | ||
| } | ||
|
|
||
| /** | ||
| * Applies the custom CSS class name to the block's rendered HTML. | ||
| * | ||
| * The class name is generated in `gutenberg_render_custom_css_support_styles` | ||
| * and stored in block attributes. This filter adds it to the actual markup. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $block_content Rendered block content. | ||
| * @param array $block Block object. | ||
| * @return string Filtered block content. | ||
| */ | ||
| function gutenberg_render_custom_css_class_name( $block_content, $block ) { | ||
| $class_string = $block['attrs']['className'] ?? ''; | ||
| preg_match( '/\bwp-custom-css-\S+\b/', $class_string, $matches ); | ||
|
|
||
| if ( empty( $matches ) ) { | ||
| return $block_content; | ||
| } | ||
|
|
||
| $tags = new WP_HTML_Tag_Processor( $block_content ); | ||
|
|
||
| if ( $tags->next_tag() ) { | ||
| $tags->add_class( $matches[0] ); | ||
| } | ||
|
|
||
| return $tags->get_updated_html(); | ||
| } | ||
|
|
||
| add_filter( 'render_block', 'gutenberg_render_custom_css_class_name', 10, 2 ); | ||
| add_filter( 'render_block_data', 'gutenberg_render_custom_css_support_styles', 10, 1 ); | ||
| add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_block_custom_css', 1 ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1494,7 +1494,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' | |
| * @param string $selector The selector to nest. | ||
| * @return string The processed CSS. | ||
| */ | ||
| protected function process_blocks_custom_css( $css, $selector ) { | ||
| public static function process_blocks_custom_css( $css, $selector ) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this one need to be public because its used in lib/block-supports/custom-css.php
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, correct. |
||
| $processed_css = ''; | ||
|
|
||
| if ( empty( $css ) ) { | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { useMemo } from '@wordpress/element'; | ||
| import { useSelect } from '@wordpress/data'; | ||
| import { useInstanceId } from '@wordpress/compose'; | ||
| import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; | ||
| import { __, sprintf } from '@wordpress/i18n'; | ||
| import { processCSSNesting } from '@wordpress/global-styles-engine'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import InspectorControls from '../components/inspector-controls'; | ||
| import AdvancedPanel from '../components/global-styles/advanced-panel'; | ||
| import { cleanEmptyObject, useStyleOverride } from './utils'; | ||
| import { store as blockEditorStore } from '../store'; | ||
|
|
||
| // Stable reference for useInstanceId. | ||
| const CUSTOM_CSS_INSTANCE_REFERENCE = {}; | ||
|
|
||
| /** | ||
| * Inspector control for custom CSS. | ||
| * | ||
| * @param {Object} props Component props. | ||
| * @param {string} props.blockName Block name. | ||
| * @param {Function} props.setAttributes Function to set block attributes. | ||
| * @param {Object} props.style Block style attribute. | ||
| */ | ||
| function CustomCSSControl( { blockName, setAttributes, style } ) { | ||
| const blockType = getBlockType( blockName ); | ||
|
|
||
| function onChange( newStyle ) { | ||
| setAttributes( { | ||
| style: cleanEmptyObject( newStyle ), | ||
| } ); | ||
| } | ||
|
|
||
| const cssHelpText = sprintf( | ||
| // translators: %s: is the name of a block e.g., 'Image' or 'Quote'. | ||
| __( | ||
| 'Add your own CSS to customize the appearance of the %s block. You do not need to include a CSS selector, just add the property and value.' | ||
|
aaronrobertshaw marked this conversation as resolved.
Resolved
|
||
| ), | ||
| blockType?.title | ||
| ); | ||
|
|
||
| return ( | ||
| <InspectorControls group="advanced"> | ||
| <AdvancedPanel | ||
| value={ style } | ||
| onChange={ onChange } | ||
| inheritedValue={ style } | ||
| help={ cssHelpText } | ||
| /> | ||
| </InspectorControls> | ||
| ); | ||
| } | ||
|
|
||
| function CustomCSSEdit( { clientId, name, setAttributes } ) { | ||
| const style = useSelect( | ||
| ( select ) => | ||
| select( blockEditorStore ).getBlockAttributes( clientId )?.style || | ||
| {}, | ||
| [ clientId ] | ||
| ); | ||
|
|
||
| return ( | ||
| <CustomCSSControl | ||
| blockName={ name } | ||
| setAttributes={ setAttributes } | ||
| style={ style } | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Hook to handle custom CSS for a block in the editor. | ||
| * Generates a unique class and applies scoped CSS via style override. | ||
| * | ||
| * @param {Object} props Block props. | ||
| * @param {Object} props.style Block style attribute. | ||
| * @return {Object} Block props including className for custom CSS scoping. | ||
| */ | ||
| function useBlockProps( { style } ) { | ||
| const customCSS = style?.css; | ||
|
|
||
| const customCSSIdentifier = useInstanceId( | ||
| CUSTOM_CSS_INSTANCE_REFERENCE, | ||
| 'wp-custom-css' | ||
| ); | ||
|
|
||
| const customCSSSelector = `.${ customCSSIdentifier }`; | ||
|
|
||
| // Transform the custom CSS using the same logic as global styles. | ||
| const transformedCSS = useMemo( () => { | ||
| return processCSSNesting( customCSS, customCSSSelector ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I wonder whether we still need this now that nesting has solid browser support. Perhaps we can consider just wrapping the whole thing in the outer selector instead? (Probably not for this PR but something to be done globally wherever we're using this function)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nested CSS doesn't work in blocks anyway because you can't specify selectors.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nesting does work for individual block custom CSS if you use the same syntax as for global block custom CSS, eg. if you had the following to a group block that has nested paragraphs it will work: & p {
color: orange;
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah nice! |
||
| }, [ customCSS, customCSSSelector ] ); | ||
|
|
||
| // Inject the CSS via style override. | ||
| useStyleOverride( { css: transformedCSS } ); | ||
|
|
||
| // Only add the class if there's custom CSS. | ||
| if ( ! customCSS ) { | ||
| return {}; | ||
| } | ||
|
|
||
| return { | ||
| className: customCSSIdentifier, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Adds a marker class to blocks with custom CSS for server-side rendering. | ||
| * | ||
| * @param {Object} props Additional props applied to save element. | ||
| * @param {Object} blockType Block type definition. | ||
| * @param {Object} attributes Block's attributes. | ||
| * @return {Object} Filtered props applied to save element. | ||
| */ | ||
| function addSaveProps( props, blockType, attributes ) { | ||
| if ( ! hasBlockSupport( blockType, 'customCSS', true ) ) { | ||
| return props; | ||
| } | ||
|
|
||
| if ( ! attributes?.style?.css ) { | ||
| return props; | ||
| } | ||
|
|
||
| // Add a class to indicate this block has custom CSS. | ||
| // The actual CSS is rendered server-side using the render_block filter. | ||
| const className = props.className | ||
| ? `${ props.className } has-custom-css` | ||
| : 'has-custom-css'; | ||
|
|
||
| return { | ||
| ...props, | ||
| className, | ||
| }; | ||
| } | ||
|
|
||
| export default { | ||
| edit: CustomCSSEdit, | ||
| useBlockProps, | ||
| addSaveProps, | ||
| attributeKeys: [ 'style' ], | ||
| hasSupport( name ) { | ||
| return hasBlockSupport( name, 'customCSS', true ); | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just remind me again, the santitization takes place before going into the database, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, are there any
if ( ! current_user_can( 'edit_css' ) )checks required? Sorry, I'm doing a very quick test of this so forgive me if this was addressed already.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have added the same sanitisation at save as global custom CSS
I guess so, added now.