diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index 05be96ef6ec7d6..269878ac6c8a3b 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -62,6 +62,17 @@ function gutenberg_initialize_experiments_settings() {
'id' => 'gutenberg-widgets-in-customizer',
)
);
+ add_settings_field(
+ 'gutenberg-gallery-refactor',
+ __( 'Gallery Refactor', 'gutenberg' ),
+ 'gutenberg_display_experiment_field',
+ 'gutenberg-experiments',
+ 'gutenberg_experiments_section',
+ array(
+ 'label' => __( 'Enable the refactored gallery block', 'gutenberg' ),
+ 'id' => 'gutenberg-gallery-refactor',
+ )
+ );
register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
@@ -99,3 +110,19 @@ function gutenberg_display_experiment_section() {
$experiments_exist ? array_key_exists( 'gutenberg-gallery-refactor', get_option( 'gutenberg-experiments' ) ) : false,
+ );
+ return array_merge( $settings, $experiments_settings );
+}
+add_filter( 'block_editor_settings', 'gutenberg_experiments_editor_settings' );
diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md
index 55653a083dd7c6..b6aedc7f35c398 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -506,6 +506,7 @@ _Properties_
- _\_\_experimentalBlockDirectory_ `boolean`: Whether the user has enabled the Block Directory
- _\_\_experimentalBlockPatterns_ `Array`: Array of objects representing the block patterns
- _\_\_experimentalBlockPatternCategories_ `Array`: Array of objects representing the block pattern categories
+- _\_\_experimentalGalleryRefactor_ `boolean`: Whether the user has enabled the refactored gallery block which uses InnerBlocks
# **SkipToSelectedBlock**
diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js
index 99edaadbd16ec8..0a03a0689f10c1 100644
--- a/packages/block-editor/src/components/media-placeholder/index.js
+++ b/packages/block-editor/src/components/media-placeholder/index.js
@@ -128,7 +128,7 @@ export function MediaPlaceholder( {
const onFilesUpload = ( files ) => {
if ( isGallery ) {
- // Because the Gallery hands the files over to Image component InnerBlocks just
+ // Because the refactored Gallery hands the files over to Image component InnerBlocks just
// hand the handling of the files over to the Gallery
onSelect( files );
return;
@@ -136,7 +136,46 @@ export function MediaPlaceholder( {
onFilesPreUpload( files );
let setMedia;
if ( multiple ) {
- setMedia = onSelect;
+ // This is still needed to handle v1 versions of the Gallery block. It can be removed
+ // once all Gallery instances are forced to migrate.
+ if ( addToGallery ) {
+ // Since the setMedia function runs multiple times per upload group
+ // and is passed newMedia containing every item in its group each time, we must
+ // filter out whatever this upload group had previously returned to the
+ // gallery before adding and returning the image array with replacement newMedia
+ // values.
+
+ // Define an array to store urls from newMedia between subsequent function calls.
+ let lastMediaPassed = [];
+ setMedia = ( newMedia ) => {
+ // Remove any images this upload group is responsible for (lastMediaPassed).
+ // Their replacements are contained in newMedia.
+ const filteredMedia = ( value ?? [] ).filter( ( item ) => {
+ // If Item has id, only remove it if lastMediaPassed has an item with that id.
+ if ( item.id ) {
+ return ! lastMediaPassed.some(
+ // Be sure to convert to number for comparison.
+ ( { id } ) => Number( id ) === Number( item.id )
+ );
+ }
+ // Compare transient images via .includes since gallery may append extra info onto the url.
+ return ! lastMediaPassed.some( ( { urlSlug } ) =>
+ item.url.includes( urlSlug )
+ );
+ } );
+ // Return the filtered media array along with newMedia.
+ onSelect( filteredMedia.concat( newMedia ) );
+ // Reset lastMediaPassed and set it with ids and urls from newMedia.
+ lastMediaPassed = newMedia.map( ( media ) => {
+ // Add everything up to '.fileType' to compare via .includes.
+ const cutOffIndex = media.url.lastIndexOf( '.' );
+ const urlSlug = media.url.slice( 0, cutOffIndex );
+ return { id: media.id, urlSlug };
+ } );
+ };
+ } else {
+ setMedia = onSelect;
+ }
} else {
setMedia = ( [ media ] ) => onSelect( media );
}
diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js
index ba608d34a35f87..a6c3d565c7d398 100644
--- a/packages/block-editor/src/store/defaults.js
+++ b/packages/block-editor/src/store/defaults.js
@@ -28,6 +28,7 @@ export const PREFERENCES_DEFAULTS = {
* @property {boolean} __experimentalBlockDirectory Whether the user has enabled the Block Directory
* @property {Array} __experimentalBlockPatterns Array of objects representing the block patterns
* @property {Array} __experimentalBlockPatternCategories Array of objects representing the block pattern categories
+ * @property {boolean} __experimentalGalleryRefactor Whether the user has enabled the refactored gallery block which uses InnerBlocks
*/
export const SETTINGS_DEFAULTS = {
alignWide: false,
@@ -151,6 +152,7 @@ export const SETTINGS_DEFAULTS = {
__experimentalBlockPatterns: [],
__experimentalBlockPatternCategories: [],
__experimentalSpotlightEntityBlocks: [],
+ __experimentalGalleryRefactor: false,
// gradients setting is not used anymore now defaults are passed from theme.json on the server and core has its own defaults.
// The setting is only kept for backward compatibility purposes.
diff --git a/packages/block-library/src/gallery/block.json b/packages/block-library/src/gallery/block.json
index 79ae042f660069..68a16fde03b550 100644
--- a/packages/block-library/src/gallery/block.json
+++ b/packages/block-library/src/gallery/block.json
@@ -3,6 +3,57 @@
"name": "core/gallery",
"category": "media",
"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": []
+ },
"imageUploads": {
"type": "array",
"default": [],
diff --git a/packages/block-library/src/gallery/deprecated.js b/packages/block-library/src/gallery/deprecated.js
index 41753817dc7837..f97adbdd300245 100644
--- a/packages/block-library/src/gallery/deprecated.js
+++ b/packages/block-library/src/gallery/deprecated.js
@@ -7,20 +7,7 @@ import { map, some } from 'lodash';
/**
* WordPress dependencies
*/
-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';
-
-const DEPRECATED_LINK_DESTINATION_MEDIA = 'file';
-const DEPRECATED_LINK_DESTINATION_ATTACHMENT = 'post';
+import { RichText } from '@wordpress/block-editor';
/**
* Original function to determine default number of columns from a block's
@@ -35,562 +22,411 @@ export function defaultColumnsNumberV1( attributes ) {
return Math.min( 3, attributes.images.length );
}
-/**
- * Original function to determines 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 {
- href: image?.source_url || image?.url, // eslint-disable-line camelcase
- linkDestination: LINK_DESTINATION_MEDIA,
- };
- case DEPRECATED_LINK_DESTINATION_ATTACHMENT:
- return {
- href: image?.link,
- linkDestination: LINK_DESTINATION_ATTACHMENT,
- };
- case LINK_DESTINATION_MEDIA:
- return {
- href: image?.source_url || image?.url, // eslint-disable-line camelcase
- linkDestination: LINK_DESTINATION_MEDIA,
- };
- case LINK_DESTINATION_ATTACHMENT:
- return {
- href: image?.link,
- linkDestination: LINK_DESTINATION_ATTACHMENT,
- };
- case LINK_DESTINATION_NONE:
- return {
- href: undefined,
- linkDestination: LINK_DESTINATION_NONE,
- };
- }
-
- return {};
-}
-
-/**
- * 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 v1 = {
- attributes: {
- images: {
- type: 'array',
- default: [],
- source: 'query',
- selector: 'div.wp-block-gallery figure.blocks-gallery-image img',
- query: {
- url: {
- source: 'attribute',
- attribute: 'src',
+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',
+ },
},
- alt: {
- source: 'attribute',
- attribute: 'alt',
- default: '',
- },
- id: {
- source: 'attribute',
- attribute: 'data-id',
+ },
+ ids: {
+ type: 'array',
+ items: {
+ type: 'number',
},
+ default: [],
},
- },
- 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 }
-
- );
- } ) }
-
- );
- },
- isEligible( { imageCount } ) {
- return ! imageCount;
- },
- migrate( { images, imageCrop, linkTo, sizeSlug, columns, caption } ) {
- const imageBlocks = images.map( ( image ) => {
- return getImageBlock( image, sizeSlug, linkTo );
- } );
- return [
- {
- caption,
- columns,
- imageCrop,
- linkTo,
- sizeSlug,
- imageCount: imageBlocks.length,
- allowResize: false,
- isGrouped: true,
+ columns: {
+ type: 'number',
+ minimum: 1,
+ maximum: 8,
},
- imageBlocks,
- ];
- },
-};
-
-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',
- },
+ caption: {
+ type: 'string',
+ source: 'html',
+ selector: '.blocks-gallery-caption',
+ },
+ imageCrop: {
+ type: 'boolean',
+ default: true,
+ },
+ linkTo: {
+ type: 'string',
+ default: 'none',
+ },
+ sizeSlug: {
+ type: 'string',
+ default: 'large',
},
},
- columns: {
- type: 'number',
- },
- imageCrop: {
- type: 'boolean',
- default: true,
- },
- linkTo: {
- type: 'string',
- default: 'none',
+ 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';
+ }
+ return {
+ ...attributes,
+ linkTo,
+ };
},
- },
- 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 ) {
- 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 = (
-
+
+ { images.map( ( image ) => {
+ let href;
+
+ switch ( linkTo ) {
+ case 'media':
+ href = image.fullUrl || image.url;
+ break;
+ case 'attachment':
+ href = image.link;
+ break;
}
- />
- );
- return (
-
-
- { href ? { img } : img }
- { image.caption && image.caption.length > 0 && (
-
- ) }
-
-
- );
- } ) }
-
- );
+ const img = (
+
+ );
+
+ return (
+
+
+ { 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',
+ {
+ 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',
+ },
},
},
+ 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',
+ },
},
- ids: {
- type: 'array',
- default: [],
- },
- columns: {
- type: 'number',
+ supports: {
+ align: true,
},
- imageCrop: {
- type: 'boolean',
- default: true,
+ isEligible( { ids } ) {
+ return ids && ids.some( ( id ) => typeof id === 'string' );
},
- linkTo: {
- type: 'string',
- default: 'none',
+ migrate( attributes ) {
+ return {
+ ...attributes,
+ ids: map( attributes.ids, ( id ) => {
+ const parsedId = parseInt( id, 10 );
+ return Number.isInteger( parsedId ) ? parsedId : null;
+ } ),
+ };
},
- },
- 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 && (
-
- ) }
-
-
- );
- } ) }
-
- );
- },
- isEligible( { imageCount } ) {
- return ! imageCount;
- },
- migrate( { images, imageCrop, linkTo, sizeSlug, columns, caption } ) {
- const imageBlocks = images.map( ( image ) => {
- return getImageBlock( image, sizeSlug, linkTo );
- } );
-
- return [
- {
- caption,
- columns,
+ save( { attributes } ) {
+ const {
+ images,
+ columns = defaultColumnsNumberV1( attributes ),
imageCrop,
+ caption,
linkTo,
- sizeSlug,
- imageCount: imageBlocks.length,
- allowResize: false,
- isGrouped: true,
- },
- imageBlocks,
- ];
- },
-};
+ } = attributes;
+
+ return (
+
+
+ { images.map( ( image ) => {
+ let href;
+
+ switch ( linkTo ) {
+ case 'media':
+ href = image.fullUrl || image.url;
+ break;
+ case 'attachment':
+ href = image.link;
+ break;
+ }
-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',
+ const img = (
+
+ );
+
+ return (
+
+
+ { href ? (
+ { img }
+ ) : (
+ img
+ ) }
+ { ! RichText.isEmpty(
+ image.caption
+ ) && (
+
+ ) }
+
+
+ );
+ } ) }
+
+ { ! 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',
+ },
+ 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',
+ },
},
- 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,
},
- },
- supports: {
- align: true,
- },
- isEligible( { ids } ) {
- return ids && ids.some( ( id ) => typeof id === 'string' );
- },
- migrate( { images, imageCrop, linkTo, sizeSlug, columns, caption } ) {
- const imageBlocks = images.map( ( image ) => {
- return getImageBlock( image, sizeSlug, linkTo );
- } );
-
- return [
- {
- caption,
- columns,
+ save( { attributes } ) {
+ const {
+ images,
+ columns = defaultColumnsNumberV1( attributes ),
imageCrop,
linkTo,
- sizeSlug,
- imageCount: imageBlocks.length,
- allowResize: false,
- isGrouped: true,
- },
- imageBlocks,
- ];
- },
- save( { attributes } ) {
- const {
- images,
- columns = defaultColumnsNumberV1( attributes ),
- imageCrop,
- caption,
- linkTo,
- } = attributes;
-
- return (
-
-
+ } = attributes;
+ return (
+
{ images.map( ( image ) => {
let href;
@@ -627,159 +463,116 @@ const v4 = {
) : (
img
) }
- { ! RichText.isEmpty( image.caption ) && (
-
- ) }
+ { image.caption &&
+ image.caption.length > 0 && (
+
+ ) }
);
} ) }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
-
- );
+ );
+ },
},
-};
-
-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',
+ {
+ 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',
+ },
},
},
- },
- ids: {
- type: 'array',
- items: {
+ columns: {
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',
+ },
},
- imageCrop: {
- type: 'boolean',
- default: true,
+ 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 ];
+ } ) )
+ );
},
- linkTo: {
- type: 'string',
- default: 'none',
+ migrate( attributes ) {
+ return {
+ ...attributes,
+ ids: map( attributes.images, ( { id } ) => {
+ if ( ! id ) {
+ return null;
+ }
+ return parseInt( id, 10 );
+ } ),
+ };
},
- sizeSlug: {
- type: 'string',
- default: 'large',
+ supports: {
+ align: true,
},
- },
- supports: {
- align: true,
- },
- isEligible( { linkTo, imageCount } ) {
- return (
- ! imageCount &&
- ( ! linkTo || linkTo === 'attachment' || linkTo === 'media' )
- );
- },
- migrate( attributes ) {
- let linkTo = attributes.linkTo;
- if ( ! attributes.linkTo ) {
- linkTo = 'none';
- }
- const imageBlocks = attributes.images.map( ( image ) => {
- return getImageBlock( image, attributes.sizeSlug, linkTo );
- } );
- return [
- {
- caption: attributes.caption,
- columns: attributes.columns,
- imageCrop: attributes.imageCrop,
+ save( { attributes } ) {
+ const {
+ images,
+ columns = defaultColumnsNumberV1( attributes ),
+ imageCrop,
linkTo,
- sizeSlug: attributes.sizeSlug,
- imageCount: imageBlocks.length,
- allowResize: false,
- isGrouped: true,
- },
- imageBlocks,
- ];
- },
- save( { attributes } ) {
- const {
- images,
- columns = defaultColumnsNumberV1( attributes ),
- imageCrop,
- caption,
- linkTo,
- } = attributes;
-
- return (
-
-
+ } = attributes;
+ return (
+
{ images.map( ( image ) => {
let href;
switch ( linkTo ) {
case 'media':
- href = image.fullUrl || image.url;
+ href = image.url;
break;
case 'attachment':
href = image.link;
@@ -791,7 +584,6 @@ const v5 = {
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
@@ -810,132 +602,86 @@ const v5 = {
) : (
img
) }
- { ! RichText.isEmpty( image.caption ) && (
-
- ) }
+ { image.caption &&
+ image.caption.length > 0 && (
+
+ ) }
);
} ) }
- { ! RichText.isEmpty( caption ) && (
-
- ) }
-
- );
+ );
+ },
},
-};
-
-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',
+ {
+ 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',
+ },
},
},
- },
- ids: {
- type: 'array',
- items: {
+ columns: {
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',
+ imageCrop: {
+ type: 'boolean',
+ default: true,
+ },
+ linkTo: {
+ type: 'string',
+ default: 'none',
+ },
+ align: {
+ type: 'string',
+ default: 'none',
+ },
},
- sizeSlug: {
- type: 'string',
- default: 'large',
+ supports: {
+ align: true,
},
- },
- supports: {
- anchor: true,
- align: true,
- },
- save( { attributes } ) {
- const {
- images,
- columns = defaultColumnsNumberV1( attributes ),
- imageCrop,
- caption,
- linkTo,
- } = attributes;
- const className = `columns-${ columns } ${
- imageCrop ? 'is-cropped' : ''
- }`;
-
- return (
-
-
+ 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 DEPRECATED_LINK_DESTINATION_MEDIA:
- href = image.fullUrl || image.url;
+ case 'media':
+ href = image.url;
break;
- case DEPRECATED_LINK_DESTINATION_ATTACHMENT:
+ case 'attachment':
href = image.link;
break;
}
@@ -945,73 +691,22 @@ const v6 = {
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
- ) }
- { ! RichText.isEmpty( image.caption ) && (
-
- ) }
-
-
+ { href ?
{ img } : img }
+
);
} ) }
-
- { ! RichText.isEmpty( caption ) && (
-
- ) }
-
- );
- },
- isEligible( { imageCount } ) {
- return ! imageCount;
- },
- migrate( { images, imageCrop, linkTo, sizeSlug, columns, caption } ) {
- if ( linkTo === 'post' ) {
- linkTo = 'attachment';
- } else if ( linkTo === 'file' ) {
- linkTo = 'media';
- }
- const imageBlocks = images.map( ( image ) => {
- return getImageBlock( image, sizeSlug, linkTo );
- } );
- return [
- {
- caption,
- columns,
- imageCrop,
- linkTo,
- sizeSlug,
- imageCount: imageBlocks.length,
- allowResize: false,
- isGrouped: true,
- },
- imageBlocks,
- ];
+
+ );
+ },
},
-};
+];
-export default [ v6, v5, v4, v3, v2, v1 ];
+export default deprecated;
diff --git a/packages/block-library/src/gallery/edit-wrapper.js b/packages/block-library/src/gallery/edit-wrapper.js
new file mode 100644
index 00000000000000..b29b50eeb1e97d
--- /dev/null
+++ b/packages/block-library/src/gallery/edit-wrapper.js
@@ -0,0 +1,35 @@
+/**
+ * WordPress dependencies
+ */
+import { store as blockEditorStore } from '@wordpress/block-editor';
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import GalleryEdit from './edit';
+import GalleryEditV1 from './v1/edit';
+
+/*
+ * 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 } = props;
+
+ const __experimentalGalleryRefactor = useSelect( ( select ) => {
+ const settings = select( blockEditorStore ).getSettings();
+ return settings.__experimentalGalleryRefactor;
+ }, [] );
+
+ if (
+ ! __experimentalGalleryRefactor ||
+ attributes?.ids?.length > 0 ||
+ attributes?.images?.length > 0
+ ) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js
index 5c3ba33400229c..1d67431b659784 100644
--- a/packages/block-library/src/gallery/edit.js
+++ b/packages/block-library/src/gallery/edit.js
@@ -18,11 +18,13 @@ import {
Spinner,
} from '@wordpress/components';
import {
+ store as blockEditorStore,
MediaPlaceholder,
InspectorControls,
useBlockProps,
store as blockEditorStore,
} from '@wordpress/block-editor';
+import { store as coreStore } from '@wordpress/core-data';
import { Platform, useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
@@ -98,23 +100,22 @@ function GalleryEdit( props ) {
const {
__unstableMarkNextChangeAsNotPersistent,
replaceInnerBlocks,
- } = useDispatch( 'core/block-editor' );
+ } = useDispatch( blockEditorStore );
const { getSettings, preferredStyle } = useSelect( ( select ) => {
- const settings = select( 'core/block-editor' ).getSettings();
+ const settings = select( blockEditorStore ).getSettings();
const preferredStyleVariations =
settings.__experimentalPreferredStyleVariations;
return {
- getBlock: select( 'core/block-editor' ).getBlock,
- getSettings: select( 'core/block-editor' ).getSettings,
+ getBlock: select( blockEditorStore ).getBlock,
+ getSettings: select( blockEditorStore ).getSettings,
preferredStyle: preferredStyleVariations?.value?.[ 'core/image' ],
};
}, [] );
const innerBlockImages = useSelect(
( select ) => {
- return select( 'core/block-editor' ).getBlock( clientId )
- ?.innerBlocks;
+ return select( blockEditorStore ).getBlock( clientId )?.innerBlocks;
},
[ clientId ]
);
@@ -140,7 +141,7 @@ function GalleryEdit( props ) {
) {
return imageData;
}
- const getMedia = select( 'core' ).getMedia;
+ const getMedia = select( coreStore ).getMedia;
const newImageData = innerBlockImages.map( ( imageBlock ) => {
return {
id: imageBlock.attributes.id,
diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss
index 3f23d81f2656df..65b9e86692de8d 100644
--- a/packages/block-library/src/gallery/editor.scss
+++ b/packages/block-library/src/gallery/editor.scss
@@ -5,11 +5,13 @@ figure.wp-block-gallery {
display: block;
margin: 0;
-
- .components-drop-zone {
- display: none;
- pointer-events: none;
+ &.has-nested-images {
+ .components-drop-zone {
+ display: none;
+ pointer-events: none;
+ }
}
+
> .blocks-gallery-caption {
flex: 0 0 100%;
}
@@ -68,3 +70,104 @@ figure.wp-block-gallery {
margin: 0 8px 0 4px;
}
}
+
+/**
+ * Deprecated css past this point. This can be removed once all galleries are migrated
+ * to V2.
+ */
+.blocks-gallery-item {
+ // Hide the focus outline that otherwise briefly appears when selecting a block.
+ figure:not(.is-selected):focus,
+ img:focus {
+ outline: none;
+ }
+
+ figure.is-selected {
+
+ &::before {
+ box-shadow: 0 0 0 $border-width $white inset, 0 0 0 3px var(--wp-admin-theme-color) inset;
+ content: "";
+ // Shown in Windows 10 high contrast mode.
+ outline: 2px solid transparent;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ }
+ }
+
+ figure.is-transient img {
+ opacity: 0.3;
+ }
+
+ .is-selected .block-library-gallery-item__inline-menu {
+ display: inline-flex;
+ }
+
+ .block-editor-media-placeholder {
+ margin: 0;
+ height: 100%;
+
+ .components-placeholder__label {
+ display: flex;
+ }
+ }
+}
+
+.block-library-gallery-item__inline-menu {
+ display: none;
+ position: absolute;
+ top: -2px;
+ margin: $grid-unit-10;
+ z-index: z-index(".block-library-gallery-item__inline-menu");
+ transition: box-shadow 0.2s ease-out;
+ @include reduce-motion("transition");
+ border-radius: $radius-block-ui;
+ background: $white;
+ border: $border-width solid $gray-900;
+
+ &:hover {
+ box-shadow: $shadow-popover;
+ }
+
+ @include break-small() {
+ // Use smaller buttons to fit when there are many columns.
+ .columns-7 &,
+ .columns-8 & {
+ padding: $grid-unit-05 / 2;
+ }
+ }
+
+ .components-button.has-icon {
+ &:not(:focus) {
+ border: none;
+ box-shadow: none;
+ }
+
+ @include break-small() {
+ // Use smaller buttons to fit when there are many columns.
+ .columns-7 &,
+ .columns-8 & {
+ padding: 0;
+ width: inherit;
+ height: inherit;
+ }
+ }
+ }
+
+ &.is-left {
+ left: -2px;
+ }
+
+ &.is-right {
+ right: -2px;
+ }
+}
+
+.wp-block-gallery ul.blocks-gallery-grid {
+ padding: 0;
+ // Some themes give all default margin instead of padding.
+ margin: 0;
+}
\ No newline at end of file
diff --git a/packages/block-library/src/gallery/index.js b/packages/block-library/src/gallery/index.js
index 4e6431c19dbc03..224688ddfdd681 100644
--- a/packages/block-library/src/gallery/index.js
+++ b/packages/block-library/src/gallery/index.js
@@ -8,7 +8,7 @@ import { gallery as icon } from '@wordpress/icons';
* Internal dependencies
*/
import deprecated from './deprecated';
-import edit from './edit';
+import edit from './edit-wrapper';
import metadata from './block.json';
import save from './save';
import transforms from './transforms';
diff --git a/packages/block-library/src/gallery/save.js b/packages/block-library/src/gallery/save.js
index dfb21461e31dc8..242dcabf986ac2 100644
--- a/packages/block-library/src/gallery/save.js
+++ b/packages/block-library/src/gallery/save.js
@@ -7,8 +7,12 @@ import { RichText, useBlockProps, InnerBlocks } from '@wordpress/block-editor';
* Internal dependencies
*/
import { defaultColumnsNumber } from './shared';
+import saveV1 from './v1/save';
export default function save( { attributes } ) {
+ if ( attributes?.ids?.length > 0 || attributes?.images?.length > 0 ) {
+ return saveV1( { attributes } );
+ }
const {
imageCount,
caption,
diff --git a/packages/block-library/src/gallery/v1/edit.js b/packages/block-library/src/gallery/v1/edit.js
new file mode 100644
index 00000000000000..15563f51795d79
--- /dev/null
+++ b/packages/block-library/src/gallery/v1/edit.js
@@ -0,0 +1,470 @@
+/**
+ * External dependencies
+ */
+import {
+ every,
+ filter,
+ find,
+ forEach,
+ get,
+ isEmpty,
+ map,
+ reduce,
+ some,
+ toString,
+} from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { compose } from '@wordpress/compose';
+import {
+ PanelBody,
+ SelectControl,
+ ToggleControl,
+ withNotices,
+ RangeControl,
+} from '@wordpress/components';
+import {
+ store as blockEditorStore,
+ MediaPlaceholder,
+ InspectorControls,
+ useBlockProps,
+} from '@wordpress/block-editor';
+import { Platform, useEffect, useState, useMemo } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
+import { useDispatch, withSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { withViewportMatch } from '@wordpress/viewport';
+import { View } from '@wordpress/primitives';
+
+/**
+ * Internal dependencies
+ */
+import { sharedIcon } from '../shared-icon';
+import { pickRelevantMediaFiles } from '../shared';
+import { defaultColumnsNumberV1 } from '../deprecated';
+import Gallery from './gallery';
+import {
+ LINK_DESTINATION_ATTACHMENT,
+ LINK_DESTINATION_MEDIA,
+ LINK_DESTINATION_NONE,
+} from '../constants';
+
+const MAX_COLUMNS = 8;
+const linkOptions = [
+ { value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) },
+ { value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) },
+ { value: LINK_DESTINATION_NONE, label: __( 'None' ) },
+];
+const ALLOWED_MEDIA_TYPES = [ 'image' ];
+
+const PLACEHOLDER_TEXT = Platform.select( {
+ web: __(
+ 'Drag images, upload new ones or select files from your library.'
+ ),
+ native: __( 'ADD MEDIA' ),
+} );
+
+const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.select( {
+ web: {},
+ native: { type: 'stepper' },
+} );
+
+export function GalleryEditV1( props ) {
+ const {
+ attributes,
+ isSelected,
+ noticeUI,
+ noticeOperations,
+ mediaUpload,
+ imageSizes,
+ resizedImages,
+ onFocus,
+ } = props;
+ const {
+ columns = defaultColumnsNumberV1( attributes ),
+ imageCrop,
+ images,
+ linkTo,
+ sizeSlug,
+ } = attributes;
+ const [ selectedImage, setSelectedImage ] = useState();
+ const [ attachmentCaptions, setAttachmentCaptions ] = useState();
+ const { __unstableMarkNextChangeAsNotPersistent } = useDispatch(
+ blockEditorStore
+ );
+
+ function setAttributes( newAttrs ) {
+ if ( newAttrs.ids ) {
+ throw new Error(
+ 'The "ids" attribute should not be changed directly. It is managed automatically when "images" attribute changes'
+ );
+ }
+
+ if ( newAttrs.images ) {
+ newAttrs = {
+ ...newAttrs,
+ // Unlike images[ n ].id which is a string, always ensure the
+ // ids array contains numbers as per its attribute type.
+ ids: map( newAttrs.images, ( { id } ) => parseInt( id, 10 ) ),
+ };
+ }
+
+ props.setAttributes( newAttrs );
+ }
+
+ function onSelectImage( index ) {
+ return () => {
+ setSelectedImage( index );
+ };
+ }
+
+ function onDeselectImage() {
+ return () => {
+ setSelectedImage();
+ };
+ }
+
+ function onMove( oldIndex, newIndex ) {
+ const newImages = [ ...images ];
+ newImages.splice( newIndex, 1, images[ oldIndex ] );
+ newImages.splice( oldIndex, 1, images[ newIndex ] );
+ setSelectedImage( newIndex );
+ setAttributes( { images: newImages } );
+ }
+
+ function onMoveForward( oldIndex ) {
+ return () => {
+ if ( oldIndex === images.length - 1 ) {
+ return;
+ }
+ onMove( oldIndex, oldIndex + 1 );
+ };
+ }
+
+ function onMoveBackward( oldIndex ) {
+ return () => {
+ if ( oldIndex === 0 ) {
+ return;
+ }
+ onMove( oldIndex, oldIndex - 1 );
+ };
+ }
+
+ function onRemoveImage( index ) {
+ return () => {
+ const newImages = filter( images, ( img, i ) => index !== i );
+ setSelectedImage();
+ setAttributes( {
+ images: newImages,
+ columns: attributes.columns
+ ? Math.min( newImages.length, attributes.columns )
+ : attributes.columns,
+ } );
+ };
+ }
+
+ function selectCaption( newImage ) {
+ // The image id in both the images and attachmentCaptions arrays is a
+ // string, so ensure comparison works correctly by converting the
+ // newImage.id to a string.
+ const newImageId = toString( newImage.id );
+ const currentImage = find( images, { id: newImageId } );
+ const currentImageCaption = currentImage
+ ? currentImage.caption
+ : newImage.caption;
+
+ if ( ! attachmentCaptions ) {
+ return currentImageCaption;
+ }
+
+ const attachment = find( attachmentCaptions, {
+ id: newImageId,
+ } );
+
+ // if the attachment caption is updated
+ if ( attachment && attachment.caption !== newImage.caption ) {
+ return newImage.caption;
+ }
+
+ return currentImageCaption;
+ }
+
+ function onSelectImages( newImages ) {
+ setAttachmentCaptions(
+ newImages.map( ( newImage ) => ( {
+ // Store the attachmentCaption id as a string for consistency
+ // with the type of the id in the images attribute.
+ id: toString( newImage.id ),
+ caption: newImage.caption,
+ } ) )
+ );
+ setAttributes( {
+ images: newImages.map( ( newImage ) => ( {
+ ...pickRelevantMediaFiles( newImage, sizeSlug ),
+ caption: selectCaption( newImage, images, attachmentCaptions ),
+ // The id value is stored in a data attribute, so when the
+ // block is parsed it's converted to a string. Converting
+ // to a string here ensures it's type is consistent.
+ id: toString( newImage.id ),
+ } ) ),
+ columns: attributes.columns
+ ? Math.min( newImages.length, attributes.columns )
+ : attributes.columns,
+ } );
+ }
+
+ function onUploadError( message ) {
+ noticeOperations.removeAllNotices();
+ noticeOperations.createErrorNotice( message );
+ }
+
+ function setLinkTo( value ) {
+ setAttributes( { linkTo: value } );
+ }
+
+ function setColumnsNumber( value ) {
+ setAttributes( { columns: value } );
+ }
+
+ function toggleImageCrop() {
+ setAttributes( { imageCrop: ! imageCrop } );
+ }
+
+ function getImageCropHelp( checked ) {
+ return checked
+ ? __( 'Thumbnails are cropped to align.' )
+ : __( 'Thumbnails are not cropped.' );
+ }
+
+ function onFocusGalleryCaption() {
+ setSelectedImage();
+ }
+
+ function setImageAttributes( index, newAttributes ) {
+ if ( ! images[ index ] ) {
+ return;
+ }
+
+ setAttributes( {
+ images: [
+ ...images.slice( 0, index ),
+ {
+ ...images[ index ],
+ ...newAttributes,
+ },
+ ...images.slice( index + 1 ),
+ ],
+ } );
+ }
+
+ function getImagesSizeOptions() {
+ return map(
+ filter( imageSizes, ( { slug } ) =>
+ some( resizedImages, ( sizes ) => sizes[ slug ] )
+ ),
+ ( { name, slug } ) => ( { value: slug, label: name } )
+ );
+ }
+
+ function updateImagesSize( newSizeSlug ) {
+ const updatedImages = map( images, ( image ) => {
+ if ( ! image.id ) {
+ return image;
+ }
+ const url = get( resizedImages, [
+ parseInt( image.id, 10 ),
+ newSizeSlug,
+ ] );
+ return {
+ ...image,
+ ...( url && { url } ),
+ };
+ } );
+
+ setAttributes( { images: updatedImages, sizeSlug: newSizeSlug } );
+ }
+
+ useEffect( () => {
+ if (
+ Platform.OS === 'web' &&
+ images &&
+ images.length > 0 &&
+ every( images, ( { url } ) => isBlobURL( url ) )
+ ) {
+ const filesList = map( images, ( { url } ) => getBlobByURL( url ) );
+ forEach( images, ( { url } ) => revokeBlobURL( url ) );
+ mediaUpload( {
+ filesList,
+ onFileChange: onSelectImages,
+ allowedTypes: [ 'image' ],
+ } );
+ }
+ }, [] );
+
+ useEffect( () => {
+ // Deselect images when deselecting the block
+ if ( ! isSelected ) {
+ setSelectedImage();
+ }
+ }, [ isSelected ] );
+
+ useEffect( () => {
+ // linkTo attribute must be saved so blocks don't break when changing
+ // image_default_link_type in options.php
+ if ( ! linkTo ) {
+ __unstableMarkNextChangeAsNotPersistent();
+ setAttributes( {
+ linkTo:
+ window?.wp?.media?.view?.settings?.defaultProps?.link ||
+ LINK_DESTINATION_NONE,
+ } );
+ }
+ }, [ linkTo ] );
+
+ const hasImages = !! images.length;
+
+ const mediaPlaceholder = (
+
+ );
+
+ const blockProps = useBlockProps();
+
+ if ( ! hasImages ) {
+ return { mediaPlaceholder } ;
+ }
+
+ const imageSizeOptions = getImagesSizeOptions();
+ const shouldShowSizeOptions = hasImages && ! isEmpty( imageSizeOptions );
+
+ return (
+ <>
+
+
+ { images.length > 1 && (
+
+ ) }
+
+
+ { shouldShowSizeOptions && (
+
+ ) }
+
+
+ { noticeUI }
+
+ >
+ );
+}
+
+export default compose( [
+ withSelect( ( select, { attributes: { ids }, isSelected } ) => {
+ const { getMedia } = select( coreStore );
+ const { getSettings } = select( blockEditorStore );
+ const { imageSizes, mediaUpload } = getSettings();
+
+ const resizedImages = useMemo( () => {
+ if ( isSelected ) {
+ return reduce(
+ ids,
+ ( currentResizedImages, id ) => {
+ if ( ! id ) {
+ return currentResizedImages;
+ }
+ const image = getMedia( id );
+ const sizes = reduce(
+ imageSizes,
+ ( currentSizes, size ) => {
+ const defaultUrl = get( image, [
+ 'sizes',
+ size.slug,
+ 'url',
+ ] );
+ const mediaDetailsUrl = get( image, [
+ 'media_details',
+ 'sizes',
+ size.slug,
+ 'source_url',
+ ] );
+ return {
+ ...currentSizes,
+ [ size.slug ]:
+ defaultUrl || mediaDetailsUrl,
+ };
+ },
+ {}
+ );
+ return {
+ ...currentResizedImages,
+ [ parseInt( id, 10 ) ]: sizes,
+ };
+ },
+ {}
+ );
+ }
+ return {};
+ }, [ isSelected, ids, imageSizes ] );
+
+ return {
+ imageSizes,
+ mediaUpload,
+ resizedImages,
+ };
+ } ),
+ withNotices,
+ withViewportMatch( { isNarrow: '< small' } ),
+] )( GalleryEditV1 );
diff --git a/packages/block-library/src/gallery/v1/gallery-image.js b/packages/block-library/src/gallery/v1/gallery-image.js
new file mode 100644
index 00000000000000..b358d845b3599a
--- /dev/null
+++ b/packages/block-library/src/gallery/v1/gallery-image.js
@@ -0,0 +1,313 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { get, omit } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { Button, Spinner, ButtonGroup } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { BACKSPACE, DELETE } from '@wordpress/keycodes';
+import { withSelect, withDispatch } from '@wordpress/data';
+import { RichText, MediaPlaceholder } from '@wordpress/block-editor';
+import { isBlobURL } from '@wordpress/blob';
+import { compose } from '@wordpress/compose';
+import {
+ closeSmall,
+ chevronLeft,
+ chevronRight,
+ edit,
+ image as imageIcon,
+} from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { pickRelevantMediaFiles } from '../shared';
+import {
+ LINK_DESTINATION_ATTACHMENT,
+ LINK_DESTINATION_MEDIA,
+} from '../constants';
+
+const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
+
+class GalleryImage extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.onSelectImage = this.onSelectImage.bind( this );
+ this.onSelectCaption = this.onSelectCaption.bind( this );
+ this.onRemoveImage = this.onRemoveImage.bind( this );
+ this.bindContainer = this.bindContainer.bind( this );
+ this.onEdit = this.onEdit.bind( this );
+ this.onSelectImageFromLibrary = this.onSelectImageFromLibrary.bind(
+ this
+ );
+ this.onSelectCustomURL = this.onSelectCustomURL.bind( this );
+ this.state = {
+ captionSelected: false,
+ isEditing: false,
+ };
+ }
+
+ bindContainer( ref ) {
+ this.container = ref;
+ }
+
+ onSelectCaption() {
+ if ( ! this.state.captionSelected ) {
+ this.setState( {
+ captionSelected: true,
+ } );
+ }
+
+ if ( ! this.props.isSelected ) {
+ this.props.onSelect();
+ }
+ }
+
+ onSelectImage() {
+ if ( ! this.props.isSelected ) {
+ this.props.onSelect();
+ }
+
+ if ( this.state.captionSelected ) {
+ this.setState( {
+ captionSelected: false,
+ } );
+ }
+ }
+
+ onRemoveImage( event ) {
+ if (
+ this.container === this.container.ownerDocument.activeElement &&
+ this.props.isSelected &&
+ [ BACKSPACE, DELETE ].indexOf( event.keyCode ) !== -1
+ ) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.props.onRemove();
+ }
+ }
+
+ onEdit() {
+ this.setState( {
+ isEditing: true,
+ } );
+ }
+
+ componentDidUpdate( prevProps ) {
+ const {
+ isSelected,
+ image,
+ url,
+ __unstableMarkNextChangeAsNotPersistent,
+ } = this.props;
+ if ( image && ! url ) {
+ __unstableMarkNextChangeAsNotPersistent();
+ this.props.setAttributes( {
+ url: image.source_url,
+ alt: image.alt_text,
+ } );
+ }
+
+ // unselect the caption so when the user selects other image and comeback
+ // the caption is not immediately selected
+ if (
+ this.state.captionSelected &&
+ ! isSelected &&
+ prevProps.isSelected
+ ) {
+ this.setState( {
+ captionSelected: false,
+ } );
+ }
+ }
+
+ deselectOnBlur() {
+ this.props.onDeselect();
+ }
+
+ onSelectImageFromLibrary( media ) {
+ const { setAttributes, id, url, alt, caption, sizeSlug } = this.props;
+ if ( ! media || ! media.url ) {
+ return;
+ }
+
+ let mediaAttributes = pickRelevantMediaFiles( media, sizeSlug );
+
+ // If the current image is temporary but an alt text was meanwhile
+ // written by the user, make sure the text is not overwritten.
+ if ( isTemporaryImage( id, url ) ) {
+ if ( alt ) {
+ mediaAttributes = omit( mediaAttributes, [ 'alt' ] );
+ }
+ }
+
+ // If a caption text was meanwhile written by the user,
+ // make sure the text is not overwritten by empty captions.
+ if ( caption && ! get( mediaAttributes, [ 'caption' ] ) ) {
+ mediaAttributes = omit( mediaAttributes, [ 'caption' ] );
+ }
+
+ setAttributes( mediaAttributes );
+ this.setState( {
+ isEditing: false,
+ } );
+ }
+
+ onSelectCustomURL( newURL ) {
+ const { setAttributes, url } = this.props;
+ if ( newURL !== url ) {
+ setAttributes( {
+ url: newURL,
+ id: undefined,
+ } );
+ this.setState( {
+ isEditing: false,
+ } );
+ }
+ }
+
+ render() {
+ const {
+ url,
+ alt,
+ id,
+ linkTo,
+ link,
+ isFirstItem,
+ isLastItem,
+ isSelected,
+ caption,
+ onRemove,
+ onMoveForward,
+ onMoveBackward,
+ setAttributes,
+ 'aria-label': ariaLabel,
+ } = this.props;
+ const { isEditing } = this.state;
+
+ let href;
+
+ switch ( linkTo ) {
+ case LINK_DESTINATION_MEDIA:
+ href = url;
+ break;
+ case LINK_DESTINATION_ATTACHMENT:
+ href = link;
+ break;
+ }
+
+ const img = (
+ // Disable reason: Image itself is not meant to be interactive, but should
+ // direct image selection and unfocus caption fields.
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+ <>
+
+ { isBlobURL( url ) && }
+ >
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */
+ );
+
+ const className = classnames( {
+ 'is-selected': isSelected,
+ 'is-transient': isBlobURL( url ),
+ } );
+
+ return (
+
+ { ! isEditing && ( href ? { img } : img ) }
+ { isEditing && (
+
+ ) }
+
+
+
+
+
+
+
+
+ { ! isEditing && ( isSelected || caption ) && (
+
+ setAttributes( { caption: newCaption } )
+ }
+ unstableOnFocus={ this.onSelectCaption }
+ inlineToolbar
+ />
+ ) }
+
+ );
+ }
+}
+
+export default compose( [
+ withSelect( ( select, ownProps ) => {
+ const { getMedia } = select( 'core' );
+ const { id } = ownProps;
+
+ return {
+ image: id ? getMedia( parseInt( id, 10 ) ) : null,
+ };
+ } ),
+ withDispatch( ( dispatch ) => {
+ const { __unstableMarkNextChangeAsNotPersistent } = dispatch(
+ 'core/block-editor'
+ );
+ return {
+ __unstableMarkNextChangeAsNotPersistent,
+ };
+ } ),
+] )( GalleryImage );
diff --git a/packages/block-library/src/gallery/v1/gallery.js b/packages/block-library/src/gallery/v1/gallery.js
new file mode 100644
index 00000000000000..ee385651d89364
--- /dev/null
+++ b/packages/block-library/src/gallery/v1/gallery.js
@@ -0,0 +1,121 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { RichText } from '@wordpress/block-editor';
+import { VisuallyHidden } from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+import { createBlock } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import GalleryImage from './gallery-image';
+import { defaultColumnsNumberV1 } from '../deprecated';
+
+export const Gallery = ( props ) => {
+ const {
+ attributes,
+ isSelected,
+ setAttributes,
+ selectedImage,
+ mediaPlaceholder,
+ onMoveBackward,
+ onMoveForward,
+ onRemoveImage,
+ onSelectImage,
+ onDeselectImage,
+ onSetImageAttributes,
+ onFocusGalleryCaption,
+ insertBlocksAfter,
+ blockProps,
+ } = props;
+
+ const {
+ align,
+ columns = defaultColumnsNumberV1( attributes ),
+ caption,
+ imageCrop,
+ images,
+ } = attributes;
+
+ return (
+
+
+ { images.map( ( img, index ) => {
+ const ariaLabel = sprintf(
+ /* translators: 1: the order number of the image. 2: the total number of images. */
+ __( 'image %1$d of %2$d in gallery' ),
+ index + 1,
+ images.length
+ );
+
+ return (
+
+
+ onSetImageAttributes( index, attrs )
+ }
+ caption={ img.caption }
+ aria-label={ ariaLabel }
+ sizeSlug={ attributes.sizeSlug }
+ />
+
+ );
+ } ) }
+
+ { mediaPlaceholder }
+ setAttributes( { caption: value } ) }
+ inlineToolbar
+ __unstableOnSplitAtEnd={ () =>
+ insertBlocksAfter( createBlock( 'core/paragraph' ) )
+ }
+ />
+
+ );
+};
+
+function RichTextVisibilityHelper( { isHidden, ...richTextProps } ) {
+ return isHidden ? (
+
+ ) : (
+
+ );
+}
+
+export default Gallery;
diff --git a/packages/block-library/src/gallery/v1/save.js b/packages/block-library/src/gallery/v1/save.js
new file mode 100644
index 00000000000000..a053f5219be0d6
--- /dev/null
+++ b/packages/block-library/src/gallery/v1/save.js
@@ -0,0 +1,81 @@
+/**
+ * WordPress dependencies
+ */
+import { RichText, useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { defaultColumnsNumberV1 } from '../deprecated';
+import {
+ LINK_DESTINATION_ATTACHMENT,
+ LINK_DESTINATION_MEDIA,
+} from '../constants';
+
+export default function saveV1( { attributes } ) {
+ const {
+ images,
+ columns = defaultColumnsNumberV1( attributes ),
+ imageCrop,
+ caption,
+ linkTo,
+ } = attributes;
+ const className = `columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }`;
+
+ return (
+
+
+ { images.map( ( image ) => {
+ let href;
+
+ switch ( linkTo ) {
+ case LINK_DESTINATION_MEDIA:
+ href = image.fullUrl || image.url;
+ break;
+ case LINK_DESTINATION_ATTACHMENT:
+ href = image.link;
+ break;
+ }
+
+ const img = (
+
+ );
+
+ return (
+
+
+ { href ? { img } : img }
+ { ! RichText.isEmpty( image.caption ) && (
+
+ ) }
+
+
+ );
+ } ) }
+
+ { ! RichText.isEmpty( caption ) && (
+
+ ) }
+
+ );
+}
diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js
index 67546ff6710cd3..2be4a5a817c77a 100644
--- a/packages/editor/src/components/provider/use-block-editor-settings.js
+++ b/packages/editor/src/components/provider/use-block-editor-settings.js
@@ -158,6 +158,7 @@ function useBlockEditorSettings( settings, hasTemplate ) {
'__experimentalGlobalStylesUserEntityId',
'__experimentalPreferredStyleVariations',
'__experimentalSetIsInserterOpened',
+ '__experimentalGalleryRefactor',
'alignWide',
'allowedBlockTypes',
'availableLegacyWidgets',