Skip to content
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

Duotone: Limit SVG filter output to used filters #48995

Merged
merged 24 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7aa4c4a
Adds WP_Duotone class to set-up preset and theme-defined blocks using…
jeryj Mar 7, 2023
23b631b
Fix incorrect filter name
jeryj Mar 8, 2023
2f63ab2
Check if duotone filter is applied on a block in theme.json
jeryj Mar 9, 2023
70581a1
Rename duotone class static variables and functions
jeryj Mar 9, 2023
a024ddb
Set WP_Duotone:: array and only output those via wp_footer
jeryj Mar 9, 2023
0256548
Only output used duotone presets
jeryj Mar 9, 2023
2c283cc
Cleanup, removal, and only output used SVG filters and CSS
jeryj Mar 10, 2023
460a413
Support var:preset|duotone|default-filter syntax for block name styles
jeryj Mar 10, 2023
2d0e235
Refactor to handle both duotone pipe and css var style syntax for duo…
jeryj Mar 10, 2023
5f47de7
Renamed output_presets to output, as it also includes custom dutone o…
jeryj Mar 10, 2023
c39504b
Fix theme json vs block styles duotone order of application
jeryj Mar 10, 2023
1f13855
Renamed filter to be more accurate, but should refactor out
jeryj Mar 10, 2023
0c67759
Simplify getting global styles presets
jeryj Mar 13, 2023
e41b1e6
Undo theme json class duotone refactoring and remove filter
jeryj Mar 13, 2023
45426d2
Remove unused code
jeryj Mar 13, 2023
3d224bf
Rename save_ to set_ class method names
jeryj Mar 14, 2023
476fe04
Fix duotone test to use real global preset value
jeryj Mar 14, 2023
c23a4a4
Tests for WP_Duotone::gutenberg_get_slug_from_attr()
jeryj Mar 14, 2023
47d86d4
Rework gutenberg_get_slug_from_attr to use regex for more accurate ma…
jeryj Mar 14, 2023
a7fab17
Tests for WP_Duotone::is_preset
jeryj Mar 14, 2023
af3a236
Rework is_preset to also check if the preset exists
jeryj Mar 14, 2023
005baa1
Remove array_key_exists checks for presets within the block filter
jeryj Mar 14, 2023
c7160de
Return early if no block content
jeryj Mar 14, 2023
f5499b4
Make linter happy
jeryj Mar 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 266 additions & 63 deletions lib/block-supports/duotone.php
Original file line number Diff line number Diff line change
Expand Up @@ -436,54 +436,68 @@ function gutenberg_render_duotone_support( $block_content, $block ) {
$duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
}

// The block should have a duotone attribute or have duotone defined in its theme.json to be processed.
$has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] );

$has_global_styles_duotone = array_key_exists( $block['blockName'], WP_Duotone::$global_styles_block_names );

if (
! $duotone_support ||
! $has_duotone_attribute
( ! $has_duotone_attribute && ! $has_global_styles_duotone )
) {
return $block_content;
}

// Possible values for duotone attribute:
// 1. Array of colors - e.g. array('#000000', '#ffffff').
// 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|green-blue'.
// 3. A CSS string - e.g. 'unset' to remove globally applied duotone.
$duotone_attr = $block['attrs']['style']['color']['duotone'];
// Generate the pieces needed for rendering a duotone to the page.
if( $has_duotone_attribute ) {

$is_preset = is_string( $duotone_attr ) && strpos( $duotone_attr, 'var:preset|duotone|' ) === 0;
$is_css = is_string( $duotone_attr ) && strpos( $duotone_attr, 'var:preset|duotone|' ) === false;
$is_custom = is_array( $duotone_attr );
// Possible values for duotone attribute:
// 1. Array of colors - e.g. array('#000000', '#ffffff').
// 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|green-blue' or 'var(--wp--preset--duotone--green-blue)''
// 3. A CSS string - e.g. 'unset' to remove globally applied duotone.

// Generate the pieces needed for rendering a duotone to the page.
if ( $is_preset ) {
// Extract the slug from the preset variable string.
$slug = str_replace( 'var:preset|duotone|', '', $duotone_attr );
$duotone_attr = $block['attrs']['style']['color']['duotone'];
$is_preset = is_string( $duotone_attr ) && WP_Duotone::is_preset( $duotone_attr );
$is_css = is_string( $duotone_attr ) && ! $is_preset;
$is_custom = is_array( $duotone_attr );

// Utilize existing preset CSS custom property.
$filter_property = "var(--wp--preset--duotone--$slug)";
} elseif ( $is_css ) {
// Build a unique slug for the filter based on the CSS value.
$slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) );

// Pass through the CSS value.
$filter_property = $duotone_attr;
} elseif ( $is_custom ) {
// Build a unique slug for the filter based on the array of colors.
$slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) );

