From 1b3a48049a1210169bb81fac7f9970c092be991d Mon Sep 17 00:00:00 2001 From: Artemio Morales Date: Fri, 15 Sep 2023 12:59:14 -0400 Subject: [PATCH] Image block: UI updates for the image lightbox (#54071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add initial implementation of image settings panel * Remove unnecessary code and fix reset functionality * Add UI to image block inspector * Add `lightbox` to the valid `theme.json` settings * Fix a bug with image selector and integrate with global styles * Update `theme.json` schema * Added the `@since`annotation * Refactor image settings panel and screen block Simplified the ImageSettingsPanel and ScreenBlock components. More specific changes: - Removed `name` and `settings` from the ImageSettingsPanel - Use `userSettings` instead of `settings` in the ImageSettingsPanel - Modified `onChangeLightbox` logic, it now takes a new setting instead of a boolean and directly passes to `onChange` - Updated the ScreenBlock component to account for this refactor * Add showUI option to lightbox settings A new option has been added to the lightbox settings in the Theme JSON reference guide and Gutenberg class. This `showUI` option allows users to toggle whether the Lightbox UI is displayed in the block editor or not. Also, updated the JSON schema accordingly to reflect these changes in theme.json files. * Add defaults for the `lightbox` to the GB `theme.json` * Change the falsy checks in image.js * Add filters; add legacy support for behaviors syntax I moved the logic to determine whether the lightbox should display or not to two render_block_data filters. One of these filters is inside of the index.php so that itc can exist in WP core, the other inside of blocks.php in order to offer legacy support for the Behaviors syntax in the Gutenberg plugin. Using the render_block_data instead of render_block allows us to store a 'lightboxEnabled' value on the block, which we can use to determine whether the lightbox should be rendered in these two separate locations relatively cleanly without needing to touch the markup. I added behaviors back to the valid top-level keys so that we can read it to offer legacy support. Lastly, I set the lightbox.enabled attribute to NULL by default so that we can determine whether the Behaviors syntax should override it or not. * Fix linter errors & add more expansive comments. * If no value is set for the lightbox in the Global Styles, then the block editor UI should inherit the value from `theme.json`. Likewise, if no value is set in the block attributes, the block editor UI should inherit the value from the Global Styles of the Image. * Rename `showUI` to `allowEditing` * Fix the `theme.json` schema * Use the globalPath for the settings on the PHP side * Add backwards support for enabling fade animation via the legacy syntax * Fix PHP linter errors * Fix error when checking lightbox['enabled'] value in global settings * Empty commit * Remove the default `false` value for `lightbox.enabled`attribute. * Add deprecation notice for 'behaviors' in image block I needed to add 'behaviors' back to the block.json attributes in order to read them on the JavaScript side in the editor to fire the deprecation notice. * Add deprecation for attribute in image block * Remove obsolete code now that block deprecation is in place * Add support for 'lightbox: true' syntax * Fix lightbox 'checked' attribute being read improperly * Add conditional display for settings panel at image block level * Fix an error with the theme.json schema. * Update docs with `npm run build:docs` * Copy `class-wp-theme-json-schema.php` from core into `class-wp-theme-json-schema-gutenberg.php` * Add a theme.json migration to v3 away from behaviors and to a new, simpler syntax used by the lightbox. * Remove the `null` value from lightbox.enabled in `lib/theme.json` * Remove `behaviors` from VALID_TOP_LEVEL_KEYS & VALID_SETTINGS * Revise backwards compatibility for behaviors; add deprecation to block_supports * Remove outdated comment * Update outdated comment * Update comment and shuffle the lines so the diff is easier on the eyes. * Update comment to explain why we use userSettings in image-settings-panel.js * Remove support for legacy fade configuration * Resolve lint error * Add clarifying comment regarding lightbox markup * Rename the migrate function to reflect that it's not a v3 migration * Add a `@since` in `gutenberg_should_render_lightbox` docblock * Add support for reading top-level 'lightbox' setting in editor By default, we read the lightbox settings underneath the 'core/image' in theme.json; however, the 'enabled' property there is undefined by default, which means it should be possible to declare a top-level setting for the lightbox that overrides an undefined block-level setting. While this appeared to be working on the PHP side, the UI wasn't accurately reflecting this inheritance structure, so this commit fixes that. Users should now be able to define a top-level lightbox setting as either 'lightbox: true' or 'lightbox: { enabled: true }' that will be used if the block-level lightbox setting for 'enabled' is undefined. * Revert "Add support for reading top-level 'lightbox' setting in editor" This reverts commit 2f5f122e5e2e505d4ccacffc2957df15cd63d628. * Add correct deprecation mentioning the Gutenberg version * Move 'allowEditing' to top-level settings * Fix top-level lightbox setting not being read properly * Fix 'false' values in theme.json not being stored in settings object * Fix error wherein 'undefined' was being passed to input component * Fix bug wherein lightbox UI would disappear if user value was set * Remove inheritance when determining whether to enable lightbox Rather than trying to check if the 'enabled' has been set or not and falling back to other levels of the theme.json inheritance structure, I decided to just read and use the settings as defined in theme.json. This is the expected behavior in Gutenberg from what I understand and has less edge cases. * Update comment * Update whitespace in theme.json Co-authored-by: Alex Lende * Add docblocks to clarify that `class-wp-theme-json-schema-gutenberg.php` is only put in GB as a temporary migration. * Add comments to clarify that behaviors.php is temporarily added to GB an will be removed in a future version. * Added integration fixtures for the lightbox * Fix incorrect reading of global lightbox settings * Clarify the comment getting the global lightbox settings * Fix PHPCS parenthesis error 🤦‍♂️ * Move lightbox settings to the block level * Remove support for shorthand * Remove 'lightbox' from hooks.js and add `.enabled` & `.allowEditing` to class-wp-theme-json-gutenberg.php --------- Co-authored-by: Michal Czaplinski Co-authored-by: Alex Lende --- docs/reference-guides/core-blocks.md | 9 +- .../theme-json-reference/theme-json-living.md | 11 + lib/block-supports/behaviors.php | 16 +- lib/blocks.php | 29 +++ lib/class-wp-theme-json-gutenberg.php | 9 +- lib/class-wp-theme-json-schema-gutenberg.php | 202 +++++++++++++++++ lib/load.php | 2 + lib/theme.json | 5 + .../src/components/global-styles/hooks.js | 2 + .../global-styles/image-settings-panel.js | 71 ++++++ .../src/components/global-styles/index.js | 4 + packages/block-library/src/image/block.json | 6 + .../block-library/src/image/deprecated.js | 211 +++++++++++++++++- packages/block-library/src/image/image.js | 33 +++ packages/block-library/src/image/index.php | 95 ++++---- .../components/global-styles/screen-block.js | 37 +++ schemas/json/theme.json | 23 ++ ...cated-v8-deprecate-behaviors-lightbox.html | 3 + ...cated-v8-deprecate-behaviors-lightbox.json | 18 ++ ...8-deprecate-behaviors-lightbox.parsed.json | 18 ++ ...precate-behaviors-lightbox.serialized.html | 3 + 21 files changed, 759 insertions(+), 48 deletions(-) create mode 100644 lib/class-wp-theme-json-schema-gutenberg.php create mode 100644 packages/block-editor/src/components/global-styles/image-settings-panel.js create mode 100644 test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.html create mode 100644 test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.json create mode 100644 test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.parsed.json create mode 100644 test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.serialized.html diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 3d5c9c344e9ce9..2c54c7d044c640 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -2,10 +2,9 @@ This page lists the blocks included in the block-library package. -- Items marked with a strikeout (~~strikeout~~) are explicitly disabled. -- Blocks marked with **Experimental:** true are only available when Gutenberg is active. -- Blocks marked with **Experimental:** fse are only available in the Site Editor. - +- Items marked with a strikeout (~~strikeout~~) are explicitly disabled. +- Blocks marked with **Experimental:** true are only available when Gutenberg is active. +- Blocks marked with **Experimental:** fse are only available in the Site Editor. @@ -340,7 +339,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre - **Name:** core/image - **Category:** media - **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone) -- **Attributes:** align, alt, aspectRatio, caption, height, href, id, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width +- **Attributes:** align, alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width ## Latest Comments diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index fe7dab112b2a45..4890386ca8333e 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -130,6 +130,17 @@ Settings related to layout. --- +### lightbox + +Settings related to the lightbox. + +| Property | Type | Default | Props | +| --- | --- | --- |--- | +| enabled | boolean | | | +| allowEditing | boolean | | | + +--- + ### position Settings related to position. diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php index b6692195eb5fb5..cf668ed22d8875 100644 --- a/lib/block-supports/behaviors.php +++ b/lib/block-supports/behaviors.php @@ -2,6 +2,10 @@ /** * Behaviors block support flag. * + * This file will NOT be backported to Core. It exists to provide a + * migration path for theme.json files that used the deprecated "behaviors". + * This file will be removed from Gutenberg in version 17.0.0. + * * @package gutenberg */ @@ -37,13 +41,23 @@ function gutenberg_register_behaviors_support( $block_type ) { /** * Add the directives and layout needed for the lightbox behavior. - * This functions shouldn't be in this file. It should be moved to a package (or somewhere else), where all the behaviors logic is defined. * * @param string $block_content Rendered block content. * @param array $block Block object. * @return string Filtered block content. */ function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { + + // We've deprecated the lightbox implementation via behaviors. + // While we may continue to explore behaviors in the future, the lightbox + // logic seems very specific to the image and will likely never be a part + // of behaviors, even in the future. With that in mind, we've rewritten the lightbox + // to be a feature of the image block and will also soon remove the block_supports. + // *Note: This logic for generating the lightbox markup has been duplicated and moved + // to the image block's index.php.* + // See https://github.com/WordPress/gutenberg/issues/53403. + _deprecated_function( 'gutenberg_render_behaviors_support_lightbox', 'Gutenberg 17.0.0', '' ); + $link_destination = isset( $block['attrs']['linkDestination'] ) ? $block['attrs']['linkDestination'] : 'none'; // Get the lightbox setting from the block attributes. if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) { diff --git a/lib/blocks.php b/lib/blocks.php index c54f5df68489ae..537fa9ce4b45e1 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -431,4 +431,33 @@ function gutenberg_legacy_wp_block_post_meta( $value, $object_id, $meta_key, $si return $value; } + add_filter( 'default_post_metadata', 'gutenberg_legacy_wp_block_post_meta', 10, 4 ); + +/** + * Complements the lightbox implementation for the 'core/image' block. + * + * This function is INTENTIONALLY left out of core as it only provides + * backwards compatibility for the legacy lightbox syntax that was only + * introduced in Gutenberg. The legacy syntax was using the `behaviors` key in + * the block attrbutes and the `theme.json` file. + * + * @since 16.7.0 + * + * @param array $block The block to check. + * @return array The block with the legacyLightboxSettings set if available. + */ +function gutenberg_should_render_lightbox( $block ) { + + if ( 'core/image' !== $block['blockName'] ) { + return $block; + } + + if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) { + $block['legacyLightboxSettings'] = $block['attrs']['behaviors']['lightbox']; + } + + return $block; +} + +add_filter( 'render_block_data', 'gutenberg_should_render_lightbox', 15, 1 ); diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 330b336bbd0a24..bf7a706923a120 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -346,6 +346,7 @@ class WP_Theme_JSON_Gutenberg { * `position.fixed` and `position.sticky`. * @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`. * @since 6.4.0 Added `layout.allowEditing`. + * @since 6.4.0 Added `lightbox`. * @var array */ const VALID_SETTINGS = array( @@ -386,6 +387,10 @@ class WP_Theme_JSON_Gutenberg { 'wideSize' => null, 'allowEditing' => null, ), + 'lightbox' => array( + 'enabled' => null, + 'allowEditing' => null, + ), 'position' => array( 'fixed' => null, 'sticky' => null, @@ -616,7 +621,7 @@ public function __construct( $theme_json = array(), $origin = 'theme' ) { $origin = 'theme'; } - $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); $registry = WP_Block_Type_Registry::get_instance(); $valid_block_names = array_keys( $registry->get_all_registered() ); $valid_element_names = array_keys( static::ELEMENTS ); @@ -2859,7 +2864,7 @@ protected static function filter_slugs( $node, $slugs ) { public static function remove_insecure_properties( $theme_json ) { $sanitized = array(); - $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); diff --git a/lib/class-wp-theme-json-schema-gutenberg.php b/lib/class-wp-theme-json-schema-gutenberg.php new file mode 100644 index 00000000000000..d11545751af362 --- /dev/null +++ b/lib/class-wp-theme-json-schema-gutenberg.php @@ -0,0 +1,202 @@ + 'border.radius', + 'spacing.customMargin' => 'spacing.margin', + 'spacing.customPadding' => 'spacing.padding', + 'typography.customLineHeight' => 'typography.lineHeight', + ); + + /** + * Function that migrates a given theme.json structure to the last version. + * + * @since 5.9.0 + * + * @param array $theme_json The structure to migrate. + * + * @return array The structure in the last version. + */ + public static function migrate( $theme_json ) { + if ( ! isset( $theme_json['version'] ) ) { + $theme_json = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + ); + } + + if ( 1 === $theme_json['version'] ) { + $theme_json = self::migrate_v1_to_v2( $theme_json ); + } + + if ( 2 === $theme_json['version'] ) { + $theme_json = self::migrate_deprecated_lightbox_behaviors( $theme_json ); + } + + return $theme_json; + } + + /** + * Removes the custom prefixes for a few properties + * that were part of v1: + * + * 'border.customRadius' => 'border.radius', + * 'spacing.customMargin' => 'spacing.margin', + * 'spacing.customPadding' => 'spacing.padding', + * 'typography.customLineHeight' => 'typography.lineHeight', + * + * @since 5.9.0 + * + * @param array $old Data to migrate. + * + * @return array Data without the custom prefixes. + */ + private static function migrate_v1_to_v2( $old ) { + // Copy everything. + $new = $old; + + // Overwrite the things that changed. + if ( isset( $old['settings'] ) ) { + $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS ); + } + + // Set the new version. + $new['version'] = 2; + + return $new; + } + + + /** + * Migrate away from the previous syntax that used a top-level "behaviors" key + * in the `theme.json` to a new "lightbox" setting. + * + * This function SHOULD NOT be ported to Core!!! + * + * It is a temporary migration that will be removed in Gutenberg 17.0.0 + * + * @since 16.7.0 + * + * @param array $old Data with (potentially) behaviors. + * @return array Data with behaviors removed. + */ + private static function migrate_deprecated_lightbox_behaviors( $old ) { + // Copy everything. + $new = $old; + + // Migrate the old behaviors syntax to the new "lightbox" syntax. + if ( isset( $old['behaviors']['blocks']['core/image']['lightbox']['enabled'] ) ) { + _wp_array_set( + $new, + array( 'settings', 'blocks', 'core/image', 'lightbox', 'enabled' ), + $old['behaviors']['blocks']['core/image']['lightbox']['enabled'] + ); + } + + // Migrate the behaviors setting to the new syntax. This setting controls + // whether the Lightbox UI shows up in the block editor. + if ( isset( $old['settings']['blocks']['core/image']['behaviors']['lightbox'] ) ) { + _wp_array_set( + $new, + array( 'settings', 'blocks', 'core/image', 'lightbox', 'allowEditing' ), + $old['settings']['blocks']['core/image']['behaviors']['lightbox'] + ); + } + + return $new; + } + + /** + * Processes the settings subtree. + * + * @since 5.9.0 + * + * @param array $settings Array to process. + * @param array $paths_to_rename Paths to rename. + * + * @return array The settings in the new format. + */ + private static function rename_paths( $settings, $paths_to_rename ) { + $new_settings = $settings; + + // Process any renamed/moved paths within default settings. + self::rename_settings( $new_settings, $paths_to_rename ); + + // Process individual block settings. + if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) { + foreach ( $new_settings['blocks'] as &$block_settings ) { + self::rename_settings( $block_settings, $paths_to_rename ); + } + } + + return $new_settings; + } + + /** + * Processes a settings array, renaming or moving properties. + * + * @since 5.9.0 + * + * @param array $settings Reference to settings either defaults or an individual block's. + * @param array $paths_to_rename Paths to rename. + */ + private static function rename_settings( &$settings, $paths_to_rename ) { + foreach ( $paths_to_rename as $original => $renamed ) { + $original_path = explode( '.', $original ); + $renamed_path = explode( '.', $renamed ); + $current_value = _wp_array_get( $settings, $original_path, null ); + + if ( null !== $current_value ) { + _wp_array_set( $settings, $renamed_path, $current_value ); + self::unset_setting_by_path( $settings, $original_path ); + } + } + } + + /** + * Removes a property from within the provided settings by its path. + * + * @since 5.9.0 + * + * @param array $settings Reference to the current settings array. + * @param array $path Path to the property to be removed. + */ + private static function unset_setting_by_path( &$settings, $path ) { + $tmp_settings = &$settings; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $last_key = array_pop( $path ); + foreach ( $path as $key ) { + $tmp_settings = &$tmp_settings[ $key ]; + } + + unset( $tmp_settings[ $last_key ] ); + } +} diff --git a/lib/load.php b/lib/load.php index c7492940030128..bb216069eccb53 100644 --- a/lib/load.php +++ b/lib/load.php @@ -201,6 +201,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/global-styles-and-settings.php'; require __DIR__ . '/class-wp-theme-json-data-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-gutenberg.php'; +require __DIR__ . '/class-wp-theme-json-schema-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-resolver-gutenberg.php'; require __DIR__ . '/class-wp-duotone-gutenberg.php'; require __DIR__ . '/blocks.php'; @@ -231,4 +232,5 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/dimensions.php'; require __DIR__ . '/block-supports/duotone.php'; require __DIR__ . '/block-supports/shadow.php'; +require __DIR__ . '/block-supports/behaviors.php'; require __DIR__ . '/block-supports/background.php'; diff --git a/lib/theme.json b/lib/theme.json index 91990215470b91..671dd50227852b 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -273,6 +273,11 @@ "radius": true } }, + "core/image": { + "lightbox": { + "allowEditing": true + } + }, "core/pullquote": { "border": { "color": true, diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 7d81e0d4d224da..4bbc9c40588e03 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -50,6 +50,8 @@ const VALID_SETTINGS = [ 'layout.contentSize', 'layout.definitions', 'layout.wideSize', + 'lightbox.enabled', + 'lightbox.allowEditing', 'position.fixed', 'position.sticky', 'spacing.customSpacingSize', diff --git a/packages/block-editor/src/components/global-styles/image-settings-panel.js b/packages/block-editor/src/components/global-styles/image-settings-panel.js new file mode 100644 index 00000000000000..d3850e5fc9722c --- /dev/null +++ b/packages/block-editor/src/components/global-styles/image-settings-panel.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + ToggleControl, +} from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; + +export function useHasImageSettingsPanel( name, settings, userSettings ) { + // Note: If lightbox userSettings exists, that means + // they were defined via the Global Styles UI and + // will NOT be a boolean value or contain the `allowEditing` + // property, so we should show the settings panel in those cases. + return ( + ( name === 'core/image' && settings?.lightbox?.allowEditing ) || + !! userSettings?.lightbox + ); +} + +export default function ImageSettingsPanel( { + onChange, + userSettings, + settings, + panelId, +} ) { + const resetLightbox = () => { + onChange( undefined ); + }; + + const onChangeLightbox = ( newSetting ) => { + onChange( { + enabled: newSetting, + } ); + }; + + let lightboxChecked = false; + + if ( settings?.lightbox?.enabled ) { + lightboxChecked = settings.lightbox.enabled; + } + + return ( + <> + + !! userSettings?.lightbox } + label={ __( 'Expand on Click' ) } + onDeselect={ resetLightbox } + isShownByDefault={ true } + panelId={ panelId } + > + + + + + ); +} diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 24bab543b9ada6..76a95357ba52b4 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -23,5 +23,9 @@ 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 ImageSettingsPanel, + useHasImageSettingsPanel, +} from './image-settings-panel'; export { default as AdvancedPanel } from './advanced-panel'; export { areGlobalStyleConfigsEqual } from './utils'; diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index a360e595281267..b8c0432866d968 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -33,6 +33,12 @@ "selector": "figcaption", "__experimentalRole": "content" }, + "lightbox": { + "type": "object", + "enabled": { + "type": "boolean" + } + }, "title": { "type": "string", "source": "attribute", diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js index 83816e1e3f74e6..4205da8e117b91 100644 --- a/packages/block-library/src/image/deprecated.js +++ b/packages/block-library/src/image/deprecated.js @@ -941,4 +941,213 @@ const v7 = { }, }; -export default [ v7, v6, v5, v4, v3, v2, v1 ]; +const v8 = { + attributes: { + align: { + type: 'string', + }, + behaviors: { + type: 'object', + }, + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + __experimentalRole: 'content', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + __experimentalRole: 'content', + }, + caption: { + type: 'string', + source: 'html', + selector: 'figcaption', + __experimentalRole: 'content', + }, + title: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'title', + __experimentalRole: 'content', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'href', + __experimentalRole: 'content', + }, + rel: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'rel', + }, + linkClass: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'class', + }, + id: { + type: 'number', + __experimentalRole: 'content', + }, + width: { + type: 'string', + }, + height: { + type: 'string', + }, + aspectRatio: { + type: 'string', + }, + scale: { + type: 'string', + }, + sizeSlug: { + type: 'string', + }, + linkDestination: { + type: 'string', + }, + linkTarget: { + type: 'string', + source: 'attribute', + selector: 'figure > a', + attribute: 'target', + }, + }, + supports: { + anchor: true, + color: { + text: false, + background: false, + }, + filter: { + duotone: true, + }, + __experimentalBorder: { + color: true, + radius: true, + width: true, + __experimentalSkipSerialization: true, + __experimentalDefaultControls: { + color: true, + radius: true, + width: true, + }, + }, + }, + migrate( { width, height, ...attributes } ) { + const { + behaviors: { + lightbox: { enabled }, + }, + } = attributes; + const newAttributes = { + ...attributes, + lightbox: { + enabled, + }, + }; + delete newAttributes.behaviors; + return newAttributes; + }, + isEligible( attributes ) { + return !! attributes.behaviors; + }, + save( { attributes } ) { + const { + url, + alt, + caption, + align, + href, + rel, + linkClass, + width, + height, + aspectRatio, + scale, + id, + linkTarget, + sizeSlug, + title, + } = attributes; + + const newRel = ! rel ? undefined : rel; + const borderProps = getBorderClassesAndStyles( attributes ); + + const classes = classnames( { + [ `align${ align }` ]: align, + [ `size-${ sizeSlug }` ]: sizeSlug, + 'is-resized': width || height, + 'has-custom-border': + !! borderProps.className || + ( borderProps.style && + Object.keys( borderProps.style ).length > 0 ), + } ); + + const imageClasses = classnames( borderProps.className, { + [ `wp-image-${ id }` ]: !! id, + } ); + + const image = ( + { + ); + + const figure = ( + <> + { href ? ( + + { image } + + ) : ( + image + ) } + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); + + return ( +
+ { figure } +
+ ); + }, +}; + +export default [ v8, v7, v6, v5, v4, v3, v2, v1 ]; diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 02c0319db6c98e..04839a47b880af 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -7,6 +7,7 @@ import { ResizableBox, Spinner, TextareaControl, + ToggleControl, TextControl, ToolbarButton, ToolbarGroup, @@ -23,6 +24,7 @@ import { __experimentalImageURLInputUI as ImageURLInputUI, MediaReplaceFlow, store as blockEditorStore, + useSetting, BlockAlignmentControl, __experimentalImageEditor as ImageEditor, __experimentalGetElementClassName, @@ -113,6 +115,7 @@ export default function Image( { scale, linkTarget, sizeSlug, + lightbox, } = attributes; // The only supported unit is px, so we can parseInt to strip the px here. @@ -171,6 +174,7 @@ export default function Image( { }, [ clientId ] ); + const { replaceBlocks, toggleSelection } = useDispatch( blockEditorStore ); const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); @@ -365,6 +369,14 @@ export default function Image( { availableUnits: [ 'px' ], } ); + const lightboxSetting = useSetting( 'lightbox' ); + + const showLightboxToggle = + lightboxSetting === true || lightboxSetting?.allowEditing === true; + + const lightboxChecked = + lightbox?.enabled || ( ! lightbox && lightboxSetting?.enabled ); + const dimensionsControl = ( + { showLightboxToggle && ( + !! lightbox } + label={ __( 'Expand on Click' ) } + onDeselect={ () => { + setAttributes( { lightbox: undefined } ); + } } + isShownByDefault={ true } + > + { + setAttributes( { + lightbox: { enabled: newValue }, + } ); + } } + /> + + ) } diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index fceba38244033c..1bf93bd81a9d05 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -31,67 +31,91 @@ function render_block_core_image( $attributes, $content, $block ) { $processor->set_attribute( 'data-id', $attributes['data-id'] ); } - $should_load_view_script = false; - $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; + $lightbox_enabled = false; + $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none'; + $lightbox_settings = block_core_image_get_lightbox_settings( $block->parsed_block ); - // Get the lightbox setting from the block attributes. - if ( isset( $attributes['lightbox'] ) ) { - $lightbox_settings = $attributes['lightbox']; - } + // If the lightbox is enabled and the image is not linked, flag the lightbox to be rendered. + if ( isset( $lightbox_settings ) && 'none' === $link_destination ) { - // If the lightbox is enabled, the image is not linked, and the Interactivity API is enabled, load the view script. - if ( isset( $lightbox_settings['enabled'] ) && - true === $lightbox_settings['enabled'] && - 'none' === $link_destination - ) { - $should_load_view_script = true; + if ( isset( $lightbox_settings['enabled'] ) && true === $lightbox_settings['enabled'] ) { + $lightbox_enabled = true; + } } // If at least one block in the page has the lightbox, mark the block type as interactive. - if ( $should_load_view_script ) { + if ( $lightbox_enabled ) { $block->block_type->supports['interactivity'] = true; } + // Determine whether the view script should be enqueued or not. $view_js_file = 'wp-block-image-view'; if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { + if ( ! $lightbox_enabled && in_array( $view_js_file, $script_handles, true ) ) { $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); } // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { + if ( $lightbox_enabled && ! in_array( $view_js_file, $script_handles, true ) ) { $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); } } + if ( $lightbox_enabled ) { + return block_core_image_render_lightbox( $processor->get_updated_html(), $block->parsed_block ); + } + return $processor->get_updated_html(); } - /** - * Add the directives and layout needed for the lightbox behavior. + * Add the lightboxEnabled flag to the block data. * - * @param string $block_content Rendered block content. - * @param array $block Block object. - * @return string Filtered block content. + * This is used to determine whether the lightbox should be rendered or not. + * + * @param array $block Block data. + * @return array Filtered block data. */ -function block_core_image_render_lightbox( $block_content, $block ) { - $link_destination = isset( $block['attrs']['linkDestination'] ) ? $block['attrs']['linkDestination'] : 'none'; +function block_core_image_get_lightbox_settings( $block ) { // Get the lightbox setting from the block attributes. if ( isset( $block['attrs']['lightbox'] ) ) { $lightbox_settings = $block['attrs']['lightbox']; + // If the lightbox setting is not set in the block attributes, + // check the legacy lightbox settings that are set using the + // `gutenberg_should_render_lightbox` filter. + // We can remove this elseif statement when the legacy lightbox settings are removed. + } elseif ( isset( $block['legacyLightboxSettings'] ) ) { + $lightbox_settings = $block['legacyLightboxSettings']; } - if ( ! isset( $lightbox_settings ) || 'none' !== $link_destination ) { - return $block_content; + if ( ! isset( $lightbox_settings ) ) { + $lightbox_settings = gutenberg_get_global_settings( array( 'lightbox' ), array( 'block_name' => 'core/image' ) ); + + // If not present in global settings, check the top-level global settings. + // + // NOTE: If no block-level settings are found, the previous call to + // `gutenberg_get_global_settings` will return the whole `theme.json` + // structure in which case we can check if the "lightbox" key is present at + // the top-level of the global settings and use its value. + if ( isset( $lightbox_settings['lightbox'] ) ) { + $lightbox_settings = gutenberg_get_global_settings( array( 'lightbox' ) ); + } } - if ( isset( $lightbox_settings['enabled'] ) && false === $lightbox_settings['enabled'] ) { - return $block_content; - } + return $lightbox_settings ?? null; +} + +/** + * Add the directives and layout needed for the lightbox behavior. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function block_core_image_render_lightbox( $block_content, $block ) { $processor = new WP_HTML_Tag_Processor( $block_content ); @@ -109,7 +133,7 @@ function block_core_image_render_lightbox( $block_content, $block ) { } $content = $processor->get_updated_html(); - // Currently, the only supported animation is 'zoom'. + // Currently, we are only enabling the zoom animation. $lightbox_animation = 'zoom'; // We want to store the src in the context so we can set it dynamically when the lightbox is opened. @@ -258,16 +282,9 @@ function block_core_image_render_lightbox( $block_content, $block ) { return str_replace( '', $lightbox_html . '', $body_content ); } -// TODO: We should not be adding a separate filter but rather move the -// the lightbox rendering to the `render_block_core_image` function. - -// Use priority 15 to run this hook after other hooks/plugins. -// They could use the `render_block_{$this->name}` filter to modify the markup. -add_filter( 'render_block_core/image', 'block_core_image_render_lightbox', 15, 2 ); - - /** - * Registers the `core/image` block on server. - */ +/** + * Registers the `core/image` block on server. + */ function register_block_core_image() { register_block_type_from_metadata( __DIR__ . '/image', @@ -276,4 +293,4 @@ function register_block_core_image() { ) ); } - add_action( 'init', 'register_block_core_image' ); +add_action( 'init', 'register_block_core_image' ); 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 f683c559e59bcb..f2bc5e920067d5 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -66,6 +66,7 @@ const { useHasColorPanel, useHasEffectsPanel, useHasFiltersPanel, + useHasImageSettingsPanel, useGlobalStyle, BorderPanel: StylesBorderPanel, ColorPanel: StylesColorPanel, @@ -73,6 +74,7 @@ const { DimensionsPanel: StylesDimensionsPanel, EffectsPanel: StylesEffectsPanel, FiltersPanel: StylesFiltersPanel, + ImageSettingsPanel, AdvancedPanel: StylesAdvancedPanel, } = unlock( blockEditorPrivateApis ); @@ -90,6 +92,7 @@ function ScreenBlock( { name, variation } ) { shouldDecodeEncode: false, } ); const [ rawSettings, setSettings ] = useGlobalSetting( '', name ); + const [ userSettings ] = useGlobalSetting( '', name, 'user' ); const settings = useSettingsForBlockElement( rawSettings, name ); const blockType = getBlockType( name ); @@ -113,6 +116,11 @@ function ScreenBlock( { name, variation } ) { const hasDimensionsPanel = useHasDimensionsPanel( settings ); const hasEffectsPanel = useHasEffectsPanel( settings ); const hasFiltersPanel = useHasFiltersPanel( settings ); + const hasImageSettingsPanel = useHasImageSettingsPanel( + name, + settings, + userSettings + ); const hasVariationsPanel = !! blockVariations?.length && ! variation; const { canEditCSS } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = @@ -158,6 +166,27 @@ function ScreenBlock( { name, variation } ) { } ); } }; + const onChangeLightbox = ( newSetting ) => { + // If the newSetting is undefined, this means that the user has deselected + // (reset) the lightbox setting. + if ( newSetting === undefined ) { + setSettings( { + ...rawSettings, + lightbox: undefined, + } ); + + // Otherwise, we simply set the lightbox setting to the new value but + // taking care of not overriding the other lightbox settings. + } else { + setSettings( { + ...rawSettings, + lightbox: { + ...rawSettings.lightbox, + ...newSetting, + }, + } ); + } + }; const onChangeBorders = ( newStyle ) => { if ( ! newStyle?.border ) { setStyle( newStyle ); @@ -265,6 +294,14 @@ function ScreenBlock( { name, variation } ) { includeLayoutControls /> ) } + { hasImageSettingsPanel && ( + + ) } + { canEditCSS && (

diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 11e36df5c1ef6a..e0f3efc01eb1fc 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -296,6 +296,26 @@ } } }, + "settingsPropertiesLightbox": { + "type": "object", + "additionalProperties": false, + "properties": { + "lightbox": { + "description": "Settings related to the lightbox.", + "type": "object", + "properties": { + "enabled": { + "description": "Defines whether the lightbox is enabled or not.", + "type": "boolean" + }, + "allowEditing": { + "description": "Defines whether to show the Lightbox UI in the block editor. If set to `false`, the user won't be able to change the lightbox settings in the block editor.", + "type": "boolean" + } + } + } + } + }, "settingsPropertiesPosition": { "type": "object", "properties": { @@ -688,6 +708,7 @@ { "$ref": "#/definitions/settingsPropertiesDimensions" }, { "$ref": "#/definitions/settingsPropertiesShadow" }, { "$ref": "#/definitions/settingsPropertiesLayout" }, + { "$ref": "#/definitions/settingsPropertiesLightbox" }, { "$ref": "#/definitions/settingsPropertiesPosition" }, { "$ref": "#/definitions/settingsPropertiesSpacing" }, { "$ref": "#/definitions/settingsPropertiesTypography" }, @@ -708,6 +729,7 @@ "color": {}, "dimensions": {}, "layout": {}, + "lightbox": {}, "position": {}, "shadow": {}, "spacing": {}, @@ -2177,6 +2199,7 @@ "background": {}, "color": {}, "layout": {}, + "lightbox": {}, "spacing": {}, "typography": {}, "border": {}, diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.html b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.html new file mode 100644 index 00000000000000..9feeb105951daa --- /dev/null +++ b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.html @@ -0,0 +1,3 @@ + +

+ diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.json b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.json new file mode 100644 index 00000000000000..a32f031dd34f43 --- /dev/null +++ b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.json @@ -0,0 +1,18 @@ +[ + { + "name": "core/image", + "isValid": true, + "attributes": { + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==", + "alt": "", + "caption": "", + "lightbox": { + "enabled": true + }, + "id": 8, + "sizeSlug": "large", + "linkDestination": "none" + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.parsed.json b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.parsed.json new file mode 100644 index 00000000000000..0ca652ff77f835 --- /dev/null +++ b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/image", + "attrs": { + "lightbox": { + "enabled": true + }, + "id": 8, + "sizeSlug": "large", + "linkDestination": "none" + }, + "innerBlocks": [], + "innerHTML": "\n
\"\"
\n", + "innerContent": [ + "\n
\"\"
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.serialized.html new file mode 100644 index 00000000000000..9feeb105951daa --- /dev/null +++ b/test/integration/fixtures/blocks/core__image__deprecated-v8-deprecate-behaviors-lightbox.serialized.html @@ -0,0 +1,3 @@ + +
+