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

Global styles: add custom CSS panel to site editor #46141

Merged
merged 39 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9c926f3
Try adding a custom CSS panel to global styles
glendaviesnz Dec 5, 2022
64d22a9
Update screen-css.js
carolinan Jul 1, 2022
28b5fb6
Update
carolinan Jul 5, 2022
c0bfa6b
Fix fatal error and update compat directory
Mamaduka Nov 29, 2022
0678fa8
No need to enable REST API support for custom_css post type
Mamaduka Nov 29, 2022
d9cced1
Fix PHPLint errors
Mamaduka Nov 29, 2022
a5b66c2
Switch to using core data store instead of direct fetches to API
glendaviesnz Nov 30, 2022
6473e7d
Render the custom css into the site editor
glendaviesnz Nov 30, 2022
144cee4
Switch to the useEntityRecord hook
glendaviesnz Nov 30, 2022
49ede18
remove use of custom_css post type as switching to use global styles
glendaviesnz Dec 1, 2022
31919b3
Switch to using global styles to store the custom css
glendaviesnz Dec 1, 2022
f13667c
Fix useMemo dependencies
Mamaduka Dec 2, 2022
23792ef
The 'css' value should be string
Mamaduka Dec 2, 2022
28fc3b0
Add custom css to editor settings
glendaviesnz Dec 12, 2022
607b48e
Refactor globals css in site editor now custom css in editor settings
glendaviesnz Dec 4, 2022
2304cd3
Add test for customCSS in get_stylesheet
glendaviesnz Dec 4, 2022
f562190
update rest endpoints
glendaviesnz Dec 8, 2022
76e3a78
Fix linting issues
glendaviesnz Dec 6, 2022
08846c0
Fix file naming issue
glendaviesnz Dec 6, 2022
4cc2a0e
Fix issue with not being able to remove existing custom css
glendaviesnz Dec 7, 2022
c5f05d7
Fix casing of php function param
glendaviesnz Dec 7, 2022
a6a67ad
make input font monospace
glendaviesnz Dec 7, 2022
035ce94
Move the custom css to its own stylesheet so invalid styles don't bre…
glendaviesnz Dec 7, 2022
472b828
Add custom css to its own stylesheet instead of combining with other …
glendaviesnz Dec 7, 2022
e8a5bf9
Update the option wording
glendaviesnz Dec 7, 2022
83244a5
use the merged config for custom css so theme.json settings picked up
glendaviesnz Dec 8, 2022
3041744
Use sass var for mono font
glendaviesnz Dec 8, 2022
4794a7c
remove some irrelevant nesting
glendaviesnz Dec 8, 2022
01a7bb4
Update label wording
glendaviesnz Dec 8, 2022
feeea4f
Always return settings
Mamaduka Dec 8, 2022
c933a60
Remove mobile block as probably not needed
glendaviesnz Dec 8, 2022
a1004ba
Port the prepare_items_for_response method in order to pick up new th…
glendaviesnz Dec 8, 2022
414c928
make prepare_item_for_response same as core to make backporting easier
glendaviesnz Dec 8, 2022
abdf4a9
Make sure the get_user_data call is always called at least once by th…
glendaviesnz Dec 12, 2022
43b29dd
Fix test and linting issues
glendaviesnz Dec 12, 2022
d716585
Use instanceof instead of is_a
glendaviesnz Dec 12, 2022
1df72bf
Add theme.json custom css to input box
glendaviesnz Dec 12, 2022
47a6d91
Add explanatory comments
glendaviesnz Dec 13, 2022
b55429d
Changes from code review
glendaviesnz Dec 13, 2022
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
28 changes: 28 additions & 0 deletions lib/compat/wordpress-6.2/block-editor-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Adds settings to the block editor.
*
* @package gutenberg
*/

