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;
}