woocommerce/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/use-forced-layout.ts

186 lines
5.4 KiB
TypeScript

/**
* External dependencies
*/
import { useRef, useEffect } from '@wordpress/element';
import { useRegistry, dispatch } from '@wordpress/data';
import {
createBlock,
getBlockType,
createBlocksFromInnerBlocksTemplate,
BlockInstance,
} from '@wordpress/blocks';
import type { Block, TemplateArray } from '@wordpress/blocks';
import type { MutableRefObject } from 'react';
interface LockableBlock extends Block {
attributes: {
lock?: {
type: 'object';
remove?: boolean;
move: boolean;
default?: {
remove?: boolean;
move?: boolean;
};
};
};
}
const isBlockLocked = ( {
attributes,
}: {
attributes: LockableBlock[ 'attributes' ];
} ) => Boolean( attributes.lock?.remove || attributes.lock?.default?.remove );
/**
* This hook is used to determine which blocks are missing from a block. Given the list of inner blocks of a block, we
* can check for any registered blocks that:
* a) Are locked,
* b) Have the parent set as the current block, and
* c) Are not present in the list of inner blocks.
*/
const getMissingBlocks = (
innerBlocks: BlockInstance[],
registeredBlockTypes: ( LockableBlock | undefined )[]
) => {
const lockedBlockTypes = registeredBlockTypes.filter(
( block: LockableBlock | undefined ) => block && isBlockLocked( block )
);
const missingBlocks: LockableBlock[] = [];
lockedBlockTypes.forEach( ( lockedBlock ) => {
if ( typeof lockedBlock === 'undefined' ) {
return;
}
const existingBlock = innerBlocks.find(
( block ) => block.name === lockedBlock.name
);
if ( ! existingBlock ) {
missingBlocks.push( lockedBlock );
}
} );
return missingBlocks;
};
/**
* This hook is used to determine the position that a missing block should be inserted at.
*
* @return The index to insert the missing block at.
*/
const findBlockPosition = ( {
defaultTemplatePosition,
innerBlocks,
currentDefaultTemplate,
}: {
defaultTemplatePosition: number;
innerBlocks: BlockInstance[];
currentDefaultTemplate: MutableRefObject< TemplateArray >;
} ) => {
switch ( defaultTemplatePosition ) {
case -1:
// The block is not part of the default template, so we append it to the current layout.
return innerBlocks.length;
// defaultTemplatePosition defaults to 0, so if this happens we can just return, this is because the block was
// the first block in the default layout, so we can prepend it to the current layout.
case 0:
return 0;
default:
// The new layout may have extra blocks compared to the default template, so rather than insert
// at the default position, we should append it after another default block.
const adjacentBlock =
currentDefaultTemplate.current[ defaultTemplatePosition - 1 ];
const position = innerBlocks.findIndex(
( { name: blockName } ) => blockName === adjacentBlock[ 0 ]
);
return position === -1 ? defaultTemplatePosition : position + 1;
}
};
/**
* Hook to ensure FORCED blocks are rendered in the correct place.
*/
export const useForcedLayout = ( {
clientId,
registeredBlocks,
defaultTemplate = [],
}: {
// Client ID of the parent block.
clientId: string;
// An array of registered blocks that may be forced in this particular layout.
registeredBlocks: Array< string >;
// The default template for the inner blocks in this layout.
defaultTemplate: TemplateArray;
} ) => {
const currentRegisteredBlocks = useRef( registeredBlocks );
const currentDefaultTemplate = useRef( defaultTemplate );
const registry = useRegistry();
useEffect( () => {
let templateSynced = false;
const { replaceInnerBlocks } = dispatch( 'core/block-editor' );
return registry.subscribe( () => {
const innerBlocks = registry
.select( 'core/block-editor' )
.getBlocks( clientId );
// If there are NO inner blocks, sync with the given template.
if (
innerBlocks.length === 0 &&
currentDefaultTemplate.current.length > 0 &&
! templateSynced
) {
const nextBlocks = createBlocksFromInnerBlocksTemplate(
currentDefaultTemplate.current
);
if ( nextBlocks.length !== 0 ) {
templateSynced = true;
replaceInnerBlocks( clientId, nextBlocks );
return;
}
}
const registeredBlockTypes = currentRegisteredBlocks.current.map(
( blockName: string ) => getBlockType( blockName )
);
const missingBlocks = getMissingBlocks(
innerBlocks,
registeredBlockTypes
);
if ( missingBlocks.length === 0 ) {
return;
}
// Initially set as -1, so we can skip checking the position multiple times. Later on in the map callback,
// we check where the forced blocks should be inserted. This gets set to >= 0 if we find a missing block,
// so we know we can skip calculating it.
let insertAtPosition = -1;
const blockConfig = missingBlocks.map( ( block ) => {
const defaultTemplatePosition =
currentDefaultTemplate.current.findIndex(
( [ blockName ] ) => blockName === block.name
);
const createdBlock = createBlock( block.name );
// As mentioned above, if this is not -1, this is the first time we're calculating the position, if it's
// already been calculated we can skip doing so.
if ( insertAtPosition === -1 ) {
insertAtPosition = findBlockPosition( {
defaultTemplatePosition,
innerBlocks,
currentDefaultTemplate,
} );
}
return createdBlock;
} );
registry.batch( () => {
registry
.dispatch( 'core/block-editor' )
.insertBlocks( blockConfig, insertAtPosition, clientId );
} );
}, 'core/block-editor' );
}, [ clientId, registry ] );
};