/**
* Adds styles and __experimentalFeatures to the block editor settings.
*
* @param array $settings Existing block editor settings.
*
* @return array New block editor settings.
*/
function gutenberg_get_block_editor_settings_6_2( $settings ) {
if ( wp_theme_has_theme_json() ) {
// Add the custom CSS as separate style sheet so any invalid CSS entered by users does not break other global styles.
$settings['styles'][] = array(
'css' => gutenberg_get_global_stylesheet( array( 'custom-css' ) ),
'__unstableType' => 'user',
'isGlobalStyles' => true,
);
}

return $settings;
}

add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_6_2', PHP_INT_MAX );
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<?php
/**
* REST API: Gutenberg_REST_Global_Styles_Controller class
*
* @package Gutenberg
* @subpackage REST_API
*/

/**
* Base Global Styles REST API Controller.
*/
class Gutenberg_REST_Global_Styles_Controller_6_2 extends WP_REST_Global_Styles_Controller {
/**
* Registers the controllers routes.
*
* @return void
*/
public function register_routes() {
parent::register_routes();
}

/**
* Prepare a global styles config output for response.
*
* @since 5.9.0
* @since 6.2 Handling of style.css was added to WP_Theme_JSON.
*
* @param WP_Post $post Global Styles post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$raw_config = json_decode( $post->post_content, true );
$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
$config = array();
if ( $is_global_styles_user_theme_json ) {
$config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data();
}

// Base fields for every post.
$data = array();
$fields = $this->get_fields_for_response( $request );

if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $post->ID;
}

if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $post->post_title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );

$data['title']['rendered'] = get_the_title( $post->ID );

remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}

if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
}

if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

// Wrap the data in a response object.
$response = rest_ensure_response( $data );

if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $post->ID );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions();
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}

return $response;
}

/**
* Updates a single global style config.
*
* @since 5.9.0
* @since 6.2.0 Added validation of styles.css property.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$post_before = $this->get_post( $request['id'] );
if ( is_wp_error( $post_before ) ) {
return $post_before;
}

$changes = $this->prepare_item_for_database( $request );
if ( is_wp_error( $changes ) ) {
return $changes;
}

$result = wp_update_post( wp_slash( (array) $changes ), true, false );
if ( is_wp_error( $result ) ) {
return $result;
}

$post = get_post( $request['id'] );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}

wp_after_insert_post( $post, true, $post_before );

$response = $this->prepare_item_for_response( $post, $request );

return rest_ensure_response( $response );
}
/**
* Prepares a single global styles config for update.
*
* @since 5.9.0
* @since 6.2.0 Added validation of styles.css property.
*
* @param WP_REST_Request $request Request object.
* @return stdClass Changes to pass to wp_update_post.
*/
protected function prepare_item_for_database( $request ) {
$changes = new stdClass();
$changes->ID = $request['id'];
$post = get_post( $request['id'] );
$existing_config = array();
if ( $post ) {
$existing_config = json_decode( $post->post_content, true );
$json_decoding_error = json_last_error();
if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
! $existing_config['isGlobalStylesUserThemeJSON'] ) {
$existing_config = array();
}
}
if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
$config = array();
if ( isset( $request['styles'] ) ) {
$config['styles'] = $request['styles'];
$validate_custom_css = $this->validate_custom_css( $request['styles']['css'] );
if ( is_wp_error( $validate_custom_css ) ) {
return $validate_custom_css;
}
} elseif ( isset( $existing_config['styles'] ) ) {
$config['styles'] = $existing_config['styles'];
}
if ( isset( $request['settings'] ) ) {
$config['settings'] = $request['settings'];
} elseif ( isset( $existing_config['settings'] ) ) {
$config['settings'] = $existing_config['settings'];
}
$config['isGlobalStylesUserThemeJSON'] = true;
$config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA;
$changes->post_content = wp_json_encode( $config );
}
// Post title.
if ( isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$changes->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$changes->post_title = $request['title']['raw'];
}
}
return $changes;
}