// This has the same shape as a preset, so it can be used in place of a
// preset when getting the filter property and SVG filter.
$filter_data = array(
'slug' => $slug,
'colors' => $duotone_attr,
);
if ( $is_preset ) {

// Build a customized CSS filter property for unique slug.
$filter_property = gutenberg_get_duotone_filter_property( $filter_data );
// TODO: Extract to set_output_preset( $filter_data );
// Extract the slug from the preset variable string.
$slug = WP_Duotone::gutenberg_get_slug_from_attr( $duotone_attr );

// Utilize existing preset CSS custom property.
$filter_property = "var(--wp--preset--duotone--$slug)";

WP_Duotone::$output[ $slug ] = WP_Duotone::$global_styles_presets[ $slug ];


} elseif ( $is_css ) {
// Build a unique slug for the filter based on the CSS value.
$slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) );

// SVG will be output on the page later.
$filter_svg = gutenberg_get_duotone_filter_svg( $filter_data );
// Pass through the CSS value.
$filter_property = $duotone_attr;
} elseif ( $is_custom ) {
// Build a unique slug for the filter based on the array of colors.
$slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) );

$filter_data = array(
'slug' => $slug,
'colors' => $duotone_attr,
);
// Build a customized CSS filter property for unique slug.
$filter_property = gutenberg_get_duotone_filter_property( $filter_data );

WP_Duotone::$output[ $slug ] = $filter_data;
}
} elseif ( $has_global_styles_duotone ) {
$slug = WP_Duotone::$global_styles_block_names[ $block['blockName'] ];

// Utilize existing preset CSS custom property.
$filter_property = "var(--wp--preset--duotone--$slug)";

WP_Duotone::$output[ $slug ] = WP_Duotone::$global_styles_presets[ $slug ];
}

// - Applied as a class attribute to the block wrapper.
Expand All @@ -493,6 +507,12 @@ function gutenberg_render_duotone_support( $block_content, $block ) {
// Build the CSS selectors to which the filter will be applied.
$selector = WP_Theme_JSON_Gutenberg::scope_selector( '.' . $filter_id, $duotone_support );

// We only want to add the selector if we have it in the output already, essentially skipping 'unset'.
// TODO: Extract to set_output_preset( $filter_data );
if( array_key_exists( $slug, WP_Duotone::$output ) ) {
WP_Duotone::$output[ $slug ][ 'selector' ] = $selector;
}

// Calling gutenberg_style_engine_get_stylesheet_from_css_rules ensures that
// the styles are rendered in an inline for block supports because we're
// using the `context` option to instruct it so.
Expand All @@ -513,33 +533,7 @@ function gutenberg_render_duotone_support( $block_content, $block ) {
'context' => 'block-supports',
)
);

// If we needed to generate an SVG, output it on the page.
if ( isset( $filter_svg ) ) {
add_action(
'wp_footer',
static function () use ( $filter_svg, $selector ) {
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(
'<script>( function() { var el = document.querySelector( %s ); var display = el.style.display; el.style.display = "none"; el.offsetHeight; el.style.display = display; } )();</script>',
wp_json_encode( $selector )
);
}
}
);
}


// Like the layout hook, this assumes the hook only applies to blocks with a single wrapper.
return preg_replace(
Expand All @@ -550,6 +544,77 @@ static function () use ( $filter_svg, $selector ) {
);
}


add_action( 'wp_footer',
static function () {

foreach( WP_Duotone::$output as $filter_data ) {

$filter_property = gutenberg_get_duotone_filter_property( $filter_data );
// SVG will be output on the page later.
$filter_svg = gutenberg_get_duotone_filter_svg( $filter_data );

echo $filter_svg;


// This is for classic themes - in block themes, the CSS is added in the head via the value_func.
if( ! wp_is_block_theme() ) {
$duotone_preset_css_var = WP_Theme_JSON_Gutenberg::get_preset_css_var( array( 'color', 'duotone' ), $filter_data[ 'slug'] );
wp_add_inline_style( 'core-block-supports', 'body{' . $duotone_preset_css_var . ' :' . $filter_property . ';}' );
}

global $is_safari;
if( $is_safari ) {
duotone_safari_rerender_hack( $selector );
}
}
}
);

/**
* Appends the used duotone fitler CSS Vars to the inline global styles CSS
*/
add_action( 'wp_enqueue_scripts', static function() {

if( empty( WP_Duotone::$output ) ) {
return;
}

$duotone_css_vars = '';

foreach ( WP_Duotone::$output as $filter_data ) {
if( ! array_key_exists( $filter_data[ 'slug' ], WP_Duotone::$global_styles_presets ) ) {
continue;
}

$filter_property = gutenberg_get_duotone_filter_property( $filter_data );

$duotone_preset_css_var = WP_Theme_JSON_Gutenberg::get_preset_css_var( array( 'color', 'duotone' ), $filter_data[ 'slug'] );
$duotone_css_vars .= $duotone_preset_css_var . ': ' . $filter_property . ';';
}

if( ! empty( $duotone_css_vars ) ) {
wp_add_inline_style( 'global-styles', 'body{' . $duotone_css_vars . '}' );
}
}, 11 );

