diff --git a/lib/block-supports/duotone.php b/lib/block-supports/duotone.php index b4a8397d72ece..684dfadb5c03f 100644 --- a/lib/block-supports/duotone.php +++ b/lib/block-supports/duotone.php @@ -403,12 +403,14 @@ function gutenberg_get_duotone_filter_svg( $preset ) { * @param WP_Block_Type $block_type Block Type. */ function gutenberg_register_duotone_support( $block_type ) { - $has_duotone_support = false; + $should_add_attributes = false; if ( property_exists( $block_type, 'supports' ) ) { - $has_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); + $has_deprecated_experimental_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); + $has_duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), false ); + $should_add_attributes = $has_duotone_support || $has_deprecated_experimental_duotone_support; } - if ( $has_duotone_support ) { + if ( $should_add_attributes ) { if ( ! $block_type->attributes ) { $block_type->attributes = array(); } @@ -421,30 +423,7 @@ function gutenberg_register_duotone_support( $block_type ) { } } -/** - * Render out the duotone stylesheet and SVG. - * - * @param string $block_content Rendered block content. - * @param array $block Block object. - * @return string Filtered block content. - */ -function gutenberg_render_duotone_support( $block_content, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - - $duotone_support = false; - if ( $block_type && property_exists( $block_type, 'supports' ) ) { - $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); - } - - $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); - - if ( - ! $duotone_support || - ! $has_duotone_attribute - ) { - return $block_content; - } - +function gutenberg_render_deprecated_experimental_duotone_support( $block_content, $block, $duotone_support ) { $colors = $block['attrs']['style']['color']['duotone']; $filter_key = is_array( $colors ) ? implode( '-', $colors ) : $colors; $filter_preset = array( @@ -508,11 +487,133 @@ static function () use ( $filter_svg, $selector ) { ); } +function gutenberg_apply_duotone_support( $block_type, $block_attributes ) { + $attributes = array(); + $duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), false ); + $colors = _wp_array_get( $block_attributes, array( 'style', 'color', 'duotone' ), null ); + if ( $duotone_support && $colors ) { + $filter_key = is_array( $colors ) ? implode( '-', $colors ) : $colors; + $filter_preset = array( + 'slug' => wp_unique_id( sanitize_key( $filter_key . '-' ) ), + 'colors' => $colors, + ); + $filter_property = gutenberg_get_duotone_filter_property( $filter_preset ); + $filter_id = gutenberg_get_duotone_filter_id( $filter_preset ); + $attributes['style'] = sprintf( '--wp--style--filter: %s;', $filter_property ); + $attributes['class'] = $filter_id; // Required for Safari block re-render. + + if ( is_array( $colors ) ) { + add_action( + 'wp_footer', + static function () use ( $filter_preset, $filter_id ) { + $filter_svg = gutenberg_get_duotone_filter_svg( $filter_preset ); + echo $filter_svg; + + /* + * Safari renders elements incorrectly on first paint when the + * SVG filter comes after the content that it is filtering, so + * we force a repaint with a WebKit hack which solves the issue. + */ + global $is_safari; + if ( $is_safari ) { + /* + * Simply accessing el.offsetHeight flushes layout and style + * changes in WebKit without having to wait for setTimeout. + */ + printf( + '', + wp_json_encode( $filter_id ) + ); + } + } + ); + } + } + + return $attributes; +} + +// TODO: This may be able to be used for applying block supports to static blocks automatically. +// TODO: Use HTML Walker instead of regular expressions. +// TODO: Function inline docs mentioning class-wp-block-supports.php get_block_wrapper_attributes and apply_block_supports. +function gutenberg_render_block_wrapper_attributes( $block_content, $new_attributes = array() ) { + // This is hardcoded on purpose. + // We only support a fixed list of attributes. + $attributes_to_render = array( 'style', 'class' ); + foreach ( $attributes_to_render as $attribute_name ) { + if ( empty( $new_attributes[ $attribute_name ] ) ) { + continue; + } + + $attribute_pattern = '/'. preg_quote( $attribute_name, '/' ) . '="([^"]*)"/'; + $wrapper_pattern = '/<[^>]+?' . substr( $attribute_pattern, 1, -1 ) . '[^>]*>/'; + preg_match( + $wrapper_pattern, + $block_content, + $matches + ); + + if ( isset( $matches[1] ) ) { + // Replace the attribute. + $value = $new_attributes[ $attribute_name ] . ' ' . $matches[1]; + $block_content = preg_replace( + $attribute_pattern, + sprintf( '%s="%s"', $attribute_name, esc_attr( $value ) ), + $block_content, + 1 + ); + } else { + // No matching attribute was found or there was an error, so add a new attribute. + $value = $new_attributes[ $attribute_name ]; + $block_content = preg_replace( + '/(\s*\/?>)/', + sprintf( ' %s="%s"${0}', $attribute_name, esc_attr( $value ) ), + $block_content, + 1 + ); + } + } + + return $block_content; +} + +/** + * Render out the duotone stylesheet and SVG. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_duotone_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + if ( ! $block_type || ! property_exists( $block_type, 'supports' )) { + return $block_content; + } + + $duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), false ); + if ( $duotone_support && isset( $block['attrs']['style']['color']['duotone'] ) ) { + $new_attributes = gutenberg_apply_duotone_support( $block_type, $block['attrs'] ); + return gutenberg_render_block_wrapper_attributes( $block_content, $new_attributes ); + } + + $deprecated_experimental_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); + if ( $deprecated_experimental_duotone_support && isset( $block['attrs']['style']['color']['duotone'] ) ) { + return gutenberg_render_deprecated_experimental_duotone_support( $block_content, $block, $deprecated_experimental_duotone_support ); + } + + return $block_content; +} + // Register the block support. WP_Block_Supports::get_instance()->register( 'duotone', array( 'register_attribute' => 'gutenberg_register_duotone_support', + /* + * If static blocks were supported, we could do this instead of + * the render_block filter for the new filter.duotone support. + */ + // 'apply' => 'gutenberg_apply_duotone_support', ) ); diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index a33ad059cb665..1b33a97c61f33 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -13,6 +13,7 @@ import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useContext, createPortal } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -128,13 +129,19 @@ function DuotonePanel( { attributes, setAttributes } ) { * @return {Object} Filtered block settings. */ function addDuotoneAttributes( settings ) { - if ( ! hasBlockSupport( settings, 'color.__experimentalDuotone' ) ) { - return settings; - } + const hasDuotoneSupport = hasBlockSupport( settings, 'filter.duotone' ); + + const hasDeprecatedExperimentalDuotoneSupport = hasBlockSupport( + settings, + 'color.__experimentalDuotone' + ); + + const shouldAddAttributes = + hasDuotoneSupport || hasDeprecatedExperimentalDuotoneSupport; // Allow blocks to specify their own attribute definition with default // values if needed. - if ( ! settings.attributes.style ) { + if ( ! settings.attributes.style && shouldAddAttributes ) { Object.assign( settings.attributes, { style: { type: 'object', @@ -156,6 +163,11 @@ function addDuotoneAttributes( settings ) { const withDuotoneControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const hasDuotoneSupport = hasBlockSupport( + props.name, + 'filter.duotone' + ); + + const hasDeprecatedExperimentalDuotoneSupport = hasBlockSupport( props.name, 'color.__experimentalDuotone' ); @@ -168,18 +180,61 @@ const withDuotoneControls = createHigherOrderComponent( [ props.clientId ] ); + const shouldShowPanel = + ( hasDuotoneSupport || hasDeprecatedExperimentalDuotoneSupport ) && + ! isContentLocked; + return ( <> - { hasDuotoneSupport && ! isContentLocked && ( - - ) } + { shouldShowPanel && } ); }, 'withDuotoneControls' ); +function setInlineFilter( wrapperProps, value ) { + return { + ...wrapperProps, + style: { + '--wp--style--filter': value, + ...wrapperProps?.style, + }, + }; +} + +function DuotoneStyles( { BlockListBlock, ...props } ) { + const colors = props?.attributes?.style?.color?.duotone; + const id = `wp-duotone-${ useInstanceId( BlockListBlock ) }`; + const element = useContext( BlockList.__unstableElementContext ); + + if ( ! colors ) { + return ; + } + + if ( 'unset' === colors ) { + const wrapperProps = setInlineFilter( props.wrapperProps, 'unset' ); + return ; + } + + const wrapperProps = setInlineFilter( + props.wrapperProps, + `url( #${ id } )` + ); + + return ( + <> + { element && + createPortal( + , + element + ) } + + + ); +} + /** * Function that scopes a selector with another one. This works a bit like * SCSS nesting except the `&` operator isn't supported. @@ -211,6 +266,49 @@ function scopeSelector( scope, selector ) { return selectorsScoped.join( ', ' ); } +function DeprecatedExperimentalDuotoneStyles( { + BlockListBlock, + deprecatedExperimentalDuotoneSupport, + ...props +} ) { + deprecated( 'color.__experimentalDuotone selector block supports', { + since: '6.1', + alternative: 'filter.duotone block supports', + link: 'TODO', + } ); + + const colors = props?.attributes?.style?.color?.duotone; + + const id = `wp-duotone-${ useInstanceId( BlockListBlock ) }`; + + // Extra .editor-styles-wrapper specificity is needed in the editor + // since we're not using inline styles to apply the filter. We need to + // override duotone applied by global styles and theme.json. + const selectorsGroup = scopeSelector( + `.editor-styles-wrapper .${ id }`, + deprecatedExperimentalDuotoneSupport + ); + + const className = classnames( props?.className, id ); + + const element = useContext( BlockList.__unstableElementContext ); + + return ( + <> + { element && + createPortal( + , + element + ) } + + + ); +} + /** * Override the default block element to include duotone styles. * @@ -220,44 +318,30 @@ function scopeSelector( scope, selector ) { */ const withDuotoneStyles = createHigherOrderComponent( ( BlockListBlock ) => ( props ) => { - const duotoneSupport = getBlockSupport( + const duotoneSupport = getBlockSupport( props.name, 'filter.duotone' ); + if ( duotoneSupport ) { + return ( + + ); + } + + const deprecatedExperimentalDuotoneSupport = getBlockSupport( props.name, 'color.__experimentalDuotone' ); - const colors = props?.attributes?.style?.color?.duotone; - - if ( ! duotoneSupport || ! colors ) { - return ; + if ( deprecatedExperimentalDuotoneSupport ) { + return ( + + ); } - const id = `wp-duotone-${ useInstanceId( BlockListBlock ) }`; - - // Extra .editor-styles-wrapper specificity is needed in the editor - // since we're not using inline styles to apply the filter. We need to - // override duotone applied by global styles and theme.json. - const selectorsGroup = scopeSelector( - `.editor-styles-wrapper .${ id }`, - duotoneSupport - ); - - const className = classnames( props?.className, id ); - - const element = useContext( BlockList.__unstableElementContext ); - - return ( - <> - { element && - createPortal( - , - element - ) } - - - ); + return ; }, 'withDuotoneStyles' ); diff --git a/packages/block-library/src/cover/block.json b/packages/block-library/src/cover/block.json index 3bcc779a0f127..7f71039e4d3f5 100644 --- a/packages/block-library/src/cover/block.json +++ b/packages/block-library/src/cover/block.json @@ -97,6 +97,9 @@ "text": false, "background": false }, + "filter": { + "duotone": true + }, "typography": { "fontSize": true, "lineHeight": true, diff --git a/packages/block-library/src/cover/index.php b/packages/block-library/src/cover/index.php index e5a497fd76889..a111828275bd9 100644 --- a/packages/block-library/src/cover/index.php +++ b/packages/block-library/src/cover/index.php @@ -14,55 +14,71 @@ * @return string Returns the cover block markup, if useFeaturedImage is true. */ function render_block_core_cover( $attributes, $content ) { - if ( 'image' !== $attributes['backgroundType'] || false === $attributes['useFeaturedImage'] ) { - return $content; - } + $wrapper_attrs = array(); - if ( ! ( $attributes['hasParallax'] || $attributes['isRepeated'] ) ) { - $attr = array( - 'class' => 'wp-block-cover__image-background', - 'data-object-fit' => 'cover', - ); + if ( 'image' === $attributes['backgroundType'] && false !== $attributes['useFeaturedImage'] ) { + if ( ! ( $attributes['hasParallax'] || $attributes['isRepeated'] ) ) { + $attr = array( + 'class' => 'wp-block-cover__image-background', + 'data-object-fit' => 'cover', + ); - if ( isset( $attributes['focalPoint'] ) ) { - $object_position = round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%'; - $attr['data-object-position'] = $object_position; - $attr['style'] = 'object-position: ' . $object_position; - } + if ( isset( $attributes['focalPoint'] ) ) { + $object_position = round( $attributes['focalPoint']['x'] * 100 ) . '%' . ' ' . round( $attributes['focalPoint']['y'] * 100 ) . '%'; + $attr['data-object-position'] = $object_position; + $attr['style'] = 'object-position: ' . $object_position; + } - $image = get_the_post_thumbnail( null, 'post-thumbnail', $attr ); + $image = get_the_post_thumbnail( null, 'post-thumbnail', $attr ); - /* - * Inserts the featured image between the (1st) cover 'background' `span` and 'inner_container' `div`, - * and removes eventual withespace characters between the two (typically introduced at template level) - */ - $inner_container_start = '/]+wp-block-cover__inner-container[\s|"][^>]*>/U'; - if ( 1 === preg_match( $inner_container_start, $content, $matches, PREG_OFFSET_CAPTURE ) ) { - $offset = $matches[0][1]; - $content = substr( $content, 0, $offset ) . $image . substr( $content, $offset ); - } - } else { - if ( in_the_loop() ) { - update_post_thumbnail_cache(); - } - $current_featured_image = get_the_post_thumbnail_url(); + /* + * Inserts the featured image between the (1st) cover 'background' `span` and 'inner_container' `div`, + * and removes eventual withespace characters between the two (typically introduced at template level) + */ + $inner_container_start = '/]+wp-block-cover__inner-container[\s|"][^>]*>/U'; + if ( 1 === preg_match( $inner_container_start, $content, $matches, PREG_OFFSET_CAPTURE ) ) { + $offset = $matches[0][1]; + $content = substr( $content, 0, $offset ) . $image . substr( $content, $offset ); + } + } else { + if ( in_the_loop() ) { + update_post_thumbnail_cache(); + } + $current_featured_image = get_the_post_thumbnail_url(); + + $styles = 'background-image:url(' . esc_url( $current_featured_image ) . '); '; - $styles = 'background-image:url(' . esc_url( $current_featured_image ) . '); '; + if ( isset( $attributes['minHeight'] ) ) { + $height_unit = empty( $attributes['minHeightUnit'] ) ? 'px' : $attributes['minHeightUnit']; + $height = " min-height:{$attributes['minHeight']}{$height_unit}"; - if ( isset( $attributes['minHeight'] ) ) { - $height_unit = empty( $attributes['minHeightUnit'] ) ? 'px' : $attributes['minHeightUnit']; - $height = " min-height:{$attributes['minHeight']}{$height_unit}"; + $styles .= $height; + } - $styles .= $height; + $wrapper_attrs['style'] = $styles; } + } - $content = preg_replace( - '/class=\".*?\"/', - '${0} style="' . $styles . '"', - $content, - 1 - ); + $preg_class_pattern = '/class="([^"]*)"/'; + preg_match( + $preg_class_pattern, + $content, + $class_matches + ); + if ( isset( $class_matches[1] ) ) { + $classes = explode( ' ', $class_matches[1] ); + $classes = array_diff( $classes, array( 'wp-block-cover' ) ) ; + $classes = implode( ' ', $classes ); + $wrapper_attrs['class'] = $classes; } + $wrapper_attributes = get_block_wrapper_attributes( $wrapper_attrs ); + + $content = preg_replace( + $preg_class_pattern, + $wrapper_attributes, + $content, + 1 + ); return $content; } diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 95fab91763886..09c3ed7f00541 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -233,6 +233,15 @@ video.wp-block-cover__video-background { z-index: z-index(".wp-block-cover__image-background"); } +// Duotone styles +.wp-block-cover { + --wp--style--filter: initial; + > .wp-block-cover__image-background, + > .wp-block-cover__video-background { + filter: var(--wp--style--filter); + } +} + // Styles below only exist to support older versions of the block. // Versions that not had inner blocks and used an h2 heading had a section (and not a div) with a class wp-block-cover-image (and not a wp-block-cover). // We are using the previous referred differences to target old versions. diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 62edb882be0c9..a745b7202a2fa 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -85,7 +85,7 @@ "supports": { "anchor": true, "color": { - "__experimentalDuotone": "img, .components-placeholder", + "__experimentalDuotone": "img", "text": false, "background": false }, diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index b3499656c2c24..97e9e1c820ca6 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -176,3 +176,8 @@ figure.wp-block-image:not(.wp-block) { padding-right: 0; } } + +// Duotone editor styles +.wp-block-image .components-placeholder { + filter: var(--wp--style--filter); +} diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 9a9f175556e81..d78c03cb5c006 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -24,6 +24,59 @@ function render_block_core_image( $attributes, $content ) { $content = str_replace( ']+?' . substr( $preg_style_pattern, 1, -1 ) . '[^>]*>/', + $content, + $style_matches + ); + if ( isset( $style_matches[1] ) ) { + $attrs['style'] = $style_matches[1]; + $content = preg_replace( + $preg_style_pattern, + '', + $content, + 1 + ); + } + + $preg_class_pattern = '/class="([^"]*)"/'; + preg_match( + /* + * The figure should always have the `wp-block-image` class which + * means it should always be the first match, so we don't have to + * do the extra checks for if the class is for the figure or not. + */ + $preg_class_pattern, + $content, + $class_matches + ); + if ( isset( $class_matches[1] ) ) { + /* + * get_block_wrapper_attributes includes the `wp-block-image` class, + * so it needs to be removed first to avoid duplication. + */ + $classes = explode( ' ', $class_matches[1] ); + $classes = array_diff( $classes, array( 'wp-block-image' ) ) ; + $classes = implode( ' ', $classes ); + $attrs['class'] = $classes; + + $wrapper_attributes = get_block_wrapper_attributes( $attrs ); + + $content = preg_replace( + $preg_class_pattern, + $wrapper_attributes, + $content, + 1 + ); + } + return $content; }