diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index fd29b1c6e1388..744b7b2896e26 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -168,29 +168,6 @@ _Returns_ - `Array?`: The list of allowed block types. -### getBehaviors - -Returns the behaviors registered with the editor. - -Behaviors are named, reusable pieces of functionality that can be attached to blocks. They are registered with the editor using the `theme.json` file. - -_Usage_ - -```js -const behaviors = select( blockEditorStore ).getBehaviors(); -if ( behaviors?.lightbox ) { - // Do something with the lightbox. -} -``` - -_Parameters_ - -- _state_ `Object`: Editor state. - -_Returns_ - -- `Object`: The editor behaviors object. - ### getBlock Returns a block given its client ID. This is a parsed copy of the block, containing its `blockName`, `clientId`, and current `attributes` state. This is not the block's registration settings, which must be retrieved from the blocks module registration store. diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php index c45ce23c5d4ea..e9c73a717d3d0 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php @@ -21,7 +21,7 @@ class Gutenberg_REST_Global_Styles_Revisions_Controller_6_3 extends WP_REST_Cont * @since 6.3.0 * @var string */ - private $parent_post_type; + protected $parent_post_type; /** * The base of the parent controller's route. @@ -102,7 +102,7 @@ public function get_collection_params() { * @param string $raw_json Encoded JSON from global styles custom post content. * @return Array|WP_Error */ - private function get_decoded_global_styles_json( $raw_json ) { + protected function get_decoded_global_styles_json( $raw_json ) { $decoded_json = json_decode( $raw_json, true ); if ( is_array( $decoded_json ) && isset( $decoded_json['isGlobalStylesUserThemeJSON'] ) && true === $decoded_json['isGlobalStylesUserThemeJSON'] ) { diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index 144ad4d50c83f..90898c0b71e24 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -52,24 +52,6 @@ function gutenberg_update_templates_template_parts_rest_controller( $args, $post } add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 ); -/** - * Registers the Global Styles Revisions REST API routes. - */ -function gutenberg_register_global_styles_revisions_endpoints() { - $global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_3(); - $global_styles_revisions_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); - -/** - * Registers the Global Styles REST API routes. - */ -function gutenberg_register_global_styles_endpoints() { - $global_styles_controller = new Gutenberg_REST_Global_Styles_Controller_6_3(); - $global_styles_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' ); - /** * Add the `modified` value to the `wp_template` schema. * diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php new file mode 100644 index 0000000000000..7ad1f264b383d --- /dev/null +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php @@ -0,0 +1,269 @@ +schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'ID of global styles config.', 'default' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'styles' => array( + 'description' => __( 'Global styles.', 'default' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'settings' => array( + 'description' => __( 'Global settings.', 'default' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'behaviors' => array( + 'description' => __( 'Global behaviors.', 'default' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'title' => array( + 'description' => __( 'Title of the global styles variation.', 'default' ), + 'type' => array( 'object', 'string' ), + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Title for the global styles variation, as it exists in the database.', 'default' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'rendered' => array( + 'description' => __( 'HTML title for the post, transformed for display.', 'default' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * 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(); + } + + if ( rest_is_field_included( 'behaviors', $fields ) ) { + $data['behaviors'] = ! empty( $config['behaviors'] ) && $is_global_styles_user_theme_json ? $config['behaviors'] : 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; + } + + /** + * Returns the given theme global styles config. + * Duplicated from core. + * The only change is that we call WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ) instead of WP_Theme_JSON_Resolver::get_merged_data( 'theme' ). + * + * @since 6.2.0 + * + * @param WP_REST_Request $request The request instance. + * @return WP_REST_Response|WP_Error + */ + public function get_theme_item( $request ) { + if ( get_stylesheet() !== $request['stylesheet'] ) { + // This endpoint only supports the active theme for now. + return new WP_Error( + 'rest_theme_not_found', + __( 'Theme not found.', 'default' ), + array( 'status' => 404 ) + ); + } + + $theme = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ); + $data = array(); + $fields = $this->get_fields_for_response( $request ); + + if ( rest_is_field_included( 'settings', $fields ) ) { + $data['settings'] = $theme->get_settings(); + } + + if ( rest_is_field_included( 'styles', $fields ) ) { + $raw_data = $theme->get_raw_data(); + $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array(); + } + + if ( rest_is_field_included( 'behaviors', $fields ) ) { + $raw_data = $theme->get_raw_data(); + $data['behaviors'] = isset( $raw_data['behaviors'] ) ? $raw_data['behaviors'] : array(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ), + ), + ); + $response->add_links( $links ); + } + + return $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'] ) || isset( $request['behaviors'] ) ) { + $config = array(); + if ( isset( $request['styles'] ) ) { + $config['styles'] = $request['styles']; + if ( isset( $request['styles']['css'] ) ) { + $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']; + } + if ( isset( $request['behaviors'] ) ) { + $config['behaviors'] = $request['behaviors']; + } elseif ( isset( $existing_config['behaviors'] ) ) { + $config['behaviors'] = $existing_config['behaviors']; + } + $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; + } + +} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php new file mode 100644 index 0000000000000..42120b44bdcb6 --- /dev/null +++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php @@ -0,0 +1,172 @@ +get_parent( $request['parent'] ); + $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); + + if ( is_wp_error( $global_styles_config ) ) { + return $global_styles_config; + } + + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) || ! empty( $global_styles_config['behaviors'] ) ) { + $global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data(); + if ( rest_is_field_included( 'settings', $fields ) ) { + $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass(); + } + if ( rest_is_field_included( 'styles', $fields ) ) { + $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass(); + } + if ( rest_is_field_included( 'behaviors', $fields ) ) { + $data['behaviors'] = ! empty( $global_styles_config['behaviors'] ) ? $global_styles_config['behaviors'] : new stdClass(); + } + } + + if ( rest_is_field_included( 'author', $fields ) ) { + $data['author'] = (int) $post->post_author; + } + + if ( rest_is_field_included( 'date', $fields ) ) { + $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); + } + + if ( rest_is_field_included( 'date_gmt', $fields ) ) { + $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt ); + } + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = (int) $post->ID; + } + + if ( rest_is_field_included( 'modified', $fields ) ) { + $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); + } + + if ( rest_is_field_included( 'modified_gmt', $fields ) ) { + $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt ); + } + + if ( rest_is_field_included( 'parent', $fields ) ) { + $data['parent'] = (int) $parent->ID; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + return rest_ensure_response( $data ); + } + + /** + * Retrieves the revision's schema, conforming to JSON Schema. + * + * @since 6.3.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => "{$this->parent_post_type}-revision", + 'type' => 'object', + // Base properties for every Revision. + 'properties' => array( + + /* + * Adds settings and styles from the WP_REST_Revisions_Controller item fields. + * Leaves out GUID as global styles shouldn't be accessible via URL. + */ + 'author' => array( + 'description' => __( 'The ID for the author of the revision.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'date' => array( + 'description' => __( "The date the revision was published, in the site's timezone.", 'gutenberg' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'date_gmt' => array( + 'description' => __( 'The date the revision was published, as GMT.', 'gutenberg' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'id' => array( + 'description' => __( 'Unique identifier for the revision.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'modified' => array( + 'description' => __( "The date the revision was last modified, in the site's timezone.", 'gutenberg' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'modified_gmt' => array( + 'description' => __( 'The date the revision was last modified, as GMT.', 'gutenberg' ), + 'type' => 'string', + 'format' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + + // Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema. + 'styles' => array( + 'description' => __( 'Global styles.', 'gutenberg' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'settings' => array( + 'description' => __( 'Global settings.', 'gutenberg' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'behaviors' => array( + 'description' => __( 'Global behaviors.', 'gutenberg' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/lib/compat/wordpress-6.4/rest-api.php b/lib/compat/wordpress-6.4/rest-api.php new file mode 100644 index 0000000000000..53979f832c09a --- /dev/null +++ b/lib/compat/wordpress-6.4/rest-api.php @@ -0,0 +1,29 @@ +register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); + +/** + * Registers the Global Styles REST API routes. + */ +function gutenberg_register_global_styles_endpoints() { + $global_styles_controller = new Gutenberg_REST_Global_Styles_Controller_6_4(); + $global_styles_controller->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' ); diff --git a/lib/experimental/behaviors.php b/lib/experimental/behaviors.php deleted file mode 100644 index 62e7be7a252d4..0000000000000 --- a/lib/experimental/behaviors.php +++ /dev/null @@ -1,20 +0,0 @@ -get_data(); - if ( array_key_exists( 'behaviors', $theme_data ) ) { - $settings['behaviors'] = $theme_data['behaviors']; - } - return $settings; - }, - PHP_INT_MAX -); diff --git a/lib/load.php b/lib/load.php index eeb463f7b762d..3ffd026a8a444 100644 --- a/lib/load.php +++ b/lib/load.php @@ -59,12 +59,16 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php'; require_once __DIR__ . '/compat/wordpress-6.3/footnotes.php'; + // WordPress 6.4 compat. + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php'; + require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; + require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; + // Experimental. if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { require_once __DIR__ . '/experimental/class-wp-rest-customizer-nonces.php'; } require_once __DIR__ . '/experimental/class-gutenberg-rest-template-revision-count.php'; - require_once __DIR__ . '/experimental/rest-api.php'; } @@ -103,7 +107,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.3/kses.php'; // Experimental features. -require __DIR__ . '/experimental/behaviors.php'; require __DIR__ . '/experimental/block-editor-settings-mobile.php'; require __DIR__ . '/experimental/blocks.php'; require __DIR__ . '/experimental/navigation-theme-opt-in.php'; diff --git a/packages/block-editor/src/components/global-styles/behaviors-panel.js b/packages/block-editor/src/components/global-styles/behaviors-panel.js new file mode 100644 index 0000000000000..fa8c2305ae037 --- /dev/null +++ b/packages/block-editor/src/components/global-styles/behaviors-panel.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { SelectControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +export default function ( { onChange, value, behaviors } ) { + const defaultBehaviors = { + default: { + value: 'default', + label: __( 'Default' ), + }, + noBehaviors: { + value: '', + label: __( 'No behaviors' ), + }, + }; + + const behaviorsOptions = Object.entries( behaviors ).map( + ( [ behaviorName ] ) => ( { + value: behaviorName, + // Capitalize the first letter of the behavior name. + label: `${ behaviorName.charAt( 0 ).toUpperCase() }${ behaviorName + .slice( 1 ) + .toLowerCase() }`, + } ) + ); + + const options = [ + ...Object.values( defaultBehaviors ), + ...behaviorsOptions, + ]; + + const animations = [ + { + value: 'zoom', + label: __( 'Zoom' ), + }, + { + value: 'fade', + label: __( 'Fade' ), + }, + ]; + return ( +
+ + { value === 'lightbox' && ( + + ) } +
+ ); +} diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 3c8b016727980..96be5779124d7 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -8,7 +8,7 @@ import fastDeepEqual from 'fast-deep-equal/es6'; */ import { useContext, useCallback, useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { store as blocksStore } from '@wordpress/blocks'; +import { store as blocksStore, hasBlockSupport } from '@wordpress/blocks'; import { _x } from '@wordpress/i18n'; /** @@ -19,10 +19,11 @@ import { getValueFromObjectPath, setImmutably } from '../../utils/object'; import { GlobalStylesContext } from './context'; import { unlock } from '../../lock-unlock'; -const EMPTY_CONFIG = { settings: {}, styles: {} }; +const EMPTY_CONFIG = { settings: {}, styles: {}, behaviors: {} }; const VALID_SETTINGS = [ 'appearanceTools', + 'behaviors', 'useRootPaddingAwareAlignments', 'border.color', 'border.radius', @@ -88,7 +89,6 @@ export const useGlobalStylesReset = () => { export function useGlobalSetting( propertyPath, blockName, source = 'all' ) { const { setUserConfig, ...configs } = useContext( GlobalStylesContext ); - const appendedBlockPath = blockName ? '.blocks.' + blockName : ''; const appendedPropertyPath = propertyPath ? '.' + propertyPath : ''; const contextualPath = `settings${ appendedBlockPath }${ appendedPropertyPath }`; @@ -135,7 +135,6 @@ export function useGlobalSetting( propertyPath, blockName, source = 'all' ) { setImmutably( currentConfig, contextualPath.split( '.' ), newValue ) ); }; - return [ settingValue, setSetting ]; } @@ -461,3 +460,112 @@ export function useGradientsPerOrigin( settings ) { shouldDisplayDefaultGradients, ] ); } + +export function __experimentalUseGlobalBehaviors( blockName, source = 'all' ) { + const { + merged: mergedConfig, + base: baseConfig, + user: userConfig, + setUserConfig, + } = useContext( GlobalStylesContext ); + const finalPath = ! blockName + ? `behaviors` + : `behaviors.blocks.${ blockName }`; + + let rawResult, result; + switch ( source ) { + case 'all': + rawResult = getValueFromObjectPath( mergedConfig, finalPath ); + result = getValueFromVariable( mergedConfig, blockName, rawResult ); + break; + case 'user': + rawResult = getValueFromObjectPath( userConfig, finalPath ); + result = getValueFromVariable( mergedConfig, blockName, rawResult ); + break; + case 'base': + rawResult = getValueFromObjectPath( baseConfig, finalPath ); + result = getValueFromVariable( baseConfig, blockName, rawResult ); + break; + default: + throw 'Unsupported source'; + } + + const animation = result?.lightbox?.animation || 'zoom'; + + const setBehavior = ( newValue ) => { + let newBehavior; + // The user saves with Apply Globally option. + if ( typeof newValue === 'object' ) { + newBehavior = newValue; + } else { + switch ( newValue ) { + case 'lightbox': + newBehavior = { + lightbox: { + enabled: true, + animation, + }, + }; + break; + case 'fade': + newBehavior = { + lightbox: { + enabled: true, + animation: 'fade', + }, + }; + break; + case 'zoom': + newBehavior = { + lightbox: { + enabled: true, + animation: 'zoom', + }, + }; + break; + case '': + newBehavior = { + lightbox: { + enabled: false, + animation, + }, + }; + break; + default: + break; + } + } + setUserConfig( ( currentConfig ) => + setImmutably( currentConfig, finalPath.split( '.' ), newBehavior ) + ); + }; + let behavior = ''; + if ( result === undefined ) behavior = 'default'; + if ( result?.lightbox.enabled ) behavior = 'lightbox'; + + return { behavior, inheritedBehaviors: result, setBehavior }; +} + +export function __experimentalUseHasBehaviorsPanel( + settings, + name, + { blockSupportOnly = false } = {} +) { + if ( ! settings?.behaviors || ! window?.__experimentalInteractivityAPI ) { + return false; + } + + // If every behavior is disabled on block supports, do not show the behaviors inspector control. + const hasSomeBlockSupport = Object.keys( settings?.behaviors ).some( + ( key ) => hasBlockSupport( name, `behaviors.${ key }` ) + ); + + if ( blockSupportOnly ) { + return hasSomeBlockSupport; + } + + // If every behavior is disabled, do not show the behaviors inspector control. + return Object.values( settings?.behaviors ).some( + ( value ) => value === true && hasSomeBlockSupport + ); +} diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 24bab543b9ada..ee5c66ebe8a65 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -1,4 +1,6 @@ export { + __experimentalUseGlobalBehaviors, + __experimentalUseHasBehaviorsPanel, useGlobalStylesReset, useGlobalSetting, useGlobalStyle, @@ -23,5 +25,6 @@ export { default as BorderPanel, useHasBorderPanel } from './border-panel'; export { default as ColorPanel, useHasColorPanel } from './color-panel'; export { default as EffectsPanel, useHasEffectsPanel } from './effects-panel'; export { default as FiltersPanel, useHasFiltersPanel } from './filters-panel'; +export { default as __experimentalBehaviorsPanel } from './behaviors-panel'; export { default as AdvancedPanel } from './advanced-panel'; export { areGlobalStyleConfigsEqual } from './utils'; diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index d4f2d959a3365..8f9e1f1f39600 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -415,6 +415,7 @@ export function areGlobalStyleConfigsEqual( original, variation ) { } return ( fastDeepEqual( original?.styles, variation?.styles ) && - fastDeepEqual( original?.settings, variation?.settings ) + fastDeepEqual( original?.settings, variation?.settings ) && + fastDeepEqual( original?.behaviors, variation?.behaviors ) ); } diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js index 42cb42e802368..27ef5b7afbfe9 100644 --- a/packages/block-editor/src/hooks/behaviors.js +++ b/packages/block-editor/src/hooks/behaviors.js @@ -22,15 +22,13 @@ function BehaviorsControl( { onChangeAnimation, disabled = false, } ) { - const { settings, themeBehaviors } = useSelect( + const { settings } = useSelect( ( select ) => { - const { getBehaviors, getSettings } = select( blockEditorStore ); - + const { getSettings } = select( blockEditorStore ); return { settings: getSettings()?.__experimentalFeatures?.blocks?.[ blockName ] - ?.behaviors, - themeBehaviors: getBehaviors()?.blocks?.[ blockName ], + ?.behaviors || {}, }; }, [ blockName ] @@ -46,7 +44,6 @@ function BehaviorsControl( { label: __( 'No behaviors' ), }, }; - const behaviorsOptions = Object.entries( settings ) .filter( ( [ behaviorName, behaviorValue ] ) => @@ -60,7 +57,6 @@ function BehaviorsControl( { .slice( 1 ) .toLowerCase() }`, } ) ); - const options = [ ...Object.values( defaultBehaviors ), ...behaviorsOptions, @@ -68,7 +64,6 @@ function BehaviorsControl( { const { behaviors, behaviorsValue } = useMemo( () => { const mergedBehaviors = { - ...themeBehaviors, ...( blockBehaviors || {} ), }; @@ -83,7 +78,8 @@ function BehaviorsControl( { behaviors: mergedBehaviors, behaviorsValue: value, }; - }, [ blockBehaviors, themeBehaviors ] ); + }, [ blockBehaviors ] ); + // If every behavior is disabled, do not show the behaviors inspector control. if ( behaviorsOptions.length === 0 ) { return null; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3aaec39a986cc..c3d8847b03239 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2577,30 +2577,6 @@ export function getSettings( state ) { return state.settings; } -/** - * Returns the behaviors registered with the editor. - * - * Behaviors are named, reusable pieces of functionality that can be - * attached to blocks. They are registered with the editor using the - * `theme.json` file. - * - * @example - * - * ```js - * const behaviors = select( blockEditorStore ).getBehaviors(); - * if ( behaviors?.lightbox ) { - * // Do something with the lightbox. - * } - *``` - * - * @param {Object} state Editor state. - * - * @return {Object} The editor behaviors object. - */ -export function getBehaviors( state ) { - return state.settings.behaviors; -} - /** * Returns true if the most recent block change is be considered persistent, or * false otherwise. A persistent change is one committed by BlockEditorProvider diff --git a/packages/edit-site/src/components/global-styles/global-styles-provider.js b/packages/edit-site/src/components/global-styles/global-styles-provider.js index 1e2d43e267a2d..250cca0ebfc6d 100644 --- a/packages/edit-site/src/components/global-styles/global-styles-provider.js +++ b/packages/edit-site/src/components/global-styles/global-styles-provider.js @@ -31,7 +31,7 @@ export function mergeBaseAndUserConfigs( base, user ) { } function useGlobalStylesUserConfig() { - const { globalStylesId, isReady, settings, styles } = useSelect( + const { globalStylesId, isReady, settings, styles, behaviors } = useSelect( ( select ) => { const { getEditedEntityRecord, hasFinishedResolution } = select( coreStore ); @@ -65,6 +65,7 @@ function useGlobalStylesUserConfig() { isReady: hasResolved, settings: record?.settings, styles: record?.styles, + behaviors: record?.behaviors, }; }, [] @@ -76,8 +77,9 @@ function useGlobalStylesUserConfig() { return { settings: settings ?? {}, styles: styles ?? {}, + behaviors: behaviors ?? {}, }; - }, [ settings, styles ] ); + }, [ settings, styles, behaviors ] ); const setConfig = useCallback( ( callback, options = {} ) => { @@ -89,6 +91,7 @@ function useGlobalStylesUserConfig() { const currentConfig = { styles: record?.styles ?? {}, settings: record?.settings ?? {}, + behaviors: record?.behaviors ?? {}, }; const updatedConfig = callback( currentConfig ); editEntityRecord( @@ -98,6 +101,8 @@ function useGlobalStylesUserConfig() { { styles: cleanEmptyObject( updatedConfig.styles ) || {}, settings: cleanEmptyObject( updatedConfig.settings ) || {}, + behaviors: + cleanEmptyObject( updatedConfig.behaviors ) || {}, }, options ); diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index b24fd5eb41de1..ea3b5b89d116f 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -61,12 +61,15 @@ const { useHasDimensionsPanel, useHasTypographyPanel, useHasBorderPanel, + __experimentalUseHasBehaviorsPanel: useHasBehaviorsPanel, useGlobalSetting, useSettingsForBlockElement, useHasColorPanel, useHasEffectsPanel, useHasFiltersPanel, useGlobalStyle, + __experimentalUseGlobalBehaviors: useGlobalBehaviors, + __experimentalBehaviorsPanel: StylesBehaviorsPanel, BorderPanel: StylesBorderPanel, ColorPanel: StylesColorPanel, TypographyPanel: StylesTypographyPanel, @@ -91,10 +94,14 @@ function ScreenBlock( { name, variation } ) { } ); const [ rawSettings, setSettings ] = useGlobalSetting( '', name ); const settings = useSettingsForBlockElement( rawSettings, name ); + const { inheritedBehaviors, setBehavior } = useGlobalBehaviors( name ); + const { behavior } = useGlobalBehaviors( name, 'user' ); + const blockType = getBlockType( name ); const blockVariations = useBlockVariations( name ); const hasTypographyPanel = useHasTypographyPanel( settings ); const hasColorPanel = useHasColorPanel( settings ); + const hasBehaviorsPanel = useHasBehaviorsPanel( rawSettings, name ); const hasBorderPanel = useHasBorderPanel( settings ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); const hasEffectsPanel = useHasEffectsPanel( settings ); @@ -267,6 +274,14 @@ function ScreenBlock( { name, variation } ) { onChange={ setStyle } inheritedValue={ inheritedStyle } /> + { hasBehaviorsPanel && ( + + ) } ) } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index c6920f3d63c24..b21c14418cbee 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -70,6 +70,7 @@ function ScreenRevisions() { setUserConfig( () => ( { styles: revision?.styles, settings: revision?.settings, + behaviors: revision?.behaviors, } ) ); setIsLoadingRevisionWithUnsavedChanges( false ); onCloseRevisions(); @@ -79,6 +80,7 @@ function ScreenRevisions() { setGlobalStylesRevision( { styles: revision?.styles, settings: revision?.settings, + behaviors: revision?.behaviors, id: revision?.id, } ); setSelectedRevisionId( revision?.id ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js index ce3123e3fd028..5aee31f1ff99a 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js @@ -82,6 +82,7 @@ export default function useGlobalStylesRevisions() { id: 'unsaved', styles: userConfig?.styles, settings: userConfig?.settings, + behaviors: userConfig?.behaviors, author: { name: currentUser?.name, avatar_urls: currentUser?.avatar_urls, diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/edit-site/src/components/global-styles/style-variations-container.js index 6cc8b53b800d3..69a66a707d252 100644 --- a/packages/edit-site/src/components/global-styles/style-variations-container.js +++ b/packages/edit-site/src/components/global-styles/style-variations-container.js @@ -113,11 +113,13 @@ export default function StyleVariationsContainer() { title: __( 'Default' ), settings: {}, styles: {}, + behaviors: {}, }, ...( variations ?? [] ).map( ( variation ) => ( { ...variation, settings: variation.settings ?? {}, styles: variation.styles ?? {}, + behaviors: variation.behaviors ?? {}, } ) ), ]; }, [ variations ] ); diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js index 6e55b89d22b2c..455f18ef74eba 100644 --- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -25,9 +25,12 @@ import { store as noticesStore } from '@wordpress/notices'; import { useSupportedStyles } from '../../components/global-styles/hooks'; import { unlock } from '../../lock-unlock'; -const { GlobalStylesContext, useBlockEditingMode } = unlock( - blockEditorPrivateApis -); +const { + GlobalStylesContext, + useBlockEditingMode, + __experimentalUseGlobalBehaviors: useGlobalBehaviors, + __experimentalUseHasBehaviorsPanel: useHasBehaviorsPanel, +} = unlock( blockEditorPrivateApis ); // TODO: Temporary duplication of constant in @wordpress/block-editor. Can be // removed by moving PushChangesToGlobalStylesControl to @@ -121,7 +124,7 @@ function useChangesToPush( name, attributes ) { : getValueFromObjectPath( attributes.style, path ); return value ? [ { path, value } ] : []; } ), - [ supports, name, attributes ] + [ supports, attributes ] ); } @@ -176,6 +179,9 @@ function PushChangesToGlobalStylesControl( { } ) { const changes = useChangesToPush( name, attributes ); + const hasBehaviorsPanel = useHasBehaviorsPanel( attributes, name, { + blockSupportOnly: true, + } ); const { user: userConfig, setUserConfig } = useContext( GlobalStylesContext ); @@ -183,55 +189,86 @@ function PushChangesToGlobalStylesControl( { useDispatch( blockEditorStore ); const { createSuccessNotice } = useDispatch( noticesStore ); + const { inheritedBehaviors, setBehavior } = useGlobalBehaviors( name ); + + const userHasEditedBehaviors = + attributes.hasOwnProperty( 'behaviors' ) && hasBehaviorsPanel; + const pushChanges = useCallback( () => { - if ( changes.length === 0 ) { + if ( changes.length === 0 && ! userHasEditedBehaviors ) { return; } + if ( changes.length > 0 ) { + const { style: blockStyles } = attributes; - const { style: blockStyles } = attributes; + const newBlockStyles = cloneDeep( blockStyles ); + const newUserConfig = cloneDeep( userConfig ); - const newBlockStyles = cloneDeep( blockStyles ); - const newUserConfig = cloneDeep( userConfig ); + for ( const { path, value } of changes ) { + setNestedValue( newBlockStyles, path, undefined ); + setNestedValue( + newUserConfig, + [ 'styles', 'blocks', name, ...path ], + value + ); + } - for ( const { path, value } of changes ) { - setNestedValue( newBlockStyles, path, undefined ); - setNestedValue( - newUserConfig, - [ 'styles', 'blocks', name, ...path ], - value + // @wordpress/core-data doesn't support editing multiple entity types in + // a single undo level. So for now, we disable @wordpress/core-data undo + // tracking and implement our own Undo button in the snackbar + // notification. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { style: newBlockStyles } ); + setUserConfig( () => newUserConfig, { undoIgnore: true } ); + createSuccessNotice( + sprintf( + // translators: %s: Title of the block e.g. 'Heading'. + __( '%s styles applied.' ), + getBlockType( name ).title + ), + { + type: 'snackbar', + actions: [ + { + label: __( 'Undo' ), + onClick() { + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { style: blockStyles } ); + setUserConfig( () => userConfig, { + undoIgnore: true, + } ); + }, + }, + ], + } ); } - - // @wordpress/core-data doesn't support editing multiple entity types in - // a single undo level. So for now, we disable @wordpress/core-data undo - // tracking and implement our own Undo button in the snackbar - // notification. - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { style: newBlockStyles } ); - setUserConfig( () => newUserConfig, { undoIgnore: true } ); - - createSuccessNotice( - sprintf( - // translators: %s: Title of the block e.g. 'Heading'. - __( '%s styles applied.' ), - getBlockType( name ).title - ), - { - type: 'snackbar', - actions: [ - { - label: __( 'Undo' ), - onClick() { - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { style: blockStyles } ); - setUserConfig( () => userConfig, { - undoIgnore: true, - } ); + if ( userHasEditedBehaviors ) { + __unstableMarkNextChangeAsNotPersistent(); + setBehavior( attributes.behaviors ); + createSuccessNotice( + sprintf( + // translators: %s: Title of the block e.g. 'Heading'. + __( '%s behaviors applied.' ), + getBlockType( name ).title + ), + { + type: 'snackbar', + actions: [ + { + label: __( 'Undo' ), + onClick() { + __unstableMarkNextChangeAsNotPersistent(); + setBehavior( inheritedBehaviors ); + setUserConfig( () => userConfig, { + undoIgnore: true, + } ); + }, }, - }, - ], - } - ); + ], + } + ); + } }, [ changes, attributes, userConfig, name ] ); return ( @@ -240,7 +277,7 @@ function PushChangesToGlobalStylesControl( { help={ sprintf( // translators: %s: Title of the block e.g. 'Heading'. __( - 'Apply this block’s typography, spacing, dimensions, and color styles to all %s blocks.' + 'Apply this block’s typography, spacing, dimensions, color styles, and behaviors to all %s blocks.' ), getBlockType( name ).title ) } @@ -250,7 +287,7 @@ function PushChangesToGlobalStylesControl( {