/**
* Validate style.css as valid CSS.
*
* Currently just checks for invalid markup.
*
* @since 6.2.0
*
* @param string $css CSS to validate.
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
*/
private function validate_custom_css( $css ) {
if ( preg_match( '#</?\w+#', $css ) ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
__( 'Markup is not allowed in CSS.', 'gutenberg' ),
array( 'status' => 400 )
);
}
return true;
}
}
111 changes: 111 additions & 0 deletions lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 {
* @since 6.1.0 Added new side properties for `border`,
* added new property `shadow`,
* updated `blockGap` to be allowed at any level.
* @since 6.2.0 Added new property `css`.
* @var array
*/
const VALID_STYLES = array(
Expand Down Expand Up @@ -234,6 +235,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 {
'textDecoration' => null,
'textTransform' => null,
),
'css' => null,
);

/**
Expand Down Expand Up @@ -277,4 +279,113 @@ protected static function remove_insecure_styles( $input ) {

return $output;
}

/**
* Returns the stylesheet that results of processing
* the theme.json structure this object represents.
*
* @param array $types Types of styles to load. Will load all by default. It accepts:
* 'variables': only the CSS Custom Properties for presets & custom ones.
* 'styles': only the styles section in theme.json.
* 'presets': only the classes for the presets.
* 'custom-css': only the css from global styles.css.
* @param array $origins A list of origins to include. By default it includes VALID_ORIGINS.
* @param array $options An array of options for now used for internal purposes only (may change without notice).
* The options currently supported are 'scope' that makes sure all style are scoped to a given selector,
* and root_selector which overwrites and forces a given selector to be used on the root node.
* @return string Stylesheet.
*/
public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
if ( null === $origins ) {
$origins = static::VALID_ORIGINS;
}

if ( is_string( $types ) ) {
// Dispatch error and map old arguments to new ones.
_deprecated_argument( __FUNCTION__, '5.9' );
if ( 'block_styles' === $types ) {
$types = array( 'styles', 'presets' );
} elseif ( 'css_variables' === $types ) {
$types = array( 'variables' );
} else {
$types = array( 'variables', 'styles', 'presets' );
}
}

$blocks_metadata = static::get_blocks_metadata();
$style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata );
$setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata );

$root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
$root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );

if ( ! empty( $options['scope'] ) ) {
foreach ( $setting_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
}
foreach ( $style_nodes as &$node ) {
$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
}
}

if ( ! empty( $options['root_selector'] ) ) {
if ( false !== $root_settings_key ) {
$setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
}
if ( false !== $root_style_key ) {
$setting_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
}
}

$stylesheet = '';

if ( in_array( 'variables', $types, true ) ) {
$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
}

if ( in_array( 'styles', $types, true ) ) {
if ( false !== $root_style_key ) {
$stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
}
$stylesheet .= $this->get_block_classes( $style_nodes );
} elseif ( in_array( 'base-layout-styles', $types, true ) ) {
$root_selector = static::ROOT_BLOCK_SELECTOR;
$columns_selector = '.wp-block-columns';
if ( ! empty( $options['scope'] ) ) {
$root_selector = static::scope_selector( $options['scope'], $root_selector );
$columns_selector = static::scope_selector( $options['scope'], $columns_selector );
}
if ( ! empty( $options['root_selector'] ) ) {
$root_selector = $options['root_selector'];
}
// Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
// For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
$base_styles_nodes = array(
array(
'path' => array( 'styles' ),
'selector' => $root_selector,
),
array(
'path' => array( 'styles', 'blocks', 'core/columns' ),
'selector' => $columns_selector,
'name' => 'core/columns',
),
);

foreach ( $base_styles_nodes as $base_style_node ) {
$stylesheet .= $this->get_layout_styles( $base_style_node );
}
}

if ( in_array( 'presets', $types, true ) ) {
$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
}

// Load the custom CSS last so it has the highest specificity.
if ( in_array( 'custom-css', $types, true ) ) {
$stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
}

return $stylesheet;
}
}
Loading