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 } ) => (
-
+ ) }
+
+
+
+ >
) }
[
+ 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;
+}