diff --git a/lib/compat.php b/lib/compat.php index 8d1f908161870e..f328928085efa9 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -191,3 +191,54 @@ function gutenberg_safe_style_attrs( $attrs ) { return $attrs; } add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs' ); + +/** + * The new gallery block format is not compatible with the use_BalanceTags option + * in WP versions <= 5.8 https://core.trac.wordpress.org/ticket/54130. + * This method adds a variable to the wp namespace to indicate if the new gallery block + * format can be enabled or not. It needs to be added this early and to the wp namespace + * as it needs to be available when the intial block parsing runs on editor load, and most of + * the editor store and standard flags are not loaded yet at that point + * + * @since 12.1.0 + * @todo This should be removed when the minimum required WP version is >= 5.9. + * + * @return void. + */ +function gutenberg_check_gallery_block_v2_compatibility() { + $use_balance_tags = (int) get_option( 'use_balanceTags' ); + $v2_gallery_enabled = boolval( 1 !== $use_balance_tags || is_wp_version_compatible( '5.9' ) ) ? 'true' : 'false'; + + wp_add_inline_script( + 'wp-dom-ready', + 'wp.galleryBlockV2Enabled = ' . $v2_gallery_enabled . ';', + 'after' + ); +} +add_action( 'init', 'gutenberg_check_gallery_block_v2_compatibility' ); + +/** + * Prevent use_balanceTags being enabled on WordPress 5.8 or earlier as it breaks + * the layout of the new Gallery block. + * + * @since 12.1.0 + * @todo This should be removed when the minimum required WP version is >= 5.9. + * + * @param int $new_value The new value for use_balanceTags. + */ +function gutenberg_use_balancetags_check( $new_value ) { + global $wp_version; + + if ( 1 === (int) $new_value && version_compare( $wp_version, '5.9', '<' ) ) { + /* translators: %s: Minimum required version */ + $message = sprintf( __( 'Gutenberg requires WordPress %s or later in order to enable the “Correct invalidly nested XHTML automatically” option. Please upgrade WordPress before enabling.', 'gutenberg' ), '5.9' ); + add_settings_error( 'gutenberg_use_balancetags_check', 'gutenberg_use_balancetags_check', $message, 'error' ); + if ( class_exists( 'WP_CLI' ) ) { + WP_CLI::error( $message ); + } + return 0; + } + + return $new_value; +} +add_filter( 'pre_update_option_use_balanceTags', 'gutenberg_use_balancetags_check' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index bd4ca968073f8a..75d4b529ee2256 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -101,7 +101,7 @@ function gutenberg_experiments_editor_settings( $settings ) { // This bypass needs to remain in place until this is resolved and a patch released. // https://core.trac.wordpress.org/ticket/54130. $experiments_settings = array( - '__unstableGalleryWithImageBlocks' => (int) get_option( 'use_balanceTags' ) !== 1, + '__unstableGalleryWithImageBlocks' => (int) get_option( 'use_balanceTags' ) !== 1 || is_wp_version_compatible( '5.9' ), ); return array_merge( $settings, $experiments_settings ); } diff --git a/packages/block-library/src/gallery/deprecated.js b/packages/block-library/src/gallery/deprecated.js index bc89ebbcc9ab16..001bbba8ea8366 100644 --- a/packages/block-library/src/gallery/deprecated.js +++ b/packages/block-library/src/gallery/deprecated.js @@ -7,7 +7,22 @@ import { map, some } from 'lodash'; /** * WordPress dependencies */ -import { RichText } from '@wordpress/block-editor'; +import { RichText, useBlockProps } from '@wordpress/block-editor'; + +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { + LINK_DESTINATION_ATTACHMENT, + LINK_DESTINATION_MEDIA, + LINK_DESTINATION_NONE, +} from './constants'; +import { isGalleryV2Enabled } from './shared'; + +const DEPRECATED_LINK_DESTINATION_MEDIA = 'file'; +const DEPRECATED_LINK_DESTINATION_ATTACHMENT = 'post'; /** * Original function to determine default number of columns from a block's @@ -19,422 +34,202 @@ import { RichText } from '@wordpress/block-editor'; * @return {number} Default number of columns for the gallery. */ export function defaultColumnsNumberV1( attributes ) { - return Math.min( 3, attributes.images.length ); + return Math.min( 3, attributes?.images?.length ); } -const deprecated = [ - { - attributes: { - images: { - type: 'array', - default: [], - source: 'query', - selector: '.blocks-gallery-item', - query: { - url: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'src', - }, - fullUrl: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'data-full-url', - }, - link: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'data-link', - }, - alt: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'alt', - default: '', - }, - id: { - type: 'string', - source: 'attribute', - selector: 'img', - attribute: 'data-id', - }, - caption: { - type: 'string', - source: 'html', - selector: '.blocks-gallery-item__caption', - }, - }, - }, - ids: { - type: 'array', - items: { - type: 'number', - }, - default: [], - }, - columns: { - type: 'number', - minimum: 1, - maximum: 8, - }, - caption: { - type: 'string', - source: 'html', - selector: '.blocks-gallery-caption', - }, - imageCrop: { - type: 'boolean', - default: true, - }, - linkTo: { - type: 'string', - default: 'none', - }, - sizeSlug: { - type: 'string', - default: 'large', - }, - }, - supports: { - align: true, - }, - isEligible( { linkTo } ) { - return ! linkTo || linkTo === 'attachment' || linkTo === 'media'; - }, - migrate( attributes ) { - let linkTo = attributes.linkTo; - if ( ! attributes.linkTo ) { - linkTo = 'none'; - } else if ( attributes.linkTo === 'attachment' ) { - linkTo = 'post'; - } else if ( attributes.linkTo === 'media' ) { - linkTo = 'file'; - } +/** + * Original function to determine new href and linkDestination values for an image block from the + * supplied Gallery link destination. + * + * Used in deprecations: v1-6. + * + * @param {Object} image Gallery image. + * @param {string} destination Gallery's selected link destination. + * @return {Object} New attributes to assign to image block. + */ +export function getHrefAndDestination( image, destination ) { + // Need to determine the URL that the selected destination maps to. + // Gutenberg and WordPress use different constants so the new link + // destination also needs to be tweaked. + switch ( destination ) { + case DEPRECATED_LINK_DESTINATION_MEDIA: return { - ...attributes, - linkTo, + href: image?.source_url || image?.url, // eslint-disable-line camelcase + linkDestination: LINK_DESTINATION_MEDIA, }; - }, - save( { attributes } ) { - const { - images, - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - caption, - linkTo, - } = attributes; - - return ( -
- - { ! RichText.isEmpty( caption ) && ( - - ) } -
- ); + return {}; +} + +function runV2Migration( attributes ) { + let linkTo = attributes.linkTo ? attributes.linkTo : 'none'; + + if ( linkTo === 'post' ) { + linkTo = 'attachment'; + } else if ( linkTo === 'file' ) { + linkTo = 'media'; + } + + const imageBlocks = attributes.images.map( ( image ) => { + return getImageBlock( image, attributes.sizeSlug, linkTo ); + } ); + + return [ + { + caption: attributes.caption, + columns: attributes.columns, + imageCrop: attributes.imageCrop, + linkTo, + sizeSlug: attributes.sizeSlug, + allowResize: false, }, - }, - { - attributes: { - images: { - type: 'array', - default: [], - source: 'query', - selector: '.blocks-gallery-item', - query: { - url: { - source: 'attribute', - selector: 'img', - attribute: 'src', - }, - fullUrl: { - source: 'attribute', - selector: 'img', - attribute: 'data-full-url', - }, - link: { - source: 'attribute', - selector: 'img', - attribute: 'data-link', - }, - alt: { - source: 'attribute', - selector: 'img', - attribute: 'alt', - default: '', - }, - id: { - source: 'attribute', - selector: 'img', - attribute: 'data-id', - }, - caption: { - type: 'string', - source: 'html', - selector: '.blocks-gallery-item__caption', - }, + imageBlocks, + ]; +} +/** + * Gets an Image block from gallery image data + * + * Used to migrate Galleries to nested Image InnerBlocks. + * + * @param {Object} image Image properties. + * @param {string} sizeSlug Gallery sizeSlug attribute. + * @param {string} linkTo Gallery linkTo attribute. + * @return {Object} Image block. + */ +export function getImageBlock( image, sizeSlug, linkTo ) { + return createBlock( 'core/image', { + ...( image.id && { id: parseInt( image.id ) } ), + url: image.url, + alt: image.alt, + caption: image.caption, + sizeSlug, + ...getHrefAndDestination( image, linkTo ), + } ); +} + +const v6 = { + attributes: { + images: { + type: 'array', + default: [], + source: 'query', + selector: '.blocks-gallery-item', + query: { + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + fullUrl: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'data-full-url', + }, + link: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'data-link', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + id: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'data-id', + }, + caption: { + type: 'string', + source: 'html', + selector: '.blocks-gallery-item__caption', }, }, - ids: { - type: 'array', - default: [], - }, - columns: { + }, + ids: { + type: 'array', + items: { type: 'number', }, - caption: { - type: 'string', - source: 'html', - selector: '.blocks-gallery-caption', - }, - imageCrop: { - type: 'boolean', - default: true, - }, - linkTo: { - type: 'string', - default: 'none', - }, + default: [], }, - supports: { - align: true, + columns: { + type: 'number', + minimum: 1, + maximum: 8, }, - isEligible( { ids } ) { - return ids && ids.some( ( id ) => typeof id === 'string' ); + caption: { + type: 'string', + source: 'html', + selector: '.blocks-gallery-caption', }, - migrate( attributes ) { - return { - ...attributes, - ids: map( attributes.ids, ( id ) => { - const parsedId = parseInt( id, 10 ); - return Number.isInteger( parsedId ) ? parsedId : null; - } ), - }; + imageCrop: { + type: 'boolean', + default: true, }, - save( { attributes } ) { - const { - images, - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - caption, - linkTo, - } = attributes; - - return ( -
- - { ! RichText.isEmpty( caption ) && ( - - ) } -
- ); + linkTo: { + type: 'string', }, - }, - { - attributes: { - images: { - type: 'array', - default: [], - source: 'query', - selector: 'ul.wp-block-gallery .blocks-gallery-item', - query: { - url: { - source: 'attribute', - selector: 'img', - attribute: 'src', - }, - fullUrl: { - source: 'attribute', - selector: 'img', - attribute: 'data-full-url', - }, - alt: { - source: 'attribute', - selector: 'img', - attribute: 'alt', - default: '', - }, - id: { - source: 'attribute', - selector: 'img', - attribute: 'data-id', - }, - link: { - source: 'attribute', - selector: 'img', - attribute: 'data-link', - }, - caption: { - type: 'array', - source: 'children', - selector: 'figcaption', - }, - }, - }, - ids: { - type: 'array', - default: [], - }, - columns: { - type: 'number', - }, - imageCrop: { - type: 'boolean', - default: true, - }, - linkTo: { - type: 'string', - default: 'none', - }, + sizeSlug: { + type: 'string', + default: 'large', }, - supports: { - align: true, - }, - save( { attributes } ) { - const { - images, - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - linkTo, - } = attributes; - return ( - - ); - }, + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); }, - { - attributes: { - images: { - type: 'array', - default: [], - source: 'query', - selector: 'ul.wp-block-gallery .blocks-gallery-item', - query: { - url: { - source: 'attribute', - selector: 'img', - attribute: 'src', - }, - alt: { - source: 'attribute', - selector: 'img', - attribute: 'alt', - default: '', - }, - id: { - source: 'attribute', - selector: 'img', - attribute: 'data-id', - }, - link: { - source: 'attribute', - selector: 'img', - attribute: 'data-link', - }, - caption: { - type: 'array', - source: 'children', - selector: 'figcaption', - }, + migrate( attributes ) { + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + return attributes; + }, +}; +const v5 = { + attributes: { + images: { + type: 'array', + default: [], + source: 'query', + selector: '.blocks-gallery-item', + query: { + url: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + fullUrl: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'data-full-url', + }, + link: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'data-link', + }, + alt: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + id: { + type: 'string', + source: 'attribute', + selector: 'img', + attribute: 'data-id', + }, + caption: { + type: 'string', + source: 'html', + selector: '.blocks-gallery-item__caption', }, }, - columns: { + }, + ids: { + type: 'array', + items: { type: 'number', }, - imageCrop: { - type: 'boolean', - default: true, - }, - linkTo: { - type: 'string', - default: 'none', - }, + default: [], }, - isEligible( { images, ids } ) { - return ( - images && - images.length > 0 && - ( ( ! ids && images ) || - ( ids && images && ids.length !== images.length ) || - some( images, ( id, index ) => { - if ( ! id && ids[ index ] !== null ) { - return true; - } - return parseInt( id, 10 ) !== ids[ index ]; - } ) ) - ); + columns: { + type: 'number', + minimum: 1, + maximum: 8, }, - migrate( attributes ) { - return { - ...attributes, - ids: map( attributes.images, ( { id } ) => { - if ( ! id ) { - return null; - } - return parseInt( id, 10 ); - } ), - }; + caption: { + type: 'string', + source: 'html', + selector: '.blocks-gallery-caption', + }, + imageCrop: { + type: 'boolean', + default: true, + }, + linkTo: { + type: 'string', + default: 'none', }, - supports: { - align: true, - }, - save( { attributes } ) { - const { - images, - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - linkTo, - } = attributes; - return ( - - ); - }, + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); }, - { - attributes: { - images: { - type: 'array', - default: [], - source: 'query', - selector: - 'div.wp-block-gallery figure.blocks-gallery-image img', - query: { - url: { - source: 'attribute', - attribute: 'src', - }, - alt: { - source: 'attribute', - attribute: 'alt', - default: '', - }, - id: { - source: 'attribute', - attribute: 'data-id', - }, +}; + +const v4 = { + attributes: { + images: { + type: 'array', + default: [], + source: 'query', + selector: '.blocks-gallery-item', + query: { + url: { + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + fullUrl: { + source: 'attribute', + selector: 'img', + attribute: 'data-full-url', + }, + link: { + source: 'attribute', + selector: 'img', + attribute: 'data-link', + }, + alt: { + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + id: { + source: 'attribute', + selector: 'img', + attribute: 'data-id', + }, + caption: { + type: 'string', + source: 'html', + selector: '.blocks-gallery-item__caption', }, - }, - columns: { - type: 'number', - }, - imageCrop: { - type: 'boolean', - default: true, - }, - linkTo: { - type: 'string', - default: 'none', - }, - align: { - type: 'string', - default: 'none', }, }, - supports: { - align: true, - }, - save( { attributes } ) { - const { - images, - columns = defaultColumnsNumberV1( attributes ), - align, - imageCrop, - linkTo, - } = attributes; - const className = classnames( `columns-${ columns }`, { - alignnone: align === 'none', - 'is-cropped': imageCrop, - } ); - return ( -
+ ids: { + type: 'array', + default: [], + }, + columns: { + type: 'number', + }, + caption: { + type: 'string', + source: 'html', + selector: '.blocks-gallery-caption', + }, + imageCrop: { + type: 'boolean', + default: true, + }, + linkTo: { + type: 'string', + default: 'none', + }, + }, + supports: { + align: true, + }, + isEligible( { ids } ) { + return ids && ids.some( ( id ) => typeof id === 'string' ); + }, + migrate( attributes ) { + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + return { + ...attributes, + ids: map( attributes.ids, ( id ) => { + const parsedId = parseInt( id, 10 ); + return Number.isInteger( parsedId ) ? parsedId : null; + } ), + }; + }, + save( { attributes } ) { + const { + images, + columns = defaultColumnsNumberV1( attributes ), + imageCrop, + caption, + linkTo, + } = attributes; + + return ( +
+
    { images.map( ( image ) => { let href; switch ( linkTo ) { case 'media': - href = image.url; + href = image.fullUrl || image.url; break; case 'attachment': href = image.link; @@ -691,22 +579,406 @@ const deprecated = [ src={ image.url } alt={ image.alt } data-id={ image.id } + data-full-url={ image.fullUrl } + data-link={ image.link } + className={ + image.id ? `wp-image-${ image.id }` : null + } /> ); return ( -
    - { href ? { img } : img } -
    +
    + { href ? ( + { img } + ) : ( + img + ) } + { ! RichText.isEmpty( image.caption ) && ( + + ) } +
    + ); } ) } -
- ); + + { ! RichText.isEmpty( caption ) && ( + + ) } + + ); + }, +}; +const v3 = { + attributes: { + images: { + type: 'array', + default: [], + source: 'query', + selector: 'ul.wp-block-gallery .blocks-gallery-item', + query: { + url: { + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + fullUrl: { + source: 'attribute', + selector: 'img', + attribute: 'data-full-url', + }, + alt: { + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + id: { + source: 'attribute', + selector: 'img', + attribute: 'data-id', + }, + link: { + source: 'attribute', + selector: 'img', + attribute: 'data-link', + }, + caption: { + type: 'array', + source: 'children', + selector: 'figcaption', + }, + }, + }, + ids: { + type: 'array', + default: [], }, + columns: { + type: 'number', + }, + imageCrop: { + type: 'boolean', + default: true, + }, + linkTo: { + type: 'string', + default: 'none', + }, + }, + supports: { + align: true, + }, + save( { attributes } ) { + const { + images, + columns = defaultColumnsNumberV1( attributes ), + imageCrop, + linkTo, + } = attributes; + return ( +
    + { images.map( ( image ) => { + let href; + + switch ( linkTo ) { + case 'media': + href = image.fullUrl || image.url; + break; + case 'attachment': + href = image.link; + break; + } + + const img = ( + { + ); + + return ( +
  • +
    + { href ? { img } : img } + { image.caption && image.caption.length > 0 && ( + + ) } +
    +
  • + ); + } ) } +
+ ); + }, + migrate( attributes ) { + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + return attributes; + }, +}; +const v2 = { + attributes: { + images: { + type: 'array', + default: [], + source: 'query', + selector: 'ul.wp-block-gallery .blocks-gallery-item', + query: { + url: { + source: 'attribute', + selector: 'img', + attribute: 'src', + }, + alt: { + source: 'attribute', + selector: 'img', + attribute: 'alt', + default: '', + }, + id: { + source: 'attribute', + selector: 'img', + attribute: 'data-id', + }, + link: { + source: 'attribute', + selector: 'img', + attribute: 'data-link', + }, + caption: { + type: 'array', + source: 'children', + selector: 'figcaption', + }, + }, + }, + columns: { + type: 'number', + }, + imageCrop: { + type: 'boolean', + default: true, + }, + linkTo: { + type: 'string', + default: 'none', + }, + }, + isEligible( { images, ids } ) { + return ( + images && + images.length > 0 && + ( ( ! ids && images ) || + ( ids && images && ids.length !== images.length ) || + some( images, ( id, index ) => { + if ( ! id && ids[ index ] !== null ) { + return true; + } + return parseInt( id, 10 ) !== ids[ index ]; + } ) ) + ); + }, + migrate( attributes ) { + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + return { + ...attributes, + ids: map( attributes.images, ( { id } ) => { + if ( ! id ) { + return null; + } + return parseInt( id, 10 ); + } ), + }; + }, + supports: { + align: true, + }, + save( { attributes } ) { + const { + images, + columns = defaultColumnsNumberV1( attributes ), + imageCrop, + linkTo, + } = attributes; + return ( +
    + { images.map( ( image ) => { + let href; + + switch ( linkTo ) { + case 'media': + href = image.url; + break; + case 'attachment': + href = image.link; + break; + } + + const img = ( + { + ); + + return ( +
  • +
    + { href ? { img } : img } + { image.caption && image.caption.length > 0 && ( + + ) } +
    +
  • + ); + } ) } +
+ ); + }, +}; + +const v1 = { + attributes: { + images: { + type: 'array', + default: [], + source: 'query', + selector: 'div.wp-block-gallery figure.blocks-gallery-image img', + query: { + url: { + source: 'attribute', + attribute: 'src', + }, + alt: { + source: 'attribute', + attribute: 'alt', + default: '', + }, + id: { + source: 'attribute', + attribute: 'data-id', + }, + }, + }, + columns: { + type: 'number', + }, + imageCrop: { + type: 'boolean', + default: true, + }, + linkTo: { + type: 'string', + default: 'none', + }, + align: { + type: 'string', + default: 'none', + }, + }, + supports: { + align: true, + }, + save( { attributes } ) { + const { + images, + columns = defaultColumnsNumberV1( attributes ), + align, + imageCrop, + linkTo, + } = attributes; + const className = classnames( `columns-${ columns }`, { + alignnone: align === 'none', + 'is-cropped': imageCrop, + } ); + return ( +
+ { images.map( ( image ) => { + let href; + + switch ( linkTo ) { + case 'media': + href = image.url; + break; + case 'attachment': + href = image.link; + break; + } + + const img = ( + { + ); + + return ( +
+ { href ? { img } : img } +
+ ); + } ) } +
+ ); + }, + migrate( attributes ) { + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + return attributes; }, -]; +}; -export default deprecated; +export default [ v6, v5, v4, v3, v2, v1 ]; diff --git a/packages/block-library/src/gallery/edit-wrapper.js b/packages/block-library/src/gallery/edit-wrapper.js index 0e101dfa85e216..2c81271902d312 100644 --- a/packages/block-library/src/gallery/edit-wrapper.js +++ b/packages/block-library/src/gallery/edit-wrapper.js @@ -1,47 +1,27 @@ /** * WordPress dependencies */ -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { withNotices } from '@wordpress/components'; /** * Internal dependencies */ import EditWithInnerBlocks from './edit'; import EditWithoutInnerBlocks from './v1/edit'; +import { isGalleryV2Enabled } from './shared'; /* * Using a wrapper around the logic to load the edit for v1 of Gallery block * or the refactored version with InnerBlocks. This is to prevent conditional * use of hooks lint errors if adding this logic to the top of the edit component. */ -export default function GalleryEditWrapper( props ) { - const { attributes, clientId } = props; - - const innerBlockImages = useSelect( - ( select ) => { - return select( blockEditorStore ).getBlock( clientId )?.innerBlocks; - }, - [ clientId ] - ); - - const __unstableGalleryWithImageBlocks = useSelect( ( select ) => { - const settings = select( blockEditorStore ).getSettings(); - return settings.__unstableGalleryWithImageBlocks; - }, [] ); - - // This logic is used to infer version information from content with higher - // precedence than the flag. New galleries (and existing empty galleries) will - // honor the flag. - const hasNewVersionContent = !! innerBlockImages?.length; - const hasOldVersionContent = - 0 < attributes?.ids?.length || 0 < attributes?.images?.length; - if ( - hasOldVersionContent || - ( ! hasNewVersionContent && ! __unstableGalleryWithImageBlocks ) - ) { +function GalleryEditWrapper( props ) { + if ( ! isGalleryV2Enabled() ) { return ; } return ; } + +export default compose( [ withNotices ] )( GalleryEditWrapper ); diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index e7cbe537c4922d..492275995ca269 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -154,7 +154,7 @@ function GalleryEdit( props ) { useEffect( () => { newImages?.forEach( ( newImage ) => { updateBlockAttributes( newImage.clientId, { - ...buildImageAttributes( false, newImage.attributes ), + ...buildImageAttributes( newImage.attributes ), id: newImage.id, align: undefined, } ); @@ -186,26 +186,24 @@ function GalleryEdit( props ) { * it already existed in the gallery. If the image is in fact new, we need * to apply the gallery's current settings to the image. * - * @param {Object} existingBlock Existing Image block that still exists after gallery update. - * @param {Object} image Media object for the actual image. - * @return {Object} Attributes to set on the new image block. + * @param {Object} imageAttributes Media object for the actual image. + * @return {Object} Attributes to set on the new image block. */ - function buildImageAttributes( existingBlock, image ) { - if ( existingBlock ) { - return existingBlock.attributes; - } + function buildImageAttributes( imageAttributes ) { + const image = imageAttributes.id + ? find( imageData, { id: imageAttributes.id } ) + : null; let newClassName; - if ( image.className && image.className !== '' ) { - newClassName = image.className; + if ( imageAttributes.className && imageAttributes.className !== '' ) { + newClassName = imageAttributes.className; } else { newClassName = preferredStyle ? `is-style-${ preferredStyle }` : undefined; } - return { - ...pickRelevantMediaFiles( image, sizeSlug ), + ...pickRelevantMediaFiles( imageAttributes, sizeSlug ), ...getHrefAndDestination( image, linkTo ), ...getUpdatedLinkTargetSettings( linkTarget, attributes ), className: newClassName, diff --git a/packages/block-library/src/gallery/save.js b/packages/block-library/src/gallery/save.js index 781b96d2aedbe9..655a1823020fc5 100644 --- a/packages/block-library/src/gallery/save.js +++ b/packages/block-library/src/gallery/save.js @@ -16,9 +16,10 @@ import { * Internal dependencies */ import saveWithoutInnerBlocks from './v1/save'; +import { isGalleryV2Enabled } from './shared'; export default function saveWithInnerBlocks( { attributes } ) { - if ( attributes?.ids?.length > 0 || attributes?.images?.length > 0 ) { + if ( ! isGalleryV2Enabled() ) { return saveWithoutInnerBlocks( { attributes } ); } diff --git a/packages/block-library/src/gallery/shared.js b/packages/block-library/src/gallery/shared.js index 0de4e8586c33ab..e2c2e717d0f707 100644 --- a/packages/block-library/src/gallery/shared.js +++ b/packages/block-library/src/gallery/shared.js @@ -21,3 +21,19 @@ export const pickRelevantMediaFiles = ( image, sizeSlug = 'large' ) => { } return imageProps; }; + +/** + * The new gallery block format is not compatible with the use_BalanceTags option + * in WP versions <= 5.8 https://core.trac.wordpress.org/ticket/54130. The + * window.wp.galleryBlockV2Enabled flag is set in lib/compat.php. This method + * can be removed when minimum supported WP version >=5.9. + */ +export function isGalleryV2Enabled() { + // We want to fail early here, at least during beta testing phase, to ensure + // there aren't instances where undefined values cause false negatives. + if ( ! window.wp || typeof window.wp.galleryBlockV2Enabled !== 'boolean' ) { + throw 'window.wp.galleryBlockV2Enabled is not defined'; + } + + return window.wp.galleryBlockV2Enabled; +} diff --git a/packages/block-library/src/gallery/transforms.js b/packages/block-library/src/gallery/transforms.js index f5fe6b341cd9e6..36106b4bb0664a 100644 --- a/packages/block-library/src/gallery/transforms.js +++ b/packages/block-library/src/gallery/transforms.js @@ -8,8 +8,6 @@ import { filter, every, toString } from 'lodash'; */ import { createBlock } from '@wordpress/blocks'; import { createBlobURL } from '@wordpress/blob'; -import { select } from '@wordpress/data'; -import { store as blockEditorStore } from '@wordpress/block-editor'; import { addFilter } from '@wordpress/hooks'; /** @@ -24,7 +22,7 @@ import { LINK_DESTINATION_ATTACHMENT as DEPRECATED_LINK_DESTINATION_ATTACHMENT, LINK_DESTINATION_MEDIA as DEPRECATED_LINK_DESTINATION_MEDIA, } from './v1/constants'; -import { pickRelevantMediaFiles } from './shared'; +import { pickRelevantMediaFiles, isGalleryV2Enabled } from './shared'; const parseShortcodeIds = ( ids ) => { if ( ! ids ) { @@ -49,9 +47,8 @@ const parseShortcodeIds = ( ids ) => { * @return {Block} The transformed block. */ function updateThirdPartyTransformToGallery( block ) { - const settings = select( blockEditorStore ).getSettings(); if ( - settings.__unstableGalleryWithImageBlocks && + isGalleryV2Enabled() && block.name === 'core/gallery' && block.attributes?.images.length > 0 ) { @@ -145,8 +142,7 @@ const transforms = { const validImages = filter( attributes, ( { url } ) => url ); - const settings = select( blockEditorStore ).getSettings(); - if ( settings.__unstableGalleryWithImageBlocks ) { + if ( isGalleryV2Enabled() ) { const innerBlocks = validImages.map( ( image ) => { return createBlock( 'core/image', image ); } ); @@ -184,10 +180,7 @@ const transforms = { images: { type: 'array', shortcode: ( { named: { ids } } ) => { - const settings = select( - blockEditorStore - ).getSettings(); - if ( ! settings.__unstableGalleryWithImageBlocks ) { + if ( ! isGalleryV2Enabled() ) { return parseShortcodeIds( ids ).map( ( id ) => ( { id: toString( id ), } ) ); @@ -197,10 +190,7 @@ const transforms = { ids: { type: 'array', shortcode: ( { named: { ids } } ) => { - const settings = select( - blockEditorStore - ).getSettings(); - if ( ! settings.__unstableGalleryWithImageBlocks ) { + if ( ! isGalleryV2Enabled() ) { return parseShortcodeIds( ids ); } }, @@ -208,10 +198,7 @@ const transforms = { shortCodeTransforms: { type: 'array', shortcode: ( { named: { ids } } ) => { - const settings = select( - blockEditorStore - ).getSettings(); - if ( settings.__unstableGalleryWithImageBlocks ) { + if ( isGalleryV2Enabled() ) { return parseShortcodeIds( ids ).map( ( id ) => ( { id: parseInt( id ), } ) ); @@ -227,10 +214,7 @@ const transforms = { linkTo: { type: 'string', shortcode: ( { named: { link } } ) => { - const settings = select( - blockEditorStore - ).getSettings(); - if ( ! settings.__unstableGalleryWithImageBlocks ) { + if ( ! isGalleryV2Enabled() ) { switch ( link ) { case 'post': return DEPRECATED_LINK_DESTINATION_ATTACHMENT; @@ -273,8 +257,7 @@ const transforms = { ); }, transform( files ) { - const settings = select( blockEditorStore ).getSettings(); - if ( settings.__unstableGalleryWithImageBlocks ) { + if ( isGalleryV2Enabled() ) { const innerBlocks = files.map( ( file ) => createBlock( 'core/image', { url: createBlobURL( file ), @@ -299,8 +282,7 @@ const transforms = { type: 'block', blocks: [ 'core/image' ], transform: ( { align, images, ids, sizeSlug }, innerBlocks ) => { - const settings = select( blockEditorStore ).getSettings(); - if ( settings.__unstableGalleryWithImageBlocks ) { + if ( isGalleryV2Enabled() ) { if ( innerBlocks.length > 0 ) { return innerBlocks.map( ( { diff --git a/packages/block-library/src/gallery/use-mobile-warning.js b/packages/block-library/src/gallery/use-mobile-warning.js index 6567d8f73349b7..95d0f4c86432fc 100644 --- a/packages/block-library/src/gallery/use-mobile-warning.js +++ b/packages/block-library/src/gallery/use-mobile-warning.js @@ -20,7 +20,7 @@ export default function useMobileWarning( newImages ) { createWarningNotice( __( - 'Editing this Gallery in the WordPress mobile app requires version 18.2 or higher.' + 'If you want to edit the gallery you just added in the mobile app, to avoid losing any data please make sure you use version 18.2 of the app or above.' ), { type: 'snackbar', explicitDismiss: true } ); diff --git a/packages/block-library/src/gallery/v1/edit.js b/packages/block-library/src/gallery/v1/edit.js index cfa1755445d8a0..ca57494142feb9 100644 --- a/packages/block-library/src/gallery/v1/edit.js +++ b/packages/block-library/src/gallery/v1/edit.js @@ -24,10 +24,8 @@ import { ToggleControl, withNotices, RangeControl, - ToolbarButton, } from '@wordpress/components'; import { - BlockControls, MediaPlaceholder, InspectorControls, useBlockProps, @@ -53,7 +51,6 @@ import { LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, } from './constants'; -import UpdateGalleryModal from './update-gallery-modal'; const MAX_COLUMNS = 8; const linkOptions = [ @@ -102,13 +99,10 @@ function GalleryEdit( props ) { mediaUpload, getMedia, wasBlockJustInserted, - __unstableGalleryWithImageBlocks, } = useSelect( ( select ) => { const settings = select( blockEditorStore ).getSettings(); return { - __unstableGalleryWithImageBlocks: - settings.__unstableGalleryWithImageBlocks, imageSizes: settings.imageSizes, mediaUpload: settings.mediaUpload, getMedia: select( coreStore ).getMedia, @@ -414,10 +408,6 @@ function GalleryEdit( props ) { /> ); - const [ isUpdateOpen, setUpdateOpen ] = useState( false ); - const openUpdateModal = () => setUpdateOpen( true ); - const closeUpdateModal = () => setUpdateOpen( false ); - const blockProps = useBlockProps(); if ( ! hasImages ) { @@ -466,24 +456,7 @@ function GalleryEdit( props ) { ) } - { /* TODO: Remove platform condition when native conversion is ready */ } - { Platform.isWeb && __unstableGalleryWithImageBlocks && ( - - - { __( 'Update' ) } - - - ) } - { Platform.isWeb && isUpdateOpen && ( - - ) } + { noticeUI } () => { - let link; - const { - attributes: { sizeSlug, linkTo, images, caption }, - } = getBlock( clientId ); - - switch ( linkTo ) { - case 'post': - link = LINK_DESTINATION_ATTACHMENT; - break; - case 'file': - link = LINK_DESTINATION_MEDIA; - break; - default: - link = LINK_DESTINATION_NONE; - break; - } - const innerBlocks = images.map( ( image ) => - createBlock( 'core/image', { - id: image.id ? parseInt( image.id, 10 ) : null, - url: image.url, - alt: image.alt, - caption: image.caption, - linkDestination: link, - } ) - ); - - replaceBlocks( - clientId, - createBlock( - 'core/gallery', - { sizeSlug, linkTo: link, caption }, - innerBlocks - ) - ); -}; - -export default function UpdateGalleryModal( { onClose, clientId } ) { - const { getBlock } = useSelect( blockEditorStore ); - const { replaceBlocks } = useDispatch( blockEditorStore ); - return ( - -

- { __( - 'Updating to the new format adds the ability to use custom links or styles on individual images in the gallery, and makes it easier to add or move them around.' - ) } -

- -
- - -
-
- ); -} diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index ea9dbf9eeaab0e..7f71d7f68ad278 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -30,6 +30,11 @@ class Editor extends Component { constructor( props ) { super( ...arguments ); + // need to set this globally to avoid race with deprecations + // defaulting to true to avoid issues with a not-yet-cached value + const { galleryWithImageBlocks = true } = props; + window.wp.galleryBlockV2Enabled = galleryWithImageBlocks; + if ( props.initialHtmlModeEnabled && props.mode === 'visual' ) { // enable html mode if the initial mode the parent wants it but we're not already in it this.props.switchEditorMode( 'text' ); diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index 059b98eb459374..ace8b1f01da136 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -75,6 +75,7 @@ class NativeEditorProvider extends Component { this.post.type, this.post ); + this.getEditorSettings = memize( ( settings, capabilities ) => ( { ...settings, @@ -90,16 +91,10 @@ class NativeEditorProvider extends Component { } componentDidMount() { - const { - capabilities, - locale, - updateSettings, - galleryWithImageBlocks, - } = this.props; + const { capabilities, locale, updateSettings } = this.props; updateSettings( { ...capabilities, - ...{ __unstableGalleryWithImageBlocks: galleryWithImageBlocks }, ...this.getThemeColors( this.props ), locale, } ); @@ -149,14 +144,11 @@ class NativeEditorProvider extends Component { ); this.subscriptionParentUpdateEditorSettings = subscribeUpdateEditorSettings( - ( editorSettings ) => { - updateSettings( { - ...{ - __unstableGalleryWithImageBlocks: - editorSettings.galleryWithImageBlocks, - }, - ...this.getThemeColors( editorSettings ), - } ); + ( { galleryWithImageBlocks, ...editorSettings } ) => { + if ( typeof galleryWithImageBlocks === 'boolean' ) { + window.wp.galleryBlockV2Enabled = galleryWithImageBlocks; + } + updateSettings( this.getThemeColors( editorSettings ) ); } ); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index bdbe2402671987..860a68419dcb3a 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -60,6 +60,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String MAP_KEY_THEME_UPDATE_COLORS = "colors"; private static final String MAP_KEY_THEME_UPDATE_GRADIENTS = "gradients"; private static final String MAP_KEY_THEME_UPDATE_RAW_STYLES = "rawStyles"; + private static final String MAP_KEY_GALLERY_WITH_IMAGE_BLOCKS = "galleryWithImageBlocks"; public static final String MAP_KEY_MEDIA_FINAL_SAVE_RESULT_SUCCESS_VALUE = "success"; private static final String MAP_KEY_IS_PREFERRED_COLOR_SCHEME_DARK = "isPreferredColorSchemeDark"; @@ -149,6 +150,13 @@ public void updateTheme(@Nullable Bundle editorTheme) { Serializable gradients = editorTheme.getSerializable(MAP_KEY_THEME_UPDATE_GRADIENTS); Serializable rawStyles = editorTheme.getSerializable(MAP_KEY_THEME_UPDATE_RAW_STYLES); + // We must assign null here to distinguish between a missing value and false + Boolean galleryWithImageBlocks = null; + if (editorTheme.containsKey(MAP_KEY_GALLERY_WITH_IMAGE_BLOCKS)) { + galleryWithImageBlocks = editorTheme.getBoolean(MAP_KEY_GALLERY_WITH_IMAGE_BLOCKS); + } + + if (colors != null) { writableMap.putArray(MAP_KEY_THEME_UPDATE_COLORS, Arguments.fromList((ArrayList)colors)); } @@ -161,6 +169,10 @@ public void updateTheme(@Nullable Bundle editorTheme) { writableMap.putString(MAP_KEY_THEME_UPDATE_RAW_STYLES, rawStyles.toString()); } + if (galleryWithImageBlocks != null) { + writableMap.putBoolean(MAP_KEY_GALLERY_WITH_IMAGE_BLOCKS, galleryWithImageBlocks); + } + emitToJS(EVENT_NAME_UPDATE_EDITOR_SETTINGS, writableMap); } diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index b926c36cb3bf30..318a9796a2e9b0 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -199,7 +199,10 @@ public class Gutenberg: NSObject { private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { var settingsUpdates = [String : Any]() settingsUpdates["isFSETheme"] = editorSettings?.isFSETheme ?? false - settingsUpdates["galleryWithImageBlocks"] = editorSettings?.galleryWithImageBlocks ?? false + + if let galleryWithImageBlocks = editorSettings?.galleryWithImageBlocks { + settingsUpdates["galleryWithImageBlocks"] = galleryWithImageBlocks + } if let rawStyles = editorSettings?.rawStyles { settingsUpdates["rawStyles"] = rawStyles diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap index 2a4ddbf8f5efac..6e3a6b8a84795f 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap @@ -57,8 +57,8 @@ exports[`rawHandler should convert HTML post to blocks with minimal content chan

Shortcode

- - + + diff --git a/test/integration/fixtures/blocks/core__gallery-with-caption.html b/test/integration/fixtures/blocks/core__gallery-with-caption.html index ca644b223d7254..0ec3003092d8b9 100644 --- a/test/integration/fixtures/blocks/core__gallery-with-caption.html +++ b/test/integration/fixtures/blocks/core__gallery-with-caption.html @@ -1,17 +1,26 @@ - -" } ] diff --git a/test/integration/fixtures/blocks/core__gallery-with-caption.parsed.json b/test/integration/fixtures/blocks/core__gallery-with-caption.parsed.json index 8060435a4fdc6f..bb43e7abcd7f82 100644 --- a/test/integration/fixtures/blocks/core__gallery-with-caption.parsed.json +++ b/test/integration/fixtures/blocks/core__gallery-with-caption.parsed.json @@ -2,12 +2,53 @@ { "blockName": "core/gallery", "attrs": { - "ids": [ null, null ] + "linkTo": "none" }, - "innerBlocks": [], - "innerHTML": "\n\n", + "innerBlocks": [ + { + "blockName": "core/image", + "attrs": { + "id": 1421, + "sizeSlug": "large", + "linkDestination": "none", + "inheritedAttributes": { + "linkDestination": true, + "linkTarget": true, + "sizeSlug": true + } + }, + "innerBlocks": [], + "innerHTML": "\n\t
\n\t\t\n\t
\n\t", + "innerContent": [ + "\n\t
\n\t\t\n\t
\n\t" + ] + }, + { + "blockName": "core/image", + "attrs": { + "id": 1440, + "sizeSlug": "large", + "linkDestination": "none", + "inheritedAttributes": { + "linkDestination": true, + "linkTarget": true, + "sizeSlug": true + } + }, + "innerBlocks": [], + "innerHTML": "\n\t
\n\t\t\n\t
\n\t", + "innerContent": [ + "\n\t
\n\t\t\n\t
\n\t" + ] + } + ], + "innerHTML": "\n\n\t\n\n\t\n\t\n\n", "innerContent": [ - "\n\n" + "\n\n\t", + null, + "\n\n\t", + null, + "\n\t\n\n" ] } ] diff --git a/test/integration/fixtures/blocks/core__gallery-with-caption.serialized.html b/test/integration/fixtures/blocks/core__gallery-with-caption.serialized.html index 39a9da57a48fa8..f67c8682af23ea 100644 --- a/test/integration/fixtures/blocks/core__gallery-with-caption.serialized.html +++ b/test/integration/fixtures/blocks/core__gallery-with-caption.serialized.html @@ -1,3 +1,9 @@ - - + + diff --git a/test/integration/fixtures/blocks/core__gallery.html b/test/integration/fixtures/blocks/core__gallery.html index a0f62e68fc5273..3db587c8d72d58 100644 --- a/test/integration/fixtures/blocks/core__gallery.html +++ b/test/integration/fixtures/blocks/core__gallery.html @@ -1,16 +1,25 @@ - -" } ] diff --git a/test/integration/fixtures/blocks/core__gallery.parsed.json b/test/integration/fixtures/blocks/core__gallery.parsed.json index c75387eef15d54..6ab1b02f4ba332 100644 --- a/test/integration/fixtures/blocks/core__gallery.parsed.json +++ b/test/integration/fixtures/blocks/core__gallery.parsed.json @@ -2,12 +2,44 @@ { "blockName": "core/gallery", "attrs": { - "ids": [ null, null ] + "linkTo": "none", + "className": "columns-2" }, - "innerBlocks": [], - "innerHTML": "\n\n", + "innerBlocks": [ + { + "blockName": "core/image", + "attrs": { + "id": 1421, + "sizeSlug": "large", + "linkDestination": "none" + }, + "innerBlocks": [], + "innerHTML": "\n\t
\n\t\t\n\t
\n\t", + "innerContent": [ + "\n\t
\n\t\t\n\t
\n\t" + ] + }, + { + "blockName": "core/image", + "attrs": { + "id": 1440, + "sizeSlug": "large", + "linkDestination": "none" + }, + "innerBlocks": [], + "innerHTML": "\n\t
\n\t\t\n\t
\n\t", + "innerContent": [ + "\n\t
\n\t\t\n\t
\n\t" + ] + } + ], + "innerHTML": "\n\n\t\n\n\t\n\n", "innerContent": [ - "\n\n" + "\n\n\t", + null, + "\n\n\t", + null, + "\n\n" ] } ] diff --git a/test/integration/fixtures/blocks/core__gallery.serialized.html b/test/integration/fixtures/blocks/core__gallery.serialized.html index 62c72702eb57f6..6ab87b7c6031d8 100644 --- a/test/integration/fixtures/blocks/core__gallery.serialized.html +++ b/test/integration/fixtures/blocks/core__gallery.serialized.html @@ -1,3 +1,9 @@ - - + + diff --git a/test/integration/fixtures/blocks/core__gallery__columns.html b/test/integration/fixtures/blocks/core__gallery__columns.html index 6ab9083784937c..b70ac2b6f06a85 100644 --- a/test/integration/fixtures/blocks/core__gallery__columns.html +++ b/test/integration/fixtures/blocks/core__gallery__columns.html @@ -1,16 +1,23 @@ - -