2021-09-21 10:18:27 +00:00
/ * *
* External dependencies
* /
2022-12-14 15:54:59 +00:00
import { useRef , useEffect } from '@wordpress/element' ;
import { useRegistry , dispatch } from '@wordpress/data' ;
2021-09-21 10:18:27 +00:00
import {
createBlock ,
getBlockType ,
2021-11-22 12:45:48 +00:00
createBlocksFromInnerBlocksTemplate ,
2022-12-14 15:54:59 +00:00
BlockInstance ,
2021-09-21 10:18:27 +00:00
} from '@wordpress/blocks' ;
2022-12-14 15:54:59 +00:00
import type { Block , TemplateArray } from '@wordpress/blocks' ;
2021-10-15 09:48:57 +00:00
import { isEqual } from 'lodash' ;
2022-12-14 15:54:59 +00:00
import { MutableRefObject } from 'react' ;
2021-09-21 10:18:27 +00:00
2022-12-14 15:54:59 +00:00
interface LockableBlock extends Block {
attributes : {
lock ? : {
type : 'object' ;
remove? : boolean ;
move : boolean ;
default ? : {
remove? : boolean ;
move? : boolean ;
} ;
} ;
} ;
}
2021-09-21 10:18:27 +00:00
const isBlockLocked = ( {
attributes ,
} : {
2022-12-14 15:54:59 +00:00
attributes : LockableBlock [ 'attributes' ] ;
2021-09-21 10:18:27 +00:00
} ) = > Boolean ( attributes . lock ? . remove || attributes . lock ? . default ? . remove ) ;
2021-10-15 09:48:57 +00:00
/ * *
2022-12-14 15:54:59 +00:00
* 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 .
2021-10-15 09:48:57 +00:00
*
2022-12-14 15:54:59 +00:00
* @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 .
2021-10-15 09:48:57 +00:00
* /
2021-09-21 10:18:27 +00:00
export const useForcedLayout = ( {
clientId ,
2021-10-15 09:48:57 +00:00
registeredBlocks ,
defaultTemplate = [ ] ,
2021-09-21 10:18:27 +00:00
} : {
2021-10-15 09:48:57 +00:00
// Client ID of the parent block.
2021-09-21 10:18:27 +00:00
clientId : string ;
2021-10-15 09:48:57 +00:00
// 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 ;
2022-12-14 15:54:59 +00:00
} ) = > {
2021-10-15 09:48:57 +00:00
const currentRegisteredBlocks = useRef ( registeredBlocks ) ;
const currentDefaultTemplate = useRef ( defaultTemplate ) ;
2022-12-14 15:54:59 +00:00
const registry = useRegistry ( ) ;
useEffect ( ( ) = > {
const { replaceInnerBlocks } = dispatch ( 'core/block-editor' ) ;
return registry . subscribe ( ( ) = > {
const innerBlocks = registry
. select ( 'core/block-editor' )
. getBlocks ( clientId ) ;
2021-10-15 09:48:57 +00:00
2022-12-14 14:08:39 +00:00
const { innerBlocks , registeredBlockTypes } = useSelect (
( select ) = > {
return {
innerBlocks :
select ( 'core/block-editor' ) . getBlocks ( clientId ) ,
registeredBlockTypes : currentRegisteredBlocks.current.map (
( blockName ) = > getBlockType ( blockName )
) ,
} ;
} ,
[ clientId , currentRegisteredBlocks . current ]
) ;
const appendBlock = useCallback (
( block , position ) = > {
const newBlock = createBlock ( block . name ) ;
insertBlock ( newBlock , position , clientId , false ) ;
} ,
// We need to skip insertBlock here due to a cache issue in wordpress.com that causes an infinite loop, see https://github.com/Automattic/wp-calypso/issues/66092 for an expanded doc.
// eslint-disable-next-line react-hooks/exhaustive-deps
[ clientId ]
) ;
const lockedBlockTypes = useMemo (
( ) = >
registeredBlockTypes . filter (
( block : Block | undefined ) = > block && isBlockLocked ( block )
) ,
[ registeredBlockTypes ]
) as Block [ ] ;
/ * *
* If the current inner blocks differ from the registered blocks , push the differences .
* /
useLayoutEffect ( ( ) = > {
if ( ! clientId ) {
return ;
}
// If there are NO inner blocks, sync with the given template.
if (
innerBlocks . length === 0 &&
currentDefaultTemplate . current . length > 0
) {
const nextBlocks = createBlocksFromInnerBlocksTemplate (
currentDefaultTemplate . current
) ;
if ( ! isEqual ( nextBlocks , innerBlocks ) ) {
replaceInnerBlocks ( clientId , nextBlocks ) ;
return ;
}
}
// Find registered locked blocks missing from Inner Blocks and append them.
lockedBlockTypes . forEach ( ( block ) = > {
// If the locked block type is already in the layout, we can skip this one.
2022-12-14 15:54:59 +00:00
if (
innerBlocks . length === 0 &&
currentDefaultTemplate . current . length > 0
) {
const nextBlocks = createBlocksFromInnerBlocksTemplate (
currentDefaultTemplate . current
) ;
if ( ! isEqual ( nextBlocks , innerBlocks ) ) {
replaceInnerBlocks ( clientId , nextBlocks ) ;
return ;
}
}
2021-10-15 09:48:57 +00:00
2022-12-14 15:54:59 +00:00
const registeredBlockTypes = currentRegisteredBlocks . current . map (
( blockName : string ) = > getBlockType ( blockName )
) ;
2021-10-15 09:48:57 +00:00
2022-12-14 15:54:59 +00:00
const missingBlocks = getMissingBlocks (
innerBlocks ,
registeredBlockTypes
2021-10-15 09:48:57 +00:00
) ;
2022-12-14 15:54:59 +00:00
if ( missingBlocks . length === 0 ) {
2021-10-15 09:48:57 +00:00
return ;
}
2022-12-14 15:54:59 +00:00
// 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
2021-10-15 09:48:57 +00:00
) ;
2022-12-14 15:54:59 +00:00
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 ) ;
} ) ;
2021-09-21 10:18:27 +00:00
} ) ;
2022-12-14 15:54:59 +00:00
} , [ clientId , registry ] ) ;
2021-09-21 10:18:27 +00:00
} ;