diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index 2de6d762ceadbb..3b5e1fcaca3557 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -7,12 +7,18 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; import { useEffect, useRef, useCallback } from '@wordpress/element'; +import { store as blocksStore, hasBlockSupport } from '@wordpress/blocks'; /** * Internal dependencies */ import { store as blockEditorStore } from '../store'; import { BlockControls, BlockSettingsMenuControls } from '../components'; +import { + flattenBlocks, + replaceContentsInBlocks, + areBlocksAlike, +} from './utils'; /** * External dependencies */ @@ -41,6 +47,113 @@ function StopEditingAsBlocksOnOutsideSelect( { return null; } +function filterBlocksForShuffle( blocks ) { + return flattenBlocks( blocks ).filter( + ( block ) => ! hasBlockSupport( block.name, '__experimentalLayout' ) + ); +} + +function compareFilteredBlocks( sourceBlocks, targetBlocks ) { + if ( sourceBlocks.length !== targetBlocks.length ) { + return false; + } + + const targetBlockNames = {}; + for ( const targetBlock of targetBlocks ) { + targetBlockNames[ targetBlock.name ] ??= 0; + targetBlockNames[ targetBlock.name ] += 1; + } + + for ( const sourceBlock of sourceBlocks ) { + if ( ! targetBlockNames[ sourceBlock.name ] ) { + return false; + } + + targetBlockNames[ sourceBlock.name ] -= 1; + } + + return true; +} + +function ShufflePatternsToolbarItem( { clientId } ) { + // TODO: Probably worth to add this to blocks' selectors. + const getFlattenContentBlocks = useSelect( ( select ) => { + const contentBlockNames = select( blocksStore ) + .getBlockTypes() + .filter( + ( blockType ) => + blockType.name !== 'core/list-item' && + Object.values( blockType.attributes ).some( + ( attribute ) => + attribute.__experimentalRole === 'content' + ) + ) + .map( ( blockType ) => blockType.name ); + + return ( blocks ) => + flattenBlocks( blocks ).filter( ( block ) => + contentBlockNames.includes( block.name ) + ); + }, [] ); + const { contentBlocks, patterns } = useSelect( + ( select ) => { + const blocks = + select( blockEditorStore ).getBlocksByClientId( clientId ); + const filteredBlocks = filterBlocksForShuffle( blocks ); + const _contentBlocks = getFlattenContentBlocks( blocks ); + const allPatterns = + select( blockEditorStore ).__experimentalGetAllowedPatterns(); + + return { + contentBlocks: _contentBlocks, + patterns: allPatterns + .filter( ( pattern ) => { + const filteredPatternBlocks = filterBlocksForShuffle( + pattern.blocks + ); + return compareFilteredBlocks( + filteredBlocks, + filteredPatternBlocks + ); + } ) + .filter( + ( pattern ) => + ! areBlocksAlike( blocks, pattern.blocks ) + ), + }; + }, + [ clientId, getFlattenContentBlocks ] + ); + const { replaceBlocks } = useDispatch( blockEditorStore ); + + function shuffle() { + // We're not using `Math.random` for instance ids here. + // eslint-disable-next-line no-restricted-syntax + const randomNumber = Math.floor( Math.random() * patterns.length ); + const pattern = patterns[ randomNumber ]; + const replacedPatternBlocks = replaceContentsInBlocks( + pattern.blocks, + contentBlocks + ).map( ( block ) => { + block.attributes.templateLock = 'contentOnly'; + return block; + } ); + replaceBlocks( clientId, replacedPatternBlocks ); + } + + if ( patterns.length === 0 ) { + return null; + } + + return ( + + + { __( 'Shuffle' ) } + + + ); +} + export const withBlockControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { getBlockListSettings, getSettings } = @@ -124,33 +237,42 @@ export const withBlockControls = createHigherOrderComponent( ) } { ! isEditingAsBlocks && isContentLocked && props.isSelected && ( - - { ( { onClose } ) => ( - { - __unstableMarkNextChangeAsNotPersistent(); - updateBlockAttributes( props.clientId, { - templateLock: undefined, - } ); - updateBlockListSettings( props.clientId, { - ...getBlockListSettings( + <> + + { ( { onClose } ) => ( + { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( props.clientId, { + templateLock: undefined, + } ); + updateBlockListSettings( + props.clientId, + { + ...getBlockListSettings( + props.clientId + ), + templateLock: false, + } + ); + focusModeToRevert.current = + getSettings().focusMode; + updateSettings( { focusMode: true } ); + __unstableSetTemporarilyEditingAsBlocks( props.clientId - ), - templateLock: false, - } ); - focusModeToRevert.current = - getSettings().focusMode; - updateSettings( { focusMode: true } ); - __unstableSetTemporarilyEditingAsBlocks( - props.clientId - ); - onClose(); - } } - > - { __( 'Modify' ) } - - ) } - + ); + onClose(); + } } + > + { __( 'Modify' ) } + + ) } + + + + ) } [ + block, + ...flattenBlocks( block.innerBlocks ), + ] ); +} + +export function replaceContentsInBlocks( sourceBlocks, contentBlocks ) { + const [ contentBlock, ...restContentBlocks ] = contentBlocks; + return sourceBlocks.map( ( block ) => { + if ( block.name !== contentBlock.name ) { + return createBlock( + block.name, + block.attributes, + replaceContentsInBlocks( block.innerBlocks, contentBlocks ) + ); + } + + const blockTypeAttributes = getBlockType( block.name ).attributes; + return createBlock( + block.name, + Object.keys( block.attributes ).reduce( + ( attributes, attributeName ) => { + if ( + blockTypeAttributes[ attributeName ] + .__experimentalRole === 'content' + ) { + attributes[ attributeName ] = + contentBlock.attributes[ attributeName ]; + } else { + attributes[ attributeName ] = + block.attributes[ attributeName ]; + } + return attributes; + }, + {} + ), + replaceContentsInBlocks( block.innerBlocks, restContentBlocks ) + ); + } ); +} + +export function areBlocksAlike( sourceBlocks, targetBlocks ) { + if ( sourceBlocks.length !== targetBlocks.length ) { + return false; + } + + for ( let index = 0; index < sourceBlocks.length; index += 1 ) { + const sourceBlock = sourceBlocks[ index ]; + const targetBlock = targetBlocks[ index ]; + + if ( sourceBlock.name !== targetBlock.name ) { + return false; + } + + const blockTypeAttributes = getBlockType( sourceBlock.name ).attributes; + + if ( + Object.keys( blockTypeAttributes ).some( + ( attribute ) => + attribute !== 'templateLock' && + blockTypeAttributes[ attribute ].__experimentalRole !== + 'content' && + ! isEqual( + sourceBlock.attributes[ attribute ], + targetBlock.attributes[ attribute ] + ) + ) + ) { + return false; + } + + if ( + ! areBlocksAlike( sourceBlock.innerBlocks, targetBlock.innerBlocks ) + ) { + return false; + } + } + + return true; +}