/**
* 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.
*
* @param string $selector The selector to apply the hack for.
*/
function duotone_safari_rerender_hack( $selector ) {
/*
* Simply accessing el.offsetHeight flushes layout and style
* changes in WebKit without having to wait for setTimeout.
*/
printf(
'<script>( function() { var el = document.querySelector( %s ); var display = el.style.display; el.style.display = "none"; el.offsetHeight; el.style.display = display; } )();</script>',
wp_json_encode( $selector )
);
}

// Register the block support.
WP_Block_Supports::get_instance()->register(
'duotone',
Expand All @@ -561,3 +626,141 @@ static function () use ( $filter_svg, $selector ) {
// Remove WordPress core filter to avoid rendering duplicate support elements.
remove_filter( 'render_block', 'wp_render_duotone_support', 10, 2 );
add_filter( 'render_block', 'gutenberg_render_duotone_support', 10, 2 );


class WP_Duotone {
/**
* An array of Duotone presets from global, theme, and custom styles.
*
* Example:
* [
* 'blue-orange' =>
* [
* 'slug' => 'blue-orange',
* 'colors' => [ '#0000ff', '#ffcc00' ],
* ]
* ],
* …
* ]
*
* @since 6.3.0
* @var array
*/
static $global_styles_presets = array();

/**
* An array of block names from global, theme, and custom styles that have duotone presets. We'll use this to quickly
* check if a block being rendered needs to have duotone applied, and which duotone preset to use.
*
* Example:
* [
* 'core/featured-image' => 'blue-orange',
* …
* ]
*
*/
static $global_styles_block_names = array();

/**
* An array of Duotone SVG and CSS ouput needed for the frontend duotone rendering based on what is
* being ouptput on the page. Organized by a slug of the preset/color group and the information needed
* to generate the SVG and CSS at render.
*
* Example:
* [
* 'blue-orange' => [
* 'slug' => 'blue-orange',
* 'colors' => [ '#0000ff', '#ffcc00' ],
* ],
* 'wp-duotone-000000-ffffff-2' => [
* 'slug' => 'wp-duotone-000000-ffffff-2',
* 'colors' => [ '#000000', '#ffffff' ],
* ],
* ]
*
* @since 6.3.0
* @var array
*/
static $output = array();



/**
* Get all possible duotone presets from global and theme styles and store as slug => [ colors array ]
* We only want to process this one time. On block render we'll access and output only the needed presets for that page.
*
*/
static function set_global_styles_presets() {
// Get the per block settings from the theme.json.
$tree = gutenberg_get_global_settings();
$presets_by_origin = _wp_array_get( $tree, array( 'color', 'duotone' ), array() );

foreach( $presets_by_origin as $presets ) {
foreach( $presets as $preset ) {
self::$global_styles_presets[ _wp_to_kebab_case( $preset['slug'] ) ] = [
'slug' => $preset[ 'slug' ],
'colors' => $preset[ 'colors' ],
];
}
}
}

/**
* Scrape all block names from global styles and store in WP_Duotone::$global_styles_block_names
*/
static function set_global_style_block_names() {
// Get the per block settings from the theme.json.
$tree = WP_Theme_JSON_Resolver::get_merged_data();
$block_nodes = $tree->get_styles_block_nodes();
$theme_json = $tree->get_raw_data();


foreach( $block_nodes as $block_node ) {
// This block definition doesn't include any duotone settings. Skip it.
if ( empty( $block_node['duotone'] ) ) {
continue;
}

// Value looks like this: 'var(--wp--preset--duotone--blue-orange)' or 'var:preset|duotone|default-filter'
$duotone_attr_path = array_merge( $block_node['path'], array( 'filter', 'duotone' ) );
$duotone_attr = _wp_array_get( $theme_json, $duotone_attr_path, array() );

if( empty( $duotone_attr ) ) {
continue;
}
// If it has a duotone filter preset, save the block name and the preset slug.
$slug = self::gutenberg_get_slug_from_attr( $duotone_attr );

if( $slug && $slug !== $duotone_attr) {
self::$global_styles_block_names[ $block_node[ 'name' ] ] = $slug;
}
}
}

/**
* Take the inline CSS duotone variable from a block and return the slug. Handles styles slugs like:
* var:preset|duotone|default-filter
* var(--wp--preset--duotone--blue-orange)
*
* @param string $duotone_attr The duotone attribute from a block.
* @return string The slug of the duotone preset or an empty string if no slug is found.
*/
static function gutenberg_get_slug_from_attr( $duotone_attr ) {
// Uses Branch Reset Groups `(?|…)` to return one capture group
preg_match( '/(?|var:preset\|duotone\|(\S+)|var\(--wp--preset--duotone--(\S+)\))/', $duotone_attr, $matches );

return ! empty( $matches[ 1 ] ) ? $matches[ 1 ] : '';
}

/**
* Check if we have a valid duotone preset
*/
static function is_preset( $duotone_attr ) {
$slug = WP_Duotone::gutenberg_get_slug_from_attr( $duotone_attr );

return array_key_exists( $slug, WP_Duotone::$global_styles_presets );
}
}

add_action( 'wp_loaded', array( 'WP_Duotone', 'set_global_styles_presets' ), 10 );
add_action( 'wp_loaded', array( 'WP_Duotone', 'set_global_style_block_names' ), 10 );
Loading