From ba7cc6c6d11a55eb3d1f3484b4457c7b768f26ca Mon Sep 17 00:00:00 2001 From: yogeshbhutkar Date: Fri, 13 Dec 2024 10:35:13 +0530 Subject: [PATCH 1/6] Refactor "Settings" panel of Gallery block to use ToolsPanel instead of PanelBody --- packages/block-library/src/gallery/edit.js | 191 ++++++++++++++------- 1 file changed, 129 insertions(+), 62 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 01e725cc16cde6..4b12945782438b 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -8,13 +8,14 @@ import clsx from 'clsx'; */ import { BaseControl, - PanelBody, SelectControl, ToggleControl, RangeControl, Spinner, MenuGroup, MenuItem, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, ToolbarDropdownMenu, } from '@wordpress/components'; import { @@ -64,6 +65,7 @@ import useGetNewImages from './use-get-new-images'; import useGetMedia from './use-get-media'; import GapStyles from './gap-styles'; +const DEFAULT_COLUMNS = 1; const MAX_COLUMNS = 8; const LINK_OPTIONS = [ { @@ -560,84 +562,149 @@ export default function GalleryEdit( props ) { return ( <> - + { + setColumnsNumber( DEFAULT_COLUMNS ); + setLinkTo( 'none' ); + toggleImageCrop(); + toggleRandomOrder(); + toggleOpenInNewTab( false ); + updateImagesSize( 'full' ); + } } + > { images.length > 1 && ( - + columns || defaultColumnsNumber( images.length ) } - onChange={ setColumnsNumber } - min={ 1 } - max={ Math.min( MAX_COLUMNS, images.length ) } - { ...MOBILE_CONTROL_PROPS_RANGE_CONTROL } - required - __next40pxDefaultSize - /> + onDeselect={ () => + setColumnsNumber( DEFAULT_COLUMNS ) + } + > + + ) } { imageSizeOptions?.length > 0 && ( - + hasValue={ () => sizeSlug } + onDeselect={ () => + updateImagesSize( imageSizeOptions[ 0 ].value ) + } + > + + ) } - { Platform.isNative ? ( - - ) : null } - linkTo } + onDeselect={ () => setLinkTo( 'none' ) } + > + + + ) } + - imageCrop } + onDeselect={ () => toggleImageCrop() } + > + + + - { hasLinkTo && ( + hasValue={ () => randomOrder } + onDeselect={ () => toggleRandomOrder() } + > + + { hasLinkTo && ( + linkTarget === '_blank' } + onDeselect={ () => toggleOpenInNewTab( false ) } + > + + ) } { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( - sizeSlug } + onDeselect={ () => updateImagesSize( 'full' ) } > - - { __( 'Resolution' ) } - - - - { __( 'Loading options…' ) } - - + + + { __( 'Resolution' ) } + + + + { __( 'Loading options…' ) } + + + ) } - + { Platform.isWeb ? ( From 66a1f6607682a228798f17d51ce792bd005adadf Mon Sep 17 00:00:00 2001 From: yogeshbhutkar Date: Fri, 13 Dec 2024 12:44:58 +0530 Subject: [PATCH 2/6] Refactored logic to determine defaults --- .../block-library/src/gallery/constants.js | 2 + packages/block-library/src/gallery/edit.js | 81 +++++++++++-------- .../block-library/src/gallery/editor.scss | 1 + 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/packages/block-library/src/gallery/constants.js b/packages/block-library/src/gallery/constants.js index 19fc61cac84583..97e8a24204547f 100644 --- a/packages/block-library/src/gallery/constants.js +++ b/packages/block-library/src/gallery/constants.js @@ -4,3 +4,5 @@ export const LINK_DESTINATION_LIGHTBOX = 'lightbox'; export const LINK_DESTINATION_ATTACHMENT = 'attachment'; export const LINK_DESTINATION_MEDIA_WP_CORE = 'file'; export const LINK_DESTINATION_ATTACHMENT_WP_CORE = 'post'; +export const DEFAULT_COLUMNS = 1; +export const DEFAULT_SIZE_SLUG = 'large'; diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 4b12945782438b..595ff03d033a8d 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -59,13 +59,14 @@ import { LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, LINK_DESTINATION_LIGHTBOX, + DEFAULT_COLUMNS, + DEFAULT_SIZE_SLUG, } from './constants'; import useImageSizes from './use-image-sizes'; import useGetNewImages from './use-get-new-images'; import useGetMedia from './use-get-media'; import GapStyles from './gap-styles'; -const DEFAULT_COLUMNS = 1; const MAX_COLUMNS = 8; const LINK_OPTIONS = [ { @@ -476,6 +477,12 @@ export default function GalleryEdit( props ) { ); } + const defaultImageSizeSlug = imageSizeOptions?.find( + ( size ) => size.value === DEFAULT_SIZE_SLUG + ) + ? DEFAULT_SIZE_SLUG + : imageSizeOptions?.[ 0 ]?.value; + useEffect( () => { // linkTo attribute must be saved so blocks don't break when changing image_default_link_type in options.php. if ( ! linkTo ) { @@ -557,6 +564,10 @@ export default function GalleryEdit( props ) { ); } + const sizeSlugInOptions = imageSizeOptions?.some( + ( size ) => size.value === sizeSlug + ); + const hasLinkTo = linkTo && linkTo !== 'none'; return ( @@ -566,11 +577,13 @@ export default function GalleryEdit( props ) { label={ __( 'Settings' ) } resetAll={ () => { setColumnsNumber( DEFAULT_COLUMNS ); - setLinkTo( 'none' ); - toggleImageCrop(); - toggleRandomOrder(); + setLinkTo( LINK_DESTINATION_NONE ); + updateImagesSize( defaultImageSizeSlug ); toggleOpenInNewTab( false ); - updateImagesSize( 'full' ); + setAttributes( { + imageCrop: true, + randomOrder: false, + } ); } } > { images.length > 1 && ( @@ -578,10 +591,10 @@ export default function GalleryEdit( props ) { isShownByDefault label={ __( 'Columns' ) } hasValue={ () => - columns || defaultColumnsNumber( images.length ) + columns ? columns !== images.length : false } onDeselect={ () => - setColumnsNumber( DEFAULT_COLUMNS ) + setColumnsNumber( images.length ) } > sizeSlug } + hasValue={ () => + sizeSlug !== defaultImageSizeSlug && + sizeSlugInOptions + } onDeselect={ () => - updateImagesSize( imageSizeOptions[ 0 ].value ) + updateImagesSize( defaultImageSizeSlug ) } > linkTo } - onDeselect={ () => setLinkTo( 'none' ) } + hasValue={ () => hasLinkTo } + onDeselect={ () => + setLinkTo( LINK_DESTINATION_NONE ) + } > imageCrop } - onDeselect={ () => toggleImageCrop() } + hasValue={ () => + imageCrop === undefined || ! imageCrop + } + onDeselect={ () => + setAttributes( { imageCrop: true } ) + } > randomOrder } - onDeselect={ () => toggleRandomOrder() } + onDeselect={ () => + setAttributes( { randomOrder: false } ) + } > ) } { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( - sizeSlug } - onDeselect={ () => updateImagesSize( 'full' ) } + - - - { __( 'Resolution' ) } - - - - { __( 'Loading options…' ) } - - - + + { __( 'Resolution' ) } + + + + { __( 'Loading options…' ) } + + ) } diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss index 61121f3dd866dc..f32c636a136181 100644 --- a/packages/block-library/src/gallery/editor.scss +++ b/packages/block-library/src/gallery/editor.scss @@ -78,6 +78,7 @@ align-items: center; color: $gray-700; font-size: $helptext-font-size; + width: max-content; } .components-spinner { From 9532b63d3cda7d9f4108b66ab640e36f486ed489 Mon Sep 17 00:00:00 2001 From: yogeshbhutkar Date: Fri, 13 Dec 2024 13:02:49 +0530 Subject: [PATCH 3/6] Remove DEFAULT_COLUMNS constant and update columns number logic in Gallery block --- packages/block-library/src/gallery/constants.js | 1 - packages/block-library/src/gallery/edit.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/block-library/src/gallery/constants.js b/packages/block-library/src/gallery/constants.js index 97e8a24204547f..d7c5684d420d42 100644 --- a/packages/block-library/src/gallery/constants.js +++ b/packages/block-library/src/gallery/constants.js @@ -4,5 +4,4 @@ export const LINK_DESTINATION_LIGHTBOX = 'lightbox'; export const LINK_DESTINATION_ATTACHMENT = 'attachment'; export const LINK_DESTINATION_MEDIA_WP_CORE = 'file'; export const LINK_DESTINATION_ATTACHMENT_WP_CORE = 'post'; -export const DEFAULT_COLUMNS = 1; export const DEFAULT_SIZE_SLUG = 'large'; diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 595ff03d033a8d..2cddd7e43c7d18 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -59,7 +59,6 @@ import { LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, LINK_DESTINATION_LIGHTBOX, - DEFAULT_COLUMNS, DEFAULT_SIZE_SLUG, } from './constants'; import useImageSizes from './use-image-sizes'; @@ -576,7 +575,7 @@ export default function GalleryEdit( props ) { { - setColumnsNumber( DEFAULT_COLUMNS ); + setColumnsNumber( images.length ); setLinkTo( LINK_DESTINATION_NONE ); updateImagesSize( defaultImageSizeSlug ); toggleOpenInNewTab( false ); From 3b4aa3ee6053dfc818c52f3a653d956b4532a175 Mon Sep 17 00:00:00 2001 From: yogeshbhutkar Date: Fri, 13 Dec 2024 14:19:16 +0530 Subject: [PATCH 4/6] Enhance reset logic in Gallery block's ToolsPanel to handle link and size updates more effectively --- packages/block-library/src/gallery/edit.js | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 2cddd7e43c7d18..f17617f576b8fd 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -575,10 +575,22 @@ export default function GalleryEdit( props ) { { + if ( hasLinkTo ) { + toggleOpenInNewTab( false ); + } + + if ( Platform.isNative && hasLinkTo ) { + setLinkTo( LINK_DESTINATION_NONE ); + } + + if ( + sizeSlug !== defaultImageSizeSlug && + sizeSlugInOptions + ) { + updateImagesSize( defaultImageSizeSlug ); + } + setColumnsNumber( images.length ); - setLinkTo( LINK_DESTINATION_NONE ); - updateImagesSize( defaultImageSizeSlug ); - toggleOpenInNewTab( false ); setAttributes( { imageCrop: true, randomOrder: false, @@ -662,9 +674,7 @@ export default function GalleryEdit( props ) { - imageCrop === undefined || ! imageCrop - } + hasValue={ () => ! imageCrop } onDeselect={ () => setAttributes( { imageCrop: true } ) } @@ -679,7 +689,7 @@ export default function GalleryEdit( props ) { randomOrder } + hasValue={ () => !! randomOrder } onDeselect={ () => setAttributes( { randomOrder: false } ) } From 59a6d8e5a2a026347948c4832fba49b2d369d33a Mon Sep 17 00:00:00 2001 From: yogeshbhutkar Date: Mon, 16 Dec 2024 09:58:33 +0530 Subject: [PATCH 5/6] Refactor: Create edit.native.js to render native specific components --- .../block-library/src/gallery/edit.native.js | 721 ++++++++++++++++++ 1 file changed, 721 insertions(+) create mode 100644 packages/block-library/src/gallery/edit.native.js diff --git a/packages/block-library/src/gallery/edit.native.js b/packages/block-library/src/gallery/edit.native.js new file mode 100644 index 00000000000000..01e725cc16cde6 --- /dev/null +++ b/packages/block-library/src/gallery/edit.native.js @@ -0,0 +1,721 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { + BaseControl, + PanelBody, + SelectControl, + ToggleControl, + RangeControl, + Spinner, + MenuGroup, + MenuItem, + ToolbarDropdownMenu, +} from '@wordpress/components'; +import { + store as blockEditorStore, + MediaPlaceholder, + InspectorControls, + useBlockProps, + useInnerBlocksProps, + BlockControls, + MediaReplaceFlow, + useSettings, +} from '@wordpress/block-editor'; +import { Platform, useEffect, useMemo } from '@wordpress/element'; +import { __, _x, sprintf } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { View } from '@wordpress/primitives'; +import { createBlock } from '@wordpress/blocks'; +import { createBlobURL } from '@wordpress/blob'; +import { store as noticesStore } from '@wordpress/notices'; +import { + link as linkIcon, + customLink, + image as imageIcon, + linkOff, + fullscreen, +} from '@wordpress/icons'; + +/** + * 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, + LINK_DESTINATION_LIGHTBOX, +} from './constants'; +import useImageSizes from './use-image-sizes'; +import useGetNewImages from './use-get-new-images'; +import useGetMedia from './use-get-media'; +import GapStyles from './gap-styles'; + +const MAX_COLUMNS = 8; +const LINK_OPTIONS = [ + { + icon: customLink, + label: __( 'Link images to attachment pages' ), + value: LINK_DESTINATION_ATTACHMENT, + noticeText: __( 'Attachment Pages' ), + }, + { + icon: imageIcon, + label: __( 'Link images to media files' ), + value: LINK_DESTINATION_MEDIA, + noticeText: __( 'Media Files' ), + }, + { + icon: fullscreen, + label: __( 'Expand on click' ), + value: LINK_DESTINATION_LIGHTBOX, + noticeText: __( 'Lightbox effect' ), + infoText: __( 'Scale images with a lightbox effect' ), + }, + { + icon: linkOff, + label: _x( 'None', 'Media item link option' ), + value: LINK_DESTINATION_NONE, + noticeText: __( 'None' ), + }, +]; +const ALLOWED_MEDIA_TYPES = [ 'image' ]; + +const PLACEHOLDER_TEXT = Platform.isNative + ? __( 'Add media' ) + : __( 'Drag and drop images, upload, or choose from your library.' ); + +const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.isNative + ? { type: 'stepper' } + : {}; + +const DEFAULT_BLOCK = { name: 'core/image' }; +const EMPTY_ARRAY = []; + +export default function GalleryEdit( props ) { + const { + setAttributes, + attributes, + className, + clientId, + isSelected, + insertBlocksAfter, + isContentLocked, + onFocus, + } = props; + + const [ lightboxSetting ] = useSettings( 'blocks.core/image.lightbox' ); + + const linkOptions = ! lightboxSetting?.allowEditing + ? LINK_OPTIONS.filter( + ( option ) => option.value !== LINK_DESTINATION_LIGHTBOX + ) + : LINK_OPTIONS; + + const { columns, imageCrop, randomOrder, linkTarget, linkTo, sizeSlug } = + attributes; + + const { + __unstableMarkNextChangeAsNotPersistent, + replaceInnerBlocks, + updateBlockAttributes, + selectBlock, + } = useDispatch( blockEditorStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + const { + getBlock, + getSettings, + innerBlockImages, + blockWasJustInserted, + multiGallerySelection, + } = useSelect( + ( select ) => { + const { + getBlockName, + getMultiSelectedBlockClientIds, + getSettings: _getSettings, + getBlock: _getBlock, + wasBlockJustInserted, + } = select( blockEditorStore ); + const multiSelectedClientIds = getMultiSelectedBlockClientIds(); + + return { + getBlock: _getBlock, + getSettings: _getSettings, + innerBlockImages: + _getBlock( clientId )?.innerBlocks ?? EMPTY_ARRAY, + blockWasJustInserted: wasBlockJustInserted( + clientId, + 'inserter_menu' + ), + multiGallerySelection: + multiSelectedClientIds.length && + multiSelectedClientIds.every( + ( _clientId ) => + getBlockName( _clientId ) === 'core/gallery' + ), + }; + }, + [ clientId ] + ); + + const images = useMemo( + () => + innerBlockImages?.map( ( block ) => ( { + clientId: block.clientId, + id: block.attributes.id, + url: block.attributes.url, + attributes: block.attributes, + fromSavedContent: Boolean( block.originalContent ), + } ) ), + [ innerBlockImages ] + ); + + const imageData = useGetMedia( innerBlockImages ); + + const newImages = useGetNewImages( images, imageData ); + + useEffect( () => { + newImages?.forEach( ( newImage ) => { + // Update the images data without creating new undo levels. + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( newImage.clientId, { + ...buildImageAttributes( newImage.attributes ), + id: newImage.id, + align: undefined, + } ); + } ); + }, [ newImages ] ); + + const imageSizeOptions = useImageSizes( + imageData, + isSelected, + getSettings + ); + + /** + * 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} imageAttributes Media object for the actual image. + * @return {Object} Attributes to set on the new image block. + */ + function buildImageAttributes( imageAttributes ) { + const image = imageAttributes.id + ? imageData.find( ( { id } ) => id === imageAttributes.id ) + : null; + + let newClassName; + if ( imageAttributes.className && imageAttributes.className !== '' ) { + newClassName = imageAttributes.className; + } + + let newLinkTarget; + if ( imageAttributes.linkTarget || imageAttributes.rel ) { + // When transformed from image blocks, the link destination and rel attributes are inherited. + newLinkTarget = { + linkTarget: imageAttributes.linkTarget, + rel: imageAttributes.rel, + }; + } else { + // When an image is added, update the link destination and rel attributes according to the gallery settings + newLinkTarget = getUpdatedLinkTargetSettings( + linkTarget, + attributes + ); + } + + return { + ...pickRelevantMediaFiles( image, sizeSlug ), + ...getHrefAndDestination( + image, + linkTo, + imageAttributes?.linkDestination + ), + ...newLinkTarget, + className: newClassName, + sizeSlug, + caption: imageAttributes.caption || image.caption?.raw, + alt: imageAttributes.alt || image.alt_text, + }; + } + + function isValidFileType( file ) { + // It's necessary to retrieve the media type from the raw image data for already-uploaded images on native. + const nativeFileData = + Platform.isNative && file.id + ? imageData.find( ( { id } ) => id === file.id ) + : null; + + const mediaTypeSelector = nativeFileData + ? nativeFileData?.media_type + : file.type; + + return ( + ALLOWED_MEDIA_TYPES.some( + ( mediaType ) => mediaTypeSelector?.indexOf( mediaType ) === 0 + ) || file.blob + ); + } + + function updateImages( selectedImages ) { + const newFileUploads = + Object.prototype.toString.call( selectedImages ) === + '[object FileList]'; + + const imageArray = newFileUploads + ? Array.from( selectedImages ).map( ( file ) => { + if ( ! file.url ) { + return { + blob: createBlobURL( file ), + }; + } + + return file; + } ) + : selectedImages; + + if ( ! imageArray.every( isValidFileType ) ) { + createErrorNotice( + __( + 'If uploading to a gallery all files need to be image formats' + ), + { id: 'gallery-upload-invalid-file', type: 'snackbar' } + ); + } + + const processedImages = imageArray + .filter( ( file ) => file.url || isValidFileType( file ) ) + .map( ( file ) => { + if ( ! file.url ) { + return { + blob: file.blob || createBlobURL( file ), + }; + } + + return file; + } ); + + // Because we are reusing existing innerImage blocks any reordering + // done in the media library will be lost so we need to reapply that ordering + // once the new image blocks are merged in with existing. + const newOrderMap = processedImages.reduce( + ( result, image, index ) => ( + ( result[ image.id ] = index ), result + ), + {} + ); + + const existingImageBlocks = ! newFileUploads + ? innerBlockImages.filter( ( block ) => + processedImages.find( + ( img ) => img.id === block.attributes.id + ) + ) + : innerBlockImages; + + const newImageList = processedImages.filter( + ( img ) => + ! existingImageBlocks.find( + ( existingImg ) => img.id === existingImg.attributes.id + ) + ); + + const newBlocks = newImageList.map( ( image ) => { + return createBlock( 'core/image', { + id: image.id, + blob: image.blob, + url: image.url, + caption: image.caption, + alt: image.alt, + } ); + } ); + + replaceInnerBlocks( + clientId, + existingImageBlocks + .concat( newBlocks ) + .sort( + ( a, b ) => + newOrderMap[ a.attributes.id ] - + newOrderMap[ b.attributes.id ] + ) + ); + + // Select the first block to scroll into view when new blocks are added. + if ( newBlocks?.length > 0 ) { + selectBlock( newBlocks[ 0 ].clientId ); + } + } + + function onUploadError( message ) { + createErrorNotice( message, { type: 'snackbar' } ); + } + + function setLinkTo( value ) { + setAttributes( { linkTo: value } ); + const changedAttributes = {}; + const blocks = []; + getBlock( clientId ).innerBlocks.forEach( ( block ) => { + blocks.push( block.clientId ); + const image = block.attributes.id + ? imageData.find( ( { id } ) => id === block.attributes.id ) + : null; + + changedAttributes[ block.clientId ] = getHrefAndDestination( + image, + value, + false, + block.attributes, + lightboxSetting + ); + } ); + updateBlockAttributes( blocks, changedAttributes, true ); + const linkToText = [ ...linkOptions ].find( + ( linkType ) => linkType.value === value + ); + + createSuccessNotice( + sprintf( + /* translators: %s: image size settings */ + __( 'All gallery image links updated to: %s' ), + linkToText.noticeText + ), + { + id: 'gallery-attributes-linkTo', + type: 'snackbar', + } + ); + } + + function setColumnsNumber( value ) { + setAttributes( { columns: value } ); + } + + function toggleImageCrop() { + setAttributes( { imageCrop: ! imageCrop } ); + } + + function toggleRandomOrder() { + setAttributes( { randomOrder: ! randomOrder } ); + } + + function toggleOpenInNewTab( openInNewTab ) { + const newLinkTarget = openInNewTab ? '_blank' : undefined; + setAttributes( { linkTarget: newLinkTarget } ); + const changedAttributes = {}; + const blocks = []; + getBlock( clientId ).innerBlocks.forEach( ( block ) => { + blocks.push( block.clientId ); + changedAttributes[ block.clientId ] = getUpdatedLinkTargetSettings( + newLinkTarget, + block.attributes + ); + } ); + updateBlockAttributes( blocks, changedAttributes, true ); + const noticeText = openInNewTab + ? __( 'All gallery images updated to open in new tab' ) + : __( 'All gallery images updated to not open in new tab' ); + createSuccessNotice( noticeText, { + id: 'gallery-attributes-openInNewTab', + type: 'snackbar', + } ); + } + + function updateImagesSize( newSizeSlug ) { + setAttributes( { sizeSlug: newSizeSlug } ); + const changedAttributes = {}; + const blocks = []; + getBlock( clientId ).innerBlocks.forEach( ( block ) => { + blocks.push( block.clientId ); + const image = block.attributes.id + ? imageData.find( ( { id } ) => id === block.attributes.id ) + : null; + changedAttributes[ block.clientId ] = getImageSizeAttributes( + image, + newSizeSlug + ); + } ); + updateBlockAttributes( blocks, changedAttributes, true ); + const imageSize = imageSizeOptions.find( + ( size ) => size.value === newSizeSlug + ); + + createSuccessNotice( + sprintf( + /* translators: %s: image size settings */ + __( 'All gallery image sizes updated to: %s' ), + imageSize.label + ), + { + id: 'gallery-attributes-sizeSlug', + type: 'snackbar', + } + ); + } + + 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 hasImageIds = hasImages && images.some( ( image ) => !! image.id ); + const imagesUploading = images.some( ( img ) => + ! Platform.isNative + ? ! img.id && img.url?.indexOf( 'blob:' ) === 0 + : img.url?.indexOf( 'file:' ) === 0 + ); + + // MediaPlaceholder props are different between web and native hence, we provide a platform-specific set. + const mediaPlaceholderProps = Platform.select( { + web: { + addToGallery: false, + disableMediaButtons: imagesUploading, + value: {}, + }, + native: { + addToGallery: hasImageIds, + isAppender: hasImages, + disableMediaButtons: + ( hasImages && ! isSelected ) || imagesUploading, + value: hasImageIds ? images : {}, + autoOpenMediaUpload: + ! hasImages && isSelected && blockWasJustInserted, + onFocus, + }, + } ); + const mediaPlaceholder = ( + + ); + + const blockProps = useBlockProps( { + className: clsx( className, 'has-nested-images' ), + } ); + + const nativeInnerBlockProps = Platform.isNative && { + marginHorizontal: 0, + marginVertical: 0, + }; + + const innerBlocksProps = useInnerBlocksProps( blockProps, { + defaultBlock: DEFAULT_BLOCK, + directInsert: true, + orientation: 'horizontal', + renderAppender: false, + ...nativeInnerBlockProps, + } ); + + if ( ! hasImages ) { + return ( + + { innerBlocksProps.children } + { mediaPlaceholder } + + ); + } + + const hasLinkTo = linkTo && linkTo !== 'none'; + + return ( + <> + + + { images.length > 1 && ( + + ) } + { imageSizeOptions?.length > 0 && ( + + ) } + { Platform.isNative ? ( + + ) : null } + + + { hasLinkTo && ( + + ) } + { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( + + + { __( 'Resolution' ) } + + + + { __( 'Loading options…' ) } + + + ) } + + + { Platform.isWeb ? ( + + + { ( { onClose } ) => ( + + { linkOptions.map( ( linkItem ) => { + const isOptionSelected = + linkTo === linkItem.value; + return ( + { + setLinkTo( linkItem.value ); + onClose(); + } } + role="menuitemradio" + info={ linkItem.infoText } + > + { linkItem.label } + + ); + } ) } + + ) } + + + ) : null } + { Platform.isWeb && ( + <> + { ! multiGallerySelection && ( + + image.id ) + .map( ( image ) => image.id ) } + addToGallery={ hasImageIds } + /> + + ) } + + + ) } + + + ); +} From 2e92776a1011f83bf5f629b452de34917978b019 Mon Sep 17 00:00:00 2001 From: yogeshbhutkar Date: Mon, 16 Dec 2024 10:30:03 +0530 Subject: [PATCH 6/6] Refactor: Update Gallery block's settings panel to use ToolsPanel for web and PanelBody for native --- packages/block-library/src/gallery/edit.js | 253 +++--- .../block-library/src/gallery/edit.native.js | 721 ------------------ 2 files changed, 146 insertions(+), 828 deletions(-) delete mode 100644 packages/block-library/src/gallery/edit.native.js diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index f17617f576b8fd..015985c8d3faca 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -17,6 +17,7 @@ import { __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, ToolbarDropdownMenu, + PanelBody, } from '@wordpress/components'; import { store as blockEditorStore, @@ -572,42 +573,142 @@ export default function GalleryEdit( props ) { return ( <> - { - if ( hasLinkTo ) { - toggleOpenInNewTab( false ); - } - - if ( Platform.isNative && hasLinkTo ) { - setLinkTo( LINK_DESTINATION_NONE ); - } - - if ( - sizeSlug !== defaultImageSizeSlug && - sizeSlugInOptions - ) { - updateImagesSize( defaultImageSizeSlug ); - } - - setColumnsNumber( images.length ); - setAttributes( { - imageCrop: true, - randomOrder: false, - } ); - } } - > - { images.length > 1 && ( + { Platform.isWeb && ( + { + setAttributes( { + columns: images.length, + linkTarget: undefined, + linkTo: 'none', + sizeSlug: defaultImageSizeSlug, + imageCrop: true, + randomOrder: false, + } ); + } } + > + { images.length > 1 && ( + + columns ? columns !== images.length : false + } + onDeselect={ () => + setColumnsNumber( images.length ) + } + > + + + ) } + { imageSizeOptions?.length > 0 && ( + + sizeSlug !== defaultImageSizeSlug && + sizeSlugInOptions + } + onDeselect={ () => + updateImagesSize( defaultImageSizeSlug ) + } + > + + + ) } - columns ? columns !== images.length : false + label={ __( 'Crop images to fit' ) } + hasValue={ () => ! imageCrop } + onDeselect={ () => + setAttributes( { imageCrop: true } ) } + > + + + !! randomOrder } onDeselect={ () => - setColumnsNumber( images.length ) + setAttributes( { randomOrder: false } ) } > + + + { hasLinkTo && ( + linkTarget === '_blank' } + onDeselect={ () => toggleOpenInNewTab( false ) } + > + + + ) } + { ! imageSizeOptions && hasImageIds && ( + + + { __( 'Resolution' ) } + + + + { __( 'Loading options…' ) } + + + ) } + + ) } + { Platform.isNative && ( + + { images.length > 1 && ( - - ) } - { imageSizeOptions?.length > 0 && ( - - sizeSlug !== defaultImageSizeSlug && - sizeSlugInOptions - } - onDeselect={ () => - updateImagesSize( defaultImageSizeSlug ) - } - > + ) } + { imageSizeOptions?.length > 0 && ( - - ) } - { Platform.isNative && ( - hasLinkTo } - onDeselect={ () => - setLinkTo( LINK_DESTINATION_NONE ) - } - > - - - ) } - ! imageCrop } - onDeselect={ () => - setAttributes( { imageCrop: true } ) - } - > + value={ linkTo } + onChange={ setLinkTo } + options={ linkOptions } + hideCancelButton + size="__unstable-large" + /> - - !! randomOrder } - onDeselect={ () => - setAttributes( { randomOrder: false } ) - } - > - - { hasLinkTo && ( - linkTarget === '_blank' } - onDeselect={ () => toggleOpenInNewTab( false ) } - > + { hasLinkTo && ( - - ) } - { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( - - - { __( 'Resolution' ) } - - - - { __( 'Loading options…' ) } - - - ) } - + ) } + + ) } { Platform.isWeb ? ( diff --git a/packages/block-library/src/gallery/edit.native.js b/packages/block-library/src/gallery/edit.native.js deleted file mode 100644 index 01e725cc16cde6..00000000000000 --- a/packages/block-library/src/gallery/edit.native.js +++ /dev/null @@ -1,721 +0,0 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - -/** - * WordPress dependencies - */ -import { - BaseControl, - PanelBody, - SelectControl, - ToggleControl, - RangeControl, - Spinner, - MenuGroup, - MenuItem, - ToolbarDropdownMenu, -} from '@wordpress/components'; -import { - store as blockEditorStore, - MediaPlaceholder, - InspectorControls, - useBlockProps, - useInnerBlocksProps, - BlockControls, - MediaReplaceFlow, - useSettings, -} from '@wordpress/block-editor'; -import { Platform, useEffect, useMemo } from '@wordpress/element'; -import { __, _x, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { View } from '@wordpress/primitives'; -import { createBlock } from '@wordpress/blocks'; -import { createBlobURL } from '@wordpress/blob'; -import { store as noticesStore } from '@wordpress/notices'; -import { - link as linkIcon, - customLink, - image as imageIcon, - linkOff, - fullscreen, -} from '@wordpress/icons'; - -/** - * 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, - LINK_DESTINATION_LIGHTBOX, -} from './constants'; -import useImageSizes from './use-image-sizes'; -import useGetNewImages from './use-get-new-images'; -import useGetMedia from './use-get-media'; -import GapStyles from './gap-styles'; - -const MAX_COLUMNS = 8; -const LINK_OPTIONS = [ - { - icon: customLink, - label: __( 'Link images to attachment pages' ), - value: LINK_DESTINATION_ATTACHMENT, - noticeText: __( 'Attachment Pages' ), - }, - { - icon: imageIcon, - label: __( 'Link images to media files' ), - value: LINK_DESTINATION_MEDIA, - noticeText: __( 'Media Files' ), - }, - { - icon: fullscreen, - label: __( 'Expand on click' ), - value: LINK_DESTINATION_LIGHTBOX, - noticeText: __( 'Lightbox effect' ), - infoText: __( 'Scale images with a lightbox effect' ), - }, - { - icon: linkOff, - label: _x( 'None', 'Media item link option' ), - value: LINK_DESTINATION_NONE, - noticeText: __( 'None' ), - }, -]; -const ALLOWED_MEDIA_TYPES = [ 'image' ]; - -const PLACEHOLDER_TEXT = Platform.isNative - ? __( 'Add media' ) - : __( 'Drag and drop images, upload, or choose from your library.' ); - -const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.isNative - ? { type: 'stepper' } - : {}; - -const DEFAULT_BLOCK = { name: 'core/image' }; -const EMPTY_ARRAY = []; - -export default function GalleryEdit( props ) { - const { - setAttributes, - attributes, - className, - clientId, - isSelected, - insertBlocksAfter, - isContentLocked, - onFocus, - } = props; - - const [ lightboxSetting ] = useSettings( 'blocks.core/image.lightbox' ); - - const linkOptions = ! lightboxSetting?.allowEditing - ? LINK_OPTIONS.filter( - ( option ) => option.value !== LINK_DESTINATION_LIGHTBOX - ) - : LINK_OPTIONS; - - const { columns, imageCrop, randomOrder, linkTarget, linkTo, sizeSlug } = - attributes; - - const { - __unstableMarkNextChangeAsNotPersistent, - replaceInnerBlocks, - updateBlockAttributes, - selectBlock, - } = useDispatch( blockEditorStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - const { - getBlock, - getSettings, - innerBlockImages, - blockWasJustInserted, - multiGallerySelection, - } = useSelect( - ( select ) => { - const { - getBlockName, - getMultiSelectedBlockClientIds, - getSettings: _getSettings, - getBlock: _getBlock, - wasBlockJustInserted, - } = select( blockEditorStore ); - const multiSelectedClientIds = getMultiSelectedBlockClientIds(); - - return { - getBlock: _getBlock, - getSettings: _getSettings, - innerBlockImages: - _getBlock( clientId )?.innerBlocks ?? EMPTY_ARRAY, - blockWasJustInserted: wasBlockJustInserted( - clientId, - 'inserter_menu' - ), - multiGallerySelection: - multiSelectedClientIds.length && - multiSelectedClientIds.every( - ( _clientId ) => - getBlockName( _clientId ) === 'core/gallery' - ), - }; - }, - [ clientId ] - ); - - const images = useMemo( - () => - innerBlockImages?.map( ( block ) => ( { - clientId: block.clientId, - id: block.attributes.id, - url: block.attributes.url, - attributes: block.attributes, - fromSavedContent: Boolean( block.originalContent ), - } ) ), - [ innerBlockImages ] - ); - - const imageData = useGetMedia( innerBlockImages ); - - const newImages = useGetNewImages( images, imageData ); - - useEffect( () => { - newImages?.forEach( ( newImage ) => { - // Update the images data without creating new undo levels. - __unstableMarkNextChangeAsNotPersistent(); - updateBlockAttributes( newImage.clientId, { - ...buildImageAttributes( newImage.attributes ), - id: newImage.id, - align: undefined, - } ); - } ); - }, [ newImages ] ); - - const imageSizeOptions = useImageSizes( - imageData, - isSelected, - getSettings - ); - - /** - * 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} imageAttributes Media object for the actual image. - * @return {Object} Attributes to set on the new image block. - */ - function buildImageAttributes( imageAttributes ) { - const image = imageAttributes.id - ? imageData.find( ( { id } ) => id === imageAttributes.id ) - : null; - - let newClassName; - if ( imageAttributes.className && imageAttributes.className !== '' ) { - newClassName = imageAttributes.className; - } - - let newLinkTarget; - if ( imageAttributes.linkTarget || imageAttributes.rel ) { - // When transformed from image blocks, the link destination and rel attributes are inherited. - newLinkTarget = { - linkTarget: imageAttributes.linkTarget, - rel: imageAttributes.rel, - }; - } else { - // When an image is added, update the link destination and rel attributes according to the gallery settings - newLinkTarget = getUpdatedLinkTargetSettings( - linkTarget, - attributes - ); - } - - return { - ...pickRelevantMediaFiles( image, sizeSlug ), - ...getHrefAndDestination( - image, - linkTo, - imageAttributes?.linkDestination - ), - ...newLinkTarget, - className: newClassName, - sizeSlug, - caption: imageAttributes.caption || image.caption?.raw, - alt: imageAttributes.alt || image.alt_text, - }; - } - - function isValidFileType( file ) { - // It's necessary to retrieve the media type from the raw image data for already-uploaded images on native. - const nativeFileData = - Platform.isNative && file.id - ? imageData.find( ( { id } ) => id === file.id ) - : null; - - const mediaTypeSelector = nativeFileData - ? nativeFileData?.media_type - : file.type; - - return ( - ALLOWED_MEDIA_TYPES.some( - ( mediaType ) => mediaTypeSelector?.indexOf( mediaType ) === 0 - ) || file.blob - ); - } - - function updateImages( selectedImages ) { - const newFileUploads = - Object.prototype.toString.call( selectedImages ) === - '[object FileList]'; - - const imageArray = newFileUploads - ? Array.from( selectedImages ).map( ( file ) => { - if ( ! file.url ) { - return { - blob: createBlobURL( file ), - }; - } - - return file; - } ) - : selectedImages; - - if ( ! imageArray.every( isValidFileType ) ) { - createErrorNotice( - __( - 'If uploading to a gallery all files need to be image formats' - ), - { id: 'gallery-upload-invalid-file', type: 'snackbar' } - ); - } - - const processedImages = imageArray - .filter( ( file ) => file.url || isValidFileType( file ) ) - .map( ( file ) => { - if ( ! file.url ) { - return { - blob: file.blob || createBlobURL( file ), - }; - } - - return file; - } ); - - // Because we are reusing existing innerImage blocks any reordering - // done in the media library will be lost so we need to reapply that ordering - // once the new image blocks are merged in with existing. - const newOrderMap = processedImages.reduce( - ( result, image, index ) => ( - ( result[ image.id ] = index ), result - ), - {} - ); - - const existingImageBlocks = ! newFileUploads - ? innerBlockImages.filter( ( block ) => - processedImages.find( - ( img ) => img.id === block.attributes.id - ) - ) - : innerBlockImages; - - const newImageList = processedImages.filter( - ( img ) => - ! existingImageBlocks.find( - ( existingImg ) => img.id === existingImg.attributes.id - ) - ); - - const newBlocks = newImageList.map( ( image ) => { - return createBlock( 'core/image', { - id: image.id, - blob: image.blob, - url: image.url, - caption: image.caption, - alt: image.alt, - } ); - } ); - - replaceInnerBlocks( - clientId, - existingImageBlocks - .concat( newBlocks ) - .sort( - ( a, b ) => - newOrderMap[ a.attributes.id ] - - newOrderMap[ b.attributes.id ] - ) - ); - - // Select the first block to scroll into view when new blocks are added. - if ( newBlocks?.length > 0 ) { - selectBlock( newBlocks[ 0 ].clientId ); - } - } - - function onUploadError( message ) { - createErrorNotice( message, { type: 'snackbar' } ); - } - - function setLinkTo( value ) { - setAttributes( { linkTo: value } ); - const changedAttributes = {}; - const blocks = []; - getBlock( clientId ).innerBlocks.forEach( ( block ) => { - blocks.push( block.clientId ); - const image = block.attributes.id - ? imageData.find( ( { id } ) => id === block.attributes.id ) - : null; - - changedAttributes[ block.clientId ] = getHrefAndDestination( - image, - value, - false, - block.attributes, - lightboxSetting - ); - } ); - updateBlockAttributes( blocks, changedAttributes, true ); - const linkToText = [ ...linkOptions ].find( - ( linkType ) => linkType.value === value - ); - - createSuccessNotice( - sprintf( - /* translators: %s: image size settings */ - __( 'All gallery image links updated to: %s' ), - linkToText.noticeText - ), - { - id: 'gallery-attributes-linkTo', - type: 'snackbar', - } - ); - } - - function setColumnsNumber( value ) { - setAttributes( { columns: value } ); - } - - function toggleImageCrop() { - setAttributes( { imageCrop: ! imageCrop } ); - } - - function toggleRandomOrder() { - setAttributes( { randomOrder: ! randomOrder } ); - } - - function toggleOpenInNewTab( openInNewTab ) { - const newLinkTarget = openInNewTab ? '_blank' : undefined; - setAttributes( { linkTarget: newLinkTarget } ); - const changedAttributes = {}; - const blocks = []; - getBlock( clientId ).innerBlocks.forEach( ( block ) => { - blocks.push( block.clientId ); - changedAttributes[ block.clientId ] = getUpdatedLinkTargetSettings( - newLinkTarget, - block.attributes - ); - } ); - updateBlockAttributes( blocks, changedAttributes, true ); - const noticeText = openInNewTab - ? __( 'All gallery images updated to open in new tab' ) - : __( 'All gallery images updated to not open in new tab' ); - createSuccessNotice( noticeText, { - id: 'gallery-attributes-openInNewTab', - type: 'snackbar', - } ); - } - - function updateImagesSize( newSizeSlug ) { - setAttributes( { sizeSlug: newSizeSlug } ); - const changedAttributes = {}; - const blocks = []; - getBlock( clientId ).innerBlocks.forEach( ( block ) => { - blocks.push( block.clientId ); - const image = block.attributes.id - ? imageData.find( ( { id } ) => id === block.attributes.id ) - : null; - changedAttributes[ block.clientId ] = getImageSizeAttributes( - image, - newSizeSlug - ); - } ); - updateBlockAttributes( blocks, changedAttributes, true ); - const imageSize = imageSizeOptions.find( - ( size ) => size.value === newSizeSlug - ); - - createSuccessNotice( - sprintf( - /* translators: %s: image size settings */ - __( 'All gallery image sizes updated to: %s' ), - imageSize.label - ), - { - id: 'gallery-attributes-sizeSlug', - type: 'snackbar', - } - ); - } - - 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 hasImageIds = hasImages && images.some( ( image ) => !! image.id ); - const imagesUploading = images.some( ( img ) => - ! Platform.isNative - ? ! img.id && img.url?.indexOf( 'blob:' ) === 0 - : img.url?.indexOf( 'file:' ) === 0 - ); - - // MediaPlaceholder props are different between web and native hence, we provide a platform-specific set. - const mediaPlaceholderProps = Platform.select( { - web: { - addToGallery: false, - disableMediaButtons: imagesUploading, - value: {}, - }, - native: { - addToGallery: hasImageIds, - isAppender: hasImages, - disableMediaButtons: - ( hasImages && ! isSelected ) || imagesUploading, - value: hasImageIds ? images : {}, - autoOpenMediaUpload: - ! hasImages && isSelected && blockWasJustInserted, - onFocus, - }, - } ); - const mediaPlaceholder = ( - - ); - - const blockProps = useBlockProps( { - className: clsx( className, 'has-nested-images' ), - } ); - - const nativeInnerBlockProps = Platform.isNative && { - marginHorizontal: 0, - marginVertical: 0, - }; - - const innerBlocksProps = useInnerBlocksProps( blockProps, { - defaultBlock: DEFAULT_BLOCK, - directInsert: true, - orientation: 'horizontal', - renderAppender: false, - ...nativeInnerBlockProps, - } ); - - if ( ! hasImages ) { - return ( - - { innerBlocksProps.children } - { mediaPlaceholder } - - ); - } - - const hasLinkTo = linkTo && linkTo !== 'none'; - - return ( - <> - - - { images.length > 1 && ( - - ) } - { imageSizeOptions?.length > 0 && ( - - ) } - { Platform.isNative ? ( - - ) : null } - - - { hasLinkTo && ( - - ) } - { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( - - - { __( 'Resolution' ) } - - - - { __( 'Loading options…' ) } - - - ) } - - - { Platform.isWeb ? ( - - - { ( { onClose } ) => ( - - { linkOptions.map( ( linkItem ) => { - const isOptionSelected = - linkTo === linkItem.value; - return ( - { - setLinkTo( linkItem.value ); - onClose(); - } } - role="menuitemradio" - info={ linkItem.infoText } - > - { linkItem.label } - - ); - } ) } - - ) } - - - ) : null } - { Platform.isWeb && ( - <> - { ! multiGallerySelection && ( - - image.id ) - .map( ( image ) => image.id ) } - addToGallery={ hasImageIds } - /> - - ) } - - - ) } - - - ); -}