diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index 50f7632e5221e2..d2fec837602d7b 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 31c2dde299bb94..f3df2bfb6d7379 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -502,6 +502,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 c602c4ca8fea72..a75520476b9eb6 100644
--- a/packages/block-editor/src/components/media-placeholder/index.js
+++ b/packages/block-editor/src/components/media-placeholder/index.js
@@ -64,6 +64,7 @@ export function MediaPlaceholder( {
accept,
addToGallery,
multiple = false,
+ handleUpload = true,
dropZoneUIOnly,
disableDropZone,
disableMediaButtons,
@@ -118,6 +119,9 @@ export function MediaPlaceholder( {
};
const onFilesUpload = ( files ) => {
+ if ( ! handleUpload ) {
+ return onSelect( files );
+ }
onFilesPreUpload( files );
let setMedia;
if ( multiple ) {
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 8beac02827f612..119afc32e7c827 100644
--- a/packages/block-library/src/gallery/block.json
+++ b/packages/block-library/src/gallery/block.json
@@ -54,6 +54,13 @@
},
"default": []
},
+ "shortCodeTransforms": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "object"
+ }
+ },
"columns": {
"type": "number",
"minimum": 1,
@@ -68,14 +75,36 @@
"type": "boolean",
"default": true
},
+ "linkTarget": {
+ "type": "string"
+ },
"linkTo": {
"type": "string"
},
"sizeSlug": {
"type": "string",
"default": "large"
+ },
+ "allowResize": {
+ "type": "boolean",
+ "default": false
+ },
+ "isGrouped": {
+ "type": "boolean",
+ "default": true
+ },
+ "imageCount": {
+ "type": "number",
+ "default": 0
+ },
+ "gutterSize": {
+ "type": "number"
}
},
+ "providesContext": {
+ "allowResize": "allowResize",
+ "isGrouped": "isGrouped"
+ },
"supports": {
"anchor": true,
"align": true
diff --git a/packages/block-library/src/gallery/constants.js b/packages/block-library/src/gallery/constants.js
index f4b6e7af56d473..3ac422b3e95907 100644
--- a/packages/block-library/src/gallery/constants.js
+++ b/packages/block-library/src/gallery/constants.js
@@ -1,3 +1,3 @@
export const LINK_DESTINATION_NONE = 'none';
-export const LINK_DESTINATION_MEDIA = 'file';
-export const LINK_DESTINATION_ATTACHMENT = 'post';
+export const LINK_DESTINATION_MEDIA = 'media';
+export const LINK_DESTINATION_ATTACHMENT = 'attachment';
diff --git a/packages/block-library/src/gallery/deprecated.js b/packages/block-library/src/gallery/deprecated.js
index 1b82131b890850..f97adbdd300245 100644
--- a/packages/block-library/src/gallery/deprecated.js
+++ b/packages/block-library/src/gallery/deprecated.js
@@ -10,9 +10,17 @@ import { map, some } from 'lodash';
import { RichText } from '@wordpress/block-editor';
/**
- * Internal dependencies
+ * Original function to determine default number of columns from a block's
+ * attributes.
+ *
+ * Used in deprecations: v1-6.
+ *
+ * @param {Object} attributes Block attributes.
+ * @return {number} Default number of columns for the gallery.
*/
-import { defaultColumnsNumber } from './shared';
+export function defaultColumnsNumberV1( attributes ) {
+ return Math.min( 3, attributes.images.length );
+}
const deprecated = [
{
@@ -114,7 +122,7 @@ const deprecated = [
save( { attributes } ) {
const {
images,
- columns = defaultColumnsNumber( attributes ),
+ columns = defaultColumnsNumberV1( attributes ),
imageCrop,
caption,
linkTo,
@@ -270,7 +278,7 @@ const deprecated = [
save( { attributes } ) {
const {
images,
- columns = defaultColumnsNumber( attributes ),
+ columns = defaultColumnsNumberV1( attributes ),
imageCrop,
caption,
linkTo,
@@ -409,7 +417,7 @@ const deprecated = [
save( { attributes } ) {
const {
images,
- columns = defaultColumnsNumber( attributes ),
+ columns = defaultColumnsNumberV1( attributes ),
imageCrop,
linkTo,
} = attributes;
@@ -549,7 +557,7 @@ const deprecated = [
save( { attributes } ) {
const {
images,
- columns = defaultColumnsNumber( attributes ),
+ columns = defaultColumnsNumberV1( attributes ),
imageCrop,
linkTo,
} = attributes;
@@ -655,7 +663,7 @@ const deprecated = [
save( { attributes } ) {
const {
images,
- columns = defaultColumnsNumber( attributes ),
+ columns = defaultColumnsNumberV1( attributes ),
align,
imageCrop,
linkTo,
diff --git a/packages/block-library/src/gallery/deprecated.scss b/packages/block-library/src/gallery/deprecated.scss
new file mode 100644
index 00000000000000..3adb4685c91f3b
--- /dev/null
+++ b/packages/block-library/src/gallery/deprecated.scss
@@ -0,0 +1,141 @@
+// Deprecated gallery styles pre refactoring to use nested image blocks.
+// https://github.com/WordPress/gutenberg/pull/25940.
+.wp-block-gallery,
+.blocks-gallery-grid {
+ display: flex;
+ flex-wrap: wrap;
+ list-style-type: none;
+ padding: 0;
+ // Some themes give all
default margin instead of padding.
+ margin: 0;
+
+ .blocks-gallery-image,
+ .blocks-gallery-item {
+ // Add space between thumbnails, and unset right most thumbnails later.
+ margin: 0 1em 1em 0;
+ display: flex;
+ flex-grow: 1;
+ flex-direction: column;
+ justify-content: center;
+ position: relative;
+
+ // On mobile and responsive viewports, we allow only 1 or 2 columns at the most.
+ width: calc(50% - 1em);
+
+ &:nth-of-type(even) {
+ margin-right: 0;
+ }
+
+ figure {
+ margin: 0;
+ height: 100%;
+
+ // IE doesn't support flex so omit that.
+ @supports (position: sticky) {
+ display: flex;
+ align-items: flex-end;
+ justify-content: flex-start;
+ }
+ }
+
+ img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+
+ // IE doesn't handle cropping, so we need an explicit width here.
+ width: 100%;
+
+ // IE11 doesn't read rules inside this query. They are applied only to modern browsers.
+ @supports (position: sticky) {
+ width: auto;
+ }
+ }
+
+ figcaption {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ max-height: 100%;
+ overflow: auto;
+ padding: 3em 0.77em 0.7em;
+ color: $white;
+ text-align: center;
+ font-size: 0.8em;
+ background: linear-gradient(0deg, rgba($color: $black, $alpha: 0.7) 0, rgba($color: $black, $alpha: 0.3) 70%, transparent);
+ box-sizing: border-box;
+ margin: 0;
+
+ img {
+ display: inline;
+ }
+ }
+ }
+
+ figcaption {
+ flex-grow: 1;
+ }
+
+ // Cropped
+ &.is-cropped .blocks-gallery-image,
+ &.is-cropped .blocks-gallery-item {
+ a,
+ img {
+ // IE11 doesn't support object-fit, so just make sure images aren't skewed.
+ // The following rules are for all browsers.
+ width: 100%;
+
+ // IE11 doesn't read rules inside this query. They are applied only to modern browsers.
+ @supports (position: sticky) {
+ height: 100%;
+ flex: 1;
+ object-fit: cover;
+ }
+ }
+ }
+
+ &.columns-1 .blocks-gallery-image,
+ &.columns-1 .blocks-gallery-item {
+ width: 100%;
+ margin-right: 0;
+ }
+
+ // Beyond mobile viewports, we allow up to 8 columns.
+ @include break-small {
+ @for $i from 3 through 8 {
+ &.columns-#{ $i } .blocks-gallery-image,
+ &.columns-#{ $i } .blocks-gallery-item {
+ width: calc(#{ 100% / $i } - #{ 1em * ( $i - 1 ) / $i });
+ margin-right: 1em;
+ }
+ }
+
+ // Unset the right margin on every rightmost gallery item to ensure center balance.
+ @for $column-count from 1 through 8 {
+ &.columns-#{ $column-count } .blocks-gallery-image:nth-of-type(#{ $column-count }n),
+ &.columns-#{ $column-count } .blocks-gallery-item:nth-of-type(#{ $column-count }n) {
+ margin-right: 0;
+ }
+ }
+ }
+
+ // Last item always needs margins reset.
+ .blocks-gallery-image:last-child,
+ .blocks-gallery-item:last-child {
+ margin-right: 0;
+ }
+
+ // Apply max-width to floated items that have no intrinsic width.
+ &.alignleft,
+ &.alignright {
+ max-width: $content-width / 2;
+ width: 100%;
+ }
+
+ // If the gallery is centered, center the content inside as well.
+ &.aligncenter {
+ .blocks-gallery-item figure {
+ justify-content: center;
+ }
+ }
+}
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 184efb0a6f5703..df4f875212f2f7 100644
--- a/packages/block-library/src/gallery/edit.js
+++ b/packages/block-library/src/gallery/edit.js
@@ -1,55 +1,56 @@
/**
* External dependencies
*/
-import {
- every,
- filter,
- find,
- forEach,
- get,
- isEmpty,
- map,
- reduce,
- some,
- toString,
-} from 'lodash';
+import classnames from 'classnames';
+import { isEmpty, concat, differenceBy, some, every, find } from 'lodash';
/**
* WordPress dependencies
*/
import { compose } from '@wordpress/compose';
import {
+ BaseControl,
PanelBody,
SelectControl,
ToggleControl,
withNotices,
RangeControl,
+ Spinner,
} from '@wordpress/components';
import {
+ store as blockEditorStore,
MediaPlaceholder,
InspectorControls,
useBlockProps,
- store as blockEditorStore,
} 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 { Platform, useEffect, useMemo } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
import { withViewportMatch } from '@wordpress/viewport';
import { View } from '@wordpress/primitives';
-import { store as coreStore } from '@wordpress/core-data';
+import { createBlock } from '@wordpress/blocks';
+import { createBlobURL } from '@wordpress/blob';
+import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
import { sharedIcon } from './shared-icon';
import { defaultColumnsNumber, pickRelevantMediaFiles } from './shared';
+import { getHrefAndDestination } from './utils';
+import {
+ getUpdatedLinkTargetSettings,
+ getImageSizeAttributes,
+} from '../image/utils';
import Gallery from './gallery';
import {
LINK_DESTINATION_ATTACHMENT,
LINK_DESTINATION_MEDIA,
LINK_DESTINATION_NONE,
} from './constants';
+import useImageSizes from './use-image-sizes';
+import useShortCodeTransform from './use-short-code-transform';
const MAX_COLUMNS = 8;
const linkOptions = [
@@ -71,148 +72,203 @@ const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.select( {
native: { type: 'stepper' },
} );
+const DEFAULT_GUTTER_SIZE = 16;
+const MAX_GUTTER_SIZE = 100;
+const MIN_GUTTER_SIZE = 0;
+
function GalleryEdit( props ) {
const {
+ setAttributes,
attributes,
+ className,
+ clientId,
+ noticeOperations,
isSelected,
noticeUI,
- noticeOperations,
- mediaUpload,
- imageSizes,
- resizedImages,
- onFocus,
+ insertBlocksAfter,
} = props;
+
const {
- columns = defaultColumnsNumber( attributes ),
+ imageCount,
+ columns = defaultColumnsNumber( imageCount ),
+ gutterSize,
imageCrop,
- images,
+ linkTarget,
linkTo,
+ shortCodeTransforms,
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 );
+ const {
+ __unstableMarkNextChangeAsNotPersistent,
+ replaceInnerBlocks,
+ updateBlockAttributes,
+ } = useDispatch( blockEditorStore );
+ const { createSuccessNotice } = useDispatch( noticesStore );
+
+ const { getBlock, getSettings, preferredStyle } = useSelect( ( select ) => {
+ const settings = select( blockEditorStore ).getSettings();
+ const preferredStyleVariations =
+ settings.__experimentalPreferredStyleVariations;
+ return {
+ getBlock: select( blockEditorStore ).getBlock,
+ getSettings: select( blockEditorStore ).getSettings,
+ preferredStyle: preferredStyleVariations?.value?.[ 'core/image' ],
};
- }
+ }, [] );
- function onDeselectImage() {
- return () => {
- setSelectedImage();
- };
- }
+ const innerBlockImages = useSelect(
+ ( select ) => {
+ return select( blockEditorStore ).getBlock( clientId )?.innerBlocks;
+ },
+ [ clientId ]
+ );
- function onMove( oldIndex, newIndex ) {
- const newImages = [ ...images ];
- newImages.splice( newIndex, 1, images[ oldIndex ] );
- newImages.splice( oldIndex, 1, images[ newIndex ] );
- setSelectedImage( newIndex );
- setAttributes( { images: newImages } );
- }
+ const images = useMemo(
+ () =>
+ innerBlockImages?.map( ( block ) => ( {
+ id: block.attributes.id,
+ url: block.attributes.url,
+ attributes: block.attributes,
+ } ) ),
+ [ innerBlockImages ]
+ );
- function onMoveForward( oldIndex ) {
- return () => {
- if ( oldIndex === images.length - 1 ) {
- return;
+ const imageData = useSelect(
+ ( select ) => {
+ if (
+ ! innerBlockImages?.length ||
+ some(
+ innerBlockImages,
+ ( imageBlock ) => ! imageBlock.attributes.id
+ )
+ ) {
+ return imageData;
}
- onMove( oldIndex, oldIndex + 1 );
- };
- }
+ const getMedia = select( coreStore ).getMedia;
+ const newImageData = innerBlockImages.map( ( imageBlock ) => {
+ return {
+ id: imageBlock.attributes.id,
+ data: getMedia( imageBlock.attributes.id ),
+ };
+ } );
- function onMoveBackward( oldIndex ) {
- return () => {
- if ( oldIndex === 0 ) {
- return;
+ if ( every( newImageData, ( img ) => img.data ) ) {
+ return newImageData;
}
- 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,
- } );
- };
- }
+ return imageData;
+ },
+ [ innerBlockImages ]
+ );
+
+ const shortCodeImages = useShortCodeTransform( shortCodeTransforms );
- 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;
+ useEffect( () => {
+ if ( ! shortCodeTransforms || ! shortCodeImages ) {
+ return;
}
+ updateImages( shortCodeImages );
+ setAttributes( { shortCodeTransforms: undefined } );
+ }, [ shortCodeTransforms, shortCodeImages ] );
- const attachment = find( attachmentCaptions, {
- id: newImageId,
- } );
+ useEffect( () => {
+ if ( ! images ) {
+ setAttributes( { imageCount: undefined } );
+ return;
+ }
- // if the attachment caption is updated
- if ( attachment && attachment.caption !== newImage.caption ) {
- return newImage.caption;
+ if ( images.length !== imageCount ) {
+ setAttributes( { imageCount: images.length } );
}
+ }, [ images ] );
+
+ const imageSizeOptions = useImageSizes(
+ imageData,
+ isSelected,
+ getSettings
+ );
- return currentImageCaption;
+ /**
+ * Determines the image attributes that should be applied to an image block
+ * after the gallery updates.
+ *
+ * The gallery will receive the full collection of images when a new image
+ * is added. As a result we need to reapply the image's original settings if
+ * 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.
+ */
+ function buildImageAttributes( existingBlock, image ) {
+ if ( existingBlock ) {
+ return existingBlock.attributes;
+ }
+ return {
+ ...pickRelevantMediaFiles( image, sizeSlug ),
+ ...getHrefAndDestination( image, linkTo ),
+ ...getUpdatedLinkTargetSettings( linkTarget, attributes ),
+ className: preferredStyle
+ ? `is-style-${ preferredStyle }`
+ : undefined,
+ sizeSlug,
+ };
}
- 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 updateImages( selectedImages ) {
+ const newFileUploads =
+ Object.prototype.toString.call( selectedImages ) ===
+ '[object FileList]';
+
+ const imageArray = newFileUploads
+ ? Array.from( selectedImages ).map( ( file ) => {
+ if ( ! file.url ) {
+ return pickRelevantMediaFiles( {
+ url: createBlobURL( file ),
+ } );
+ }
+
+ return file;
+ } )
+ : selectedImages;
+
+ const processedImages = imageArray
+ .filter(
+ ( file ) => file.url || file.type?.indexOf( 'image/' ) === 0
+ )
+ .map( ( file ) => {
+ if ( ! file.url ) {
+ return pickRelevantMediaFiles( {
+ url: createBlobURL( file ),
+ } );
+ }
+
+ return file;
+ } );
+
+ const existingImageBlocks = ! newFileUploads
+ ? innerBlockImages.filter( ( block ) =>
+ processedImages.find(
+ ( img ) => img.url === block.attributes.url
+ )
+ )
+ : innerBlockImages;
+
+ const newImages = differenceBy( processedImages, images, 'url' );
+
+ const newBlocks = newImages.map( ( image ) => {
+ return createBlock( 'core/image', {
+ ...buildImageAttributes( false, image ),
+ id: image.id,
+ } );
} );
+
+ replaceInnerBlocks(
+ clientId,
+ concat( existingImageBlocks, newBlocks )
+ );
}
function onUploadError( message ) {
@@ -222,6 +278,29 @@ function GalleryEdit( props ) {
function setLinkTo( value ) {
setAttributes( { linkTo: value } );
+ getBlock( clientId ).innerBlocks.forEach( ( block ) => {
+ const image = block.attributes.id
+ ? find( imageData, { id: block.attributes.id } )
+ : null;
+ updateBlockAttributes( block.clientId, {
+ ...getHrefAndDestination( image.data, value ),
+ } );
+ } );
+
+ const linkToText = [ ...linkOptions ].find(
+ ( linkType ) => linkType.value === value
+ );
+
+ createSuccessNotice(
+ sprintf(
+ /* translators: %s: image size settings */
+ __( 'All gallery image links updated to: %s' ),
+ linkToText.label
+ ),
+ {
+ type: 'snackbar',
+ }
+ );
}
function setColumnsNumber( value ) {
@@ -238,81 +317,54 @@ function GalleryEdit( props ) {
: __( 'Thumbnails are not cropped.' );
}
- function onFocusGalleryCaption() {
- setSelectedImage();
+ function toggleOpenInNewTab( openInNewTab ) {
+ const newLinkTarget = openInNewTab ? '_blank' : undefined;
+ setAttributes( { linkTarget: newLinkTarget } );
+ getBlock( clientId ).innerBlocks.forEach( ( block ) => {
+ updateBlockAttributes( block.clientId, {
+ ...getUpdatedLinkTargetSettings(
+ newLinkTarget,
+ block.attributes
+ ),
+ } );
+ } );
+ const noticeText = openInNewTab
+ ? __( 'All gallery images updated to open in new tab' )
+ : __( 'All gallery images updated to not open in new tab' );
+ createSuccessNotice( noticeText, {
+ type: 'snackbar',
+ } );
}
- function setImageAttributes( index, newAttributes ) {
- if ( ! images[ index ] ) {
- return;
- }
-
- setAttributes( {
- images: [
- ...images.slice( 0, index ),
- {
- ...images[ index ],
- ...newAttributes,
- },
- ...images.slice( index + 1 ),
- ],
+ function updateImagesSize( newSizeSlug ) {
+ setAttributes( { sizeSlug: newSizeSlug } );
+ getBlock( clientId ).innerBlocks.forEach( ( block ) => {
+ const image = block.attributes.id
+ ? find( imageData, { id: block.attributes.id } )
+ : null;
+ updateBlockAttributes( block.clientId, {
+ ...getImageSizeAttributes( image.data, newSizeSlug ),
+ } );
} );
- }
- function getImagesSizeOptions() {
- return map(
- filter( imageSizes, ( { slug } ) =>
- some( resizedImages, ( sizes ) => sizes[ slug ] )
- ),
- ( { name, slug } ) => ( { value: slug, label: name } )
+ const imageSize = imageSizeOptions.find(
+ ( size ) => size.value === newSizeSlug
);
- }
- function updateImagesSize( newSizeSlug ) {
- const updatedImages = map( images, ( image ) => {
- if ( ! image.id ) {
- return image;
+ createSuccessNotice(
+ sprintf(
+ /* translators: %s: image size settings */
+ __( 'All gallery image sizes updated to: %s' ),
+ imageSize.label
+ ),
+ {
+ type: 'snackbar',
}
- 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
+ // linkTo attribute must be saved so blocks don't break when changing image_default_link_type in options.php
if ( ! linkTo ) {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( {
@@ -323,11 +375,12 @@ function GalleryEdit( props ) {
}
}, [ linkTo ] );
- const hasImages = !! images.length;
+ const hasImages = !! images?.length;
const mediaPlaceholder = (
);
- const blockProps = useBlockProps();
+ const blockProps = useBlockProps( {
+ className: classnames( className, 'has-nested-images' ),
+ style: {
+ '--gallery-block--gutter-size':
+ gutterSize !== undefined ? `${ gutterSize }px` : undefined,
+ },
+ } );
if ( ! hasImages ) {
return { mediaPlaceholder };
}
- const imageSizeOptions = getImagesSizeOptions();
- const shouldShowSizeOptions = hasImages && ! isEmpty( imageSizeOptions );
+ const shouldShowSizeOptions = ! isEmpty( imageSizeOptions );
+ const hasLinkTo = linkTo && linkTo !== 'none';
return (
<>
@@ -370,6 +428,19 @@ function GalleryEdit( props ) {
required
/>
) }
+
+ setAttributes( { gutterSize: newGutterSize } )
+ }
+ initialPosition={ DEFAULT_GUTTER_SIZE }
+ min={ MIN_GUTTER_SIZE }
+ max={ MAX_GUTTER_SIZE }
+ { ...MOBILE_CONTROL_PROPS_RANGE_CONTROL }
+ resetFallbackValue={ DEFAULT_GUTTER_SIZE }
+ allowReset
+ />
- { shouldShowSizeOptions && (
+ { hasLinkTo && (
+
+ ) }
+ { shouldShowSizeOptions ? (
+ ) : (
+
+
+ { __( 'Image size' ) }
+
+
+
+ { __( 'Loading options…' ) }
+
+
) }
{ 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' } ),
] )( GalleryEdit );
diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss
index 817f65a62327b0..65b9e86692de8d 100644
--- a/packages/block-library/src/gallery/editor.scss
+++ b/packages/block-library/src/gallery/editor.scss
@@ -1,11 +1,24 @@
-.wp-block-gallery {
+figure.wp-block-gallery {
// Override the default list style type _only in the editor_
// to avoid :not() selector specificity issues.
// See https://github.com/WordPress/gutenberg/pull/10358
- li {
- list-style-type: none;
+
+ display: block;
+ margin: 0;
+ &.has-nested-images {
+ .components-drop-zone {
+ display: none;
+ pointer-events: none;
+ }
+ }
+
+ > .blocks-gallery-caption {
+ flex: 0 0 100%;
}
+ .components-form-file-upload {
+ flex-basis: 100%;
+ }
// @todo: this deserves a refactor, by being moved to the toolbar.
.block-editor-media-placeholder.is-appender {
.components-placeholder__label {
@@ -15,22 +28,54 @@
margin-bottom: 0;
}
}
+ .block-editor-media-placeholder {
+ margin: 0;
+ height: 100%;
+
+ .components-placeholder__label {
+ display: flex;
+ }
+ figcaption {
+ z-index: 2;
+ }
+ }
+
+
}
-figure.wp-block-gallery {
- display: block;
- margin: 0;
+
+/**
+ * Gallery inspector controls settings.
+ */
+.gallery-settings-buttons {
+ .components-button:first-child {
+ margin-right: 8px;
+ }
}
-// Necessary to to override default editor ul styles.
-.blocks-gallery-grid.blocks-gallery-grid {
- padding-left: 0;
- margin-left: 0;
- margin-bottom: 0;
+.gallery-image-sizes {
+ .components-base-control__label {
+ display: block;
+ margin-bottom: 4px;
+ }
+
+ .gallery-image-sizes__loading {
+ display: flex;
+ align-items: center;
+ color: $gray-700;
+ font-size: $helptext-font-size;
+ }
+
+ .components-spinner {
+ 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 {
@@ -51,9 +96,6 @@ figure.wp-block-gallery {
left: 0;
z-index: 1;
}
- figcaption {
- z-index: 2;
- }
}
figure.is-transient img {
@@ -124,11 +166,8 @@ figure.wp-block-gallery {
}
}
-
-.blocks-gallery-item .components-spinner {
- position: absolute;
- top: 50%;
- left: 50%;
- margin-top: -9px;
- margin-left: -9px;
-}
+.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/gallery.js b/packages/block-library/src/gallery/gallery.js
index dc261bbf1e4699..1461cbe8cbfaf3 100644
--- a/packages/block-library/src/gallery/gallery.js
+++ b/packages/block-library/src/gallery/gallery.js
@@ -6,15 +6,18 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { RichText } from '@wordpress/block-editor';
+import {
+ RichText,
+ __experimentalUseInnerBlocksProps as useInnerBlocksProps,
+} from '@wordpress/block-editor';
import { VisuallyHidden } from '@wordpress/components';
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
+import { useRef, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
-import GalleryImage from './gallery-image';
import { defaultColumnsNumber } from './shared';
export const Gallery = ( props ) => {
@@ -22,76 +25,47 @@ export const Gallery = ( props ) => {
attributes,
isSelected,
setAttributes,
- selectedImage,
mediaPlaceholder,
- onMoveBackward,
- onMoveForward,
- onRemoveImage,
- onSelectImage,
- onDeselectImage,
- onSetImageAttributes,
- onFocusGalleryCaption,
insertBlocksAfter,
blockProps,
} = props;
const {
+ imageCount,
align,
- columns = defaultColumnsNumber( attributes ),
+ columns = defaultColumnsNumber( imageCount ),
caption,
imageCrop,
- images,
} = attributes;
+ const galleryRef = useRef();
+ const { children, ...innerBlocksProps } = useInnerBlocksProps( blockProps, {
+ allowedBlocks: [ 'core/image' ],
+ orientation: 'horizontal',
+ renderAppender: false,
+ __experimentalLayout: { type: 'default', alignments: [] },
+ } );
+
+ useEffect( () => {
+ if ( galleryRef.current && isSelected ) {
+ galleryRef.current.parentElement.focus();
+ }
+ }, [ isSelected ] );
return (
);
};
diff --git a/packages/block-library/src/gallery/index.js b/packages/block-library/src/gallery/index.js
index 677291252e7ce6..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';
@@ -25,17 +25,24 @@ export const settings = {
example: {
attributes: {
columns: 2,
- images: [
- {
+ imageCount: 2,
+ },
+ innerBlocks: [
+ {
+ name: 'core/image',
+ attributes: {
url:
'https://s.w.org/images/core/5.3/Glacial_lakes%2C_Bhutan.jpg',
},
- {
+ },
+ {
+ name: 'core/image',
+ attributes: {
url:
'https://s.w.org/images/core/5.3/Sediment_off_the_Yucatan_Peninsula.jpg',
},
- ],
- },
+ },
+ ],
},
transforms,
edit,
diff --git a/packages/block-library/src/gallery/save.js b/packages/block-library/src/gallery/save.js
index 0c51884a5db957..54f6d7f2a1c7b7 100644
--- a/packages/block-library/src/gallery/save.js
+++ b/packages/block-library/src/gallery/save.js
@@ -1,74 +1,39 @@
/**
* WordPress dependencies
*/
-import { RichText, useBlockProps } from '@wordpress/block-editor';
+import { RichText, useBlockProps, InnerBlocks } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { defaultColumnsNumber } from './shared';
-import {
- LINK_DESTINATION_ATTACHMENT,
- LINK_DESTINATION_MEDIA,
-} from './constants';
+import saveV1 from './v1/save';
export default function save( { attributes } ) {
+ if ( attributes?.ids?.length > 0 || attributes?.images?.length > 0 ) {
+ return saveV1( { attributes } );
+ }
+
const {
- images,
- columns = defaultColumnsNumber( attributes ),
- imageCrop,
+ imageCount,
caption,
- linkTo,
+ columns = defaultColumnsNumber( imageCount ),
+ gutterSize,
+ imageCrop,
} = attributes;
- const className = `columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }`;
- return (
-