diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 38a93552bcbef..a0acea3ced683 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -846,6 +846,10 @@ _Returns_ - `string|false`: Block Template Lock +### getUnsyncedPatterns + +Undocumented declaration. + ### hasBlockMovingClientId Returns whether block moving mode is enabled. diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index 084c9c1d7a5fb..0c3c95beb1f1b 100644 --- a/docs/reference-guides/data/data-core-blocks.md +++ b/docs/reference-guides/data/data-core-blocks.md @@ -246,6 +246,14 @@ _Returns_ - `(WPBlockVariation[]|void)`: Block variations. +### getBootstrappedBlockType + +Undocumented declaration. + +### getBootstrappedBlockTypes + +Undocumented declaration. + ### getCategories Returns all the available block categories. diff --git a/lib/blocks.php b/lib/blocks.php index 698b5d6873748..fc20c98d37f6e 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -227,6 +227,13 @@ function gutenberg_deregister_core_block_and_assets( $block_name ) { } } } + if ( ! empty( $block_type->editor_script_handles ) ) { + foreach ( $block_type->editor_script_handles as $editor_script_handle ) { + if ( str_starts_with( $editor_script_handle, 'wp-block-' ) ) { + wp_deregister_script( $editor_script_handle ); + } + } + } $registry->unregister( $block_name ); } } diff --git a/lib/init.php b/lib/init.php index 71b6a276c60f3..3fa7fdbaece97 100644 --- a/lib/init.php +++ b/lib/init.php @@ -57,3 +57,55 @@ function gutenberg_menu() { ); } add_action( 'admin_menu', 'gutenberg_menu', 9 ); + +// disable loading and enqueuing block editor scripts and styles +add_filter( 'should_load_block_editor_scripts_and_styles', '__return_false', 11 ); + +function get_block_importmap() { + $block_registry = WP_Block_Type_Registry::get_instance(); + $scripts = wp_scripts(); + $styles = wp_styles(); + $blocks = array(); + + foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { + $imports = array(); + if ( isset( $block_type->editor_script_handles ) ) { + foreach ( $block_type->editor_script_handles as $handle ) { + $spec = $scripts->registered[ $handle ]; + $imports[] = array( + 'type' => 'script', + 'handle' => $spec->handle, + 'src' => $spec->src, + 'ver' => $spec->ver + ); + } + } + if ( isset( $block_type->editor_style_handles ) ) { + foreach ( $block_type->editor_style_handles as $handle ) { + if ( ! isset( $styles->registered[ $handle ] ) ) { + continue; + } + $spec = $styles->registered[ $handle ]; + $imports[] = array( + 'type' => 'style', + 'handle' => $spec->handle, + 'src' => $spec->src, + 'ver' => $spec->ver + ); + } + } + if ( ! empty( $imports ) ) { + $blocks[ $block_name ] = $imports; + } + } + + return $blocks; +} + +function emit_importmap() { + wp_register_script( 'wp-importmap', ''); + wp_add_inline_script( 'wp-importmap', 'wp.importmap = ' . wp_json_encode( get_block_importmap() ) . ';' ); + wp_enqueue_script('wp-importmap'); +} + +add_action( 'enqueue_block_editor_assets', 'emit_importmap' ); \ No newline at end of file diff --git a/packages/block-editor/src/autocompleters/block.js b/packages/block-editor/src/autocompleters/block.js index bc06c9de5aaaf..e3539d7ba2e76 100644 --- a/packages/block-editor/src/autocompleters/block.js +++ b/packages/block-editor/src/autocompleters/block.js @@ -116,7 +116,7 @@ function createBlockCompleter() { allowContext( before, after ) { return ! ( /\S/.test( before ) || /\S/.test( after ) ); }, - getOptionCompletion( inserterItem ) { + async getOptionCompletion( inserterItem ) { const { name, initialAttributes, @@ -124,12 +124,11 @@ function createBlockCompleter() { syncStatus, content, } = inserterItem; - return { action: 'replace', value: syncStatus === 'unsynced' - ? parse( content, { + ? await parse( content, { __unstableSkipMigrationLogs: true, } ) : createBlock( diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index 5d942d2b25c70..2f82cda208619 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -103,7 +103,7 @@ export default function BlockActions( { selectBlock( clientIds[ 0 ] ); setBlockMovingClientId( clientIds[ 0 ] ); }, - onGroup() { + async onGroup() { if ( ! blocks.length ) { return; } @@ -111,7 +111,10 @@ export default function BlockActions( { const groupingBlockName = getGroupingBlockName(); // Activate the `transform` on `core/group` which does the conversion. - const newBlocks = switchToBlockType( blocks, groupingBlockName ); + const newBlocks = await switchToBlockType( + blocks, + groupingBlockName + ); if ( ! newBlocks ) { return; diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index b38dcf3ef1f2e..d5c9b41987967 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -391,7 +391,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { ) { removeBlock( _clientId ); } else { - registry.batch( () => { + registry.batch( async () => { if ( canInsertBlockType( getBlockName( firstClientId ), @@ -405,7 +405,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { getBlockIndex( _clientId ) ); } else { - const replacement = switchToBlockType( + const replacement = await switchToBlockType( getBlock( firstClientId ), getDefaultBlockName() ); diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index 70a66c445f58f..de7e9f2181574 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -482,7 +482,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { ) { removeBlock( _clientId ); } else { - registry.batch( () => { + registry.batch( async () => { if ( canInsertBlockType( getBlockName( firstClientId ), @@ -496,7 +496,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { getBlockIndex( _clientId ) ); } else { - const replacement = switchToBlockType( + const replacement = await switchToBlockType( getBlock( firstClientId ), getDefaultBlockName() ); diff --git a/packages/block-editor/src/components/block-styles/use-styles-for-block.js b/packages/block-editor/src/components/block-styles/use-styles-for-block.js index 6d73dca557b05..d89557be05864 100644 --- a/packages/block-editor/src/components/block-styles/use-styles-for-block.js +++ b/packages/block-editor/src/components/block-styles/use-styles-for-block.js @@ -24,20 +24,13 @@ import { store as blockEditorStore } from '../../store'; */ function useGenericPreviewBlock( block, type ) { return useMemo( () => { - const example = type?.example; - const blockName = type?.name; - - if ( example && blockName ) { - return getBlockFromExample( blockName, { - attributes: example.attributes, - innerBlocks: example.innerBlocks, - } ); + if ( type && type.blockName && type.example ) { + return getBlockFromExample( type.blockName, type.example ); } - if ( block ) { return cloneBlock( block ); } - }, [ type?.example ? block?.name : block, type ] ); + }, [ type, type?.example ? block?.name : block ] ); } /** diff --git a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js index 033201d7facad..f89b1605391bc 100644 --- a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js @@ -7,7 +7,7 @@ import { getBlockMenuDefaultClassName, switchToBlockType, } from '@wordpress/blocks'; -import { useState, useMemo } from '@wordpress/element'; +import { useEffect, useState, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -63,6 +63,21 @@ function useGroupedTransforms( possibleBlockTransformations ) { return transformations; } +function Preview( { blocks, transformName } ) { + const [ switched, setSwitched ] = useState( null ); + useEffect( () => { + switchToBlockType( blocks, transformName ).then( ( result ) => + setSwitched( result ) + ); + }, [ blocks, transformName ] ); + + if ( ! switched ) { + return null; + } + + return ; +} + const BlockTransformationsMenu = ( { className, possibleBlockTransformations, @@ -91,11 +106,10 @@ const BlockTransformationsMenu = ( { <> { hoveredTransformItemName && ( - ) } { !! possibleBlockVariationTransformations?.length && ( diff --git a/packages/block-editor/src/components/block-switcher/block-transformations-menu.native.js b/packages/block-editor/src/components/block-switcher/block-transformations-menu.native.js index 33952378f601f..a9ae04bcf90b3 100644 --- a/packages/block-editor/src/components/block-switcher/block-transformations-menu.native.js +++ b/packages/block-editor/src/components/block-switcher/block-transformations-menu.native.js @@ -55,10 +55,10 @@ const BlockTransformationsMenu = ( { const getAnchor = () => anchorNodeRef ? findNodeHandle( anchorNodeRef ) : undefined; - function onPickerSelect( value ) { + async function onPickerSelect( value ) { replaceBlocks( selectedBlockClientId, - switchToBlockType( selectedBlock, value ) + await switchToBlockType( selectedBlock, value ) ); const selectedItem = pickerOptions().find( diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 0960dc87eaa49..136e987382e4d 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -13,9 +13,11 @@ import { store as blocksStore, isReusableBlock, isTemplatePart, + getTransformItemsForBlocks, } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; import { copy } from '@wordpress/icons'; +import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -33,56 +35,58 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const { replaceBlocks, multiSelect, updateBlockAttributes } = useDispatch( blockEditorStore ); const blockInformation = useBlockDisplayInformation( blocks[ 0 ].clientId ); - const { - possibleBlockTransformations, - canRemove, - hasBlockStyles, - icon, - patterns, - } = useSelect( - ( select ) => { - const { - getBlockRootClientId, - getBlockTransformItems, - __experimentalGetPatternTransformItems, - canRemoveBlocks, - } = select( blockEditorStore ); - const { getBlockStyles, getBlockType } = select( blocksStore ); - const rootClientId = getBlockRootClientId( - Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds - ); - const [ { name: firstBlockName } ] = blocks; - const _isSingleBlockSelected = blocks.length === 1; - const styles = - _isSingleBlockSelected && getBlockStyles( firstBlockName ); - let _icon; - if ( _isSingleBlockSelected ) { - _icon = blockInformation?.icon; // Take into account active block variations. - } else { - const isSelectionOfSameType = - new Set( blocks.map( ( { name } ) => name ) ).size === 1; - // When selection consists of blocks of multiple types, display an - // appropriate icon to communicate the non-uniformity. - _icon = isSelectionOfSameType - ? getBlockType( firstBlockName )?.icon - : copy; - } - return { - possibleBlockTransformations: getBlockTransformItems( - blocks, - rootClientId - ), - canRemove: canRemoveBlocks( clientIds, rootClientId ), - hasBlockStyles: !! styles?.length, - icon: _icon, - patterns: __experimentalGetPatternTransformItems( - blocks, - rootClientId - ), - }; - }, - [ clientIds, blocks, blockInformation?.icon ] - ); + const { blockTransformations, canRemove, hasBlockStyles, icon, patterns } = + useSelect( + ( select ) => { + const { + getBlockRootClientId, + getBlockTransformItems, + __experimentalGetPatternTransformItems, + canRemoveBlocks, + } = select( blockEditorStore ); + const { getBlockStyles, getBlockType } = select( blocksStore ); + const rootClientId = getBlockRootClientId( + Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds + ); + const [ { name: firstBlockName } ] = blocks; + const _isSingleBlockSelected = blocks.length === 1; + const styles = + _isSingleBlockSelected && getBlockStyles( firstBlockName ); + let _icon; + if ( _isSingleBlockSelected ) { + _icon = blockInformation?.icon; // Take into account active block variations. + } else { + const isSelectionOfSameType = + new Set( blocks.map( ( { name } ) => name ) ).size === + 1; + // When selection consists of blocks of multiple types, display an + // appropriate icon to communicate the non-uniformity. + _icon = isSelectionOfSameType + ? getBlockType( firstBlockName )?.icon + : copy; + } + return { + blockTransformations: + getBlockTransformItems( rootClientId ), + canRemove: canRemoveBlocks( clientIds, rootClientId ), + hasBlockStyles: !! styles?.length, + icon: _icon, + patterns: __experimentalGetPatternTransformItems( + blocks, + rootClientId + ), + }; + }, + [ clientIds, blocks, blockInformation?.icon ] + ); + + const [ possibleBlockTransformations, setPossibleBlockTransformations ] = + useState( null ); + useEffect( () => { + getTransformItemsForBlocks( blockTransformations, blocks ).then( + ( result ) => setPossibleBlockTransformations( result ) + ); + }, [ blockTransformations, blocks ] ); const blockVariationTransformations = useBlockVariationTransforms( { clientIds, @@ -108,8 +112,8 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { } // Simple block tranformation based on the `Block Transforms` API. - function onBlockTransform( name ) { - const newBlocks = switchToBlockType( blocks, name ); + async function onBlockTransform( name ) { + const newBlocks = await switchToBlockType( blocks, name ); replaceBlocks( clientIds, newBlocks ); selectForMultipleBlocks( newBlocks ); } @@ -134,7 +138,7 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { * by allowing to exclude blocks from wildcard transformations. */ const hasPossibleBlockTransformations = - !! possibleBlockTransformations.length && canRemove && ! isTemplate; + possibleBlockTransformations?.length && canRemove && ! isTemplate; const hasPossibleBlockVariationTransformations = !! blockVariationTransformations?.length; const hasPatternTransformation = !! patterns?.length && canRemove; diff --git a/packages/block-editor/src/components/convert-to-group-buttons/index.js b/packages/block-editor/src/components/convert-to-group-buttons/index.js index c2bb3fb25b845..1053f76ba7efe 100644 --- a/packages/block-editor/src/components/convert-to-group-buttons/index.js +++ b/packages/block-editor/src/components/convert-to-group-buttons/index.js @@ -23,9 +23,9 @@ function ConvertToGroupButton( { onClose = () => {}, } ) { const { replaceBlocks } = useDispatch( blockEditorStore ); - const onConvertToGroup = () => { + const onConvertToGroup = async () => { // Activate the `transform` on the Grouping Block which does the conversion. - const newBlocks = switchToBlockType( + const newBlocks = await switchToBlockType( blocksSelection, groupingBlockName ); diff --git a/packages/block-editor/src/components/convert-to-group-buttons/toolbar.js b/packages/block-editor/src/components/convert-to-group-buttons/toolbar.js index 38405fe3ee6ab..cde6fe11daec9 100644 --- a/packages/block-editor/src/components/convert-to-group-buttons/toolbar.js +++ b/packages/block-editor/src/components/convert-to-group-buttons/toolbar.js @@ -40,16 +40,12 @@ function BlockGroupToolbar() { [ clientIds, groupingBlockName ] ); - const onConvertToGroup = ( layout ) => { - const newBlocks = switchToBlockType( + const onConvertToGroup = async ( layout ) => { + const newBlocks = await switchToBlockType( blocksSelection, groupingBlockName ); - if ( typeof layout !== 'string' ) { - layout = 'group'; - } - if ( newBlocks && newBlocks.length > 0 ) { // Because the block is not in the store yet we can't use // updateBlockAttributes so need to manually update attributes. @@ -69,10 +65,10 @@ function BlockGroupToolbar() { return null; } - const canInsertRow = !! variations.find( + const canInsertRow = variations?.some( ( { name } ) => name === 'group-row' ); - const canInsertStack = !! variations.find( + const canInsertStack = variations?.some( ( { name } ) => name === 'group-stack' ); @@ -81,7 +77,7 @@ function BlockGroupToolbar() { onConvertToGroup( 'group' ) } /> { canInsertRow && ( ( { + item: select( blocksStore ).getBlockType( name ), + } ), + [ name ] + ); + + const itemIconStyle = item?.icon ? { backgroundColor: item.icon.background, color: item.icon.foreground, } : {}; + const blocks = useMemo( () => { + if ( ! item ) { + return []; + } return [ createBlock( item.name, @@ -47,7 +61,11 @@ function InserterListItem( { createBlocksFromInnerBlocksTemplate( item.innerBlocks ) ), ]; - }, [ item.name, item.initialAttributes, item.initialAttributes ] ); + }, [ item ] ); + + if ( ! item ) { + return null; + } const isSynced = ( isReusableBlock( item ) && item.syncStatus !== 'unsynced' ) || @@ -55,7 +73,7 @@ function InserterListItem( { return ( @@ -89,7 +107,7 @@ function InserterListItem( { 'block-editor-block-types-list__item', className ) } - disabled={ item.isDisabled } + disabled={ isDisabled } onClick={ ( event ) => { event.preventDefault(); onSelect( diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 2faa503632783..b618e287f9d69 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -23,34 +23,35 @@ import { store as blockEditorStore } from '../../../store'; * @return {Array} Returns the block types state. (block types, categories, collections, onSelect handler) */ const useBlockTypesState = ( rootClientId, onInsert ) => { - const { categories, collections, items } = useSelect( - ( select ) => { - const { getInserterItems } = select( blockEditorStore ); - const { getCategories, getCollections } = select( blocksStore ); - - return { - categories: getCategories(), - collections: getCollections(), - items: getInserterItems( rootClientId ), - }; - }, + const { items } = useSelect( + ( select ) => ( { + items: select( blockEditorStore ).getInserterItems( rootClientId ), + } ), [ rootClientId ] ); + const { categories, collections } = useSelect( ( select ) => { + const { getCategories, getCollections } = select( blocksStore ); + + return { + categories: getCategories(), + collections: getCollections(), + }; + } ); + const onSelectItem = useCallback( - ( - { name, initialAttributes, innerBlocks, syncStatus, content }, - shouldFocusBlock - ) => { + async ( item, shouldFocusBlock ) => { const insertedBlock = - syncStatus === 'unsynced' - ? parse( content, { + item.syncStatus === 'unsynced' + ? await parse( item.content, { __unstableSkipMigrationLogs: true, } ) : createBlock( - name, - initialAttributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) + item.name, + item.initialAttributes, + createBlocksFromInnerBlocksTemplate( + item.innerBlocks + ) ); onInsert( insertedBlock, undefined, shouldFocusBlock ); diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index ae4f6875e4c9b..69beb31e8ffdb 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -208,13 +208,17 @@ function InserterMenu( }, } ) ); + const readyToShow = showPatterns !== null; + const showPatternPanel = selectedTab === 'patterns' && ! delayedFilterValue && selectedPatternCategory; + const showAsTabs = ! delayedFilterValue && ( showPatterns || hasReusableBlocks || showMedia ); + const showMediaPanel = selectedTab === 'media' && ! delayedFilterValue && @@ -247,7 +251,7 @@ function InserterMenu( placeholder={ __( 'Search' ) } ref={ searchRef } /> - { !! delayedFilterValue && ( + { readyToShow && delayedFilterValue && (
) } - { showAsTabs && ( + { readyToShow && showAsTabs && ( ) } - { ! delayedFilterValue && ! showAsTabs && ( + { readyToShow && ! delayedFilterValue && ! showAsTabs && (
{ blocksTab }
diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 540b51a4757e0..9edc1d7001f83 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -68,7 +68,7 @@ export default function QuickInserter( { ); const showPatterns = - patterns.length && ( !! filterValue || prioritizePatterns ); + patterns?.length && ( !! filterValue || prioritizePatterns ); const showSearch = ( showPatterns && patterns.length > SEARCH_THRESHOLD ) || blockTypes.length > SEARCH_THRESHOLD; diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 41b1d3fe37c56..a1f588b25cdcb 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -290,7 +290,7 @@ export function MediaPlaceholder( { } async function onHTMLDrop( HTML ) { - const blocks = pasteHandler( { HTML } ); + const blocks = await pasteHandler( { HTML } ); return await handleBlocksDrop( blocks ); } diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index acadfb24a7221..911c5809d3268 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -300,16 +300,17 @@ export function RichTextWrapper( ); const onEnter = useCallback( - ( { value, onChange, shiftKey } ) => { + async ( { value, onChange, shiftKey } ) => { const canSplit = onReplace && onSplit; if ( onReplace ) { - const transforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'enter' + const transforms = await getBlockTransforms( 'from' ); + const enterTransforms = transforms.filter( + ( t ) => t.type === 'enter' + ); + const transformation = findTransform( enterTransforms, ( t ) => + t.regExp.test( value.text ) ); - const transformation = findTransform( transforms, ( item ) => { - return item.regExp.test( value.text ); - } ); if ( transformation ) { onReplace( [ @@ -364,7 +365,7 @@ export function RichTextWrapper( ); const onPaste = useCallback( - ( { + async ( { value, onChange, html, @@ -394,7 +395,7 @@ export function RichTextWrapper( // Only process file if no HTML is present. // Note: a pasted file may have the URL as plain text. if ( files && files.length && ! html ) { - const content = pasteHandler( { + const content = await pasteHandler( { HTML: filePasteHandler( files ), mode: 'BLOCKS', tagName, @@ -433,7 +434,7 @@ export function RichTextWrapper( mode = 'BLOCKS'; } - const content = pasteHandler( { + const content = await pasteHandler( { HTML: html, plainText, mode, @@ -486,7 +487,7 @@ export function RichTextWrapper( ); const inputRule = useCallback( - ( value, valueToFormat ) => { + async ( value, valueToFormat ) => { if ( ! onReplace ) { return; } @@ -503,14 +504,12 @@ export function RichTextWrapper( } const trimmedTextBefore = text.slice( 0, startPosition ).trim(); - const prefixTransforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'prefix' - ); + const prefixTransforms = ( + await getBlockTransforms( 'from' ) + ).filter( ( { type } ) => type === 'prefix' ); const transformation = findTransform( prefixTransforms, - ( { prefix } ) => { - return trimmedTextBefore === prefix; - } + ( { prefix } ) => trimmedTextBefore === prefix ); if ( ! transformation ) { diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js index 40d4f9be4759f..6bb6db1d3c870 100644 --- a/packages/block-editor/src/components/rich-text/use-enter.js +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -20,7 +20,7 @@ export function useEnter( props ) { const propsRef = useRef( props ); propsRef.current = props; return useRefEffect( ( element ) => { - function onKeyDown( event ) { + async function onKeyDown( event ) { if ( event.defaultPrevented ) { return; } @@ -47,9 +47,9 @@ export function useEnter( props ) { const canSplit = onReplace && onSplit; if ( onReplace ) { - const transforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'enter' - ); + const transforms = ( + await getBlockTransforms( 'from' ) + ).filter( ( { type } ) => type === 'enter' ); const transformation = findTransform( transforms, ( item ) => { return item.regExp.test( _value.text ); } ); diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 5640a85f5f269..1e7b3986c5198 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -83,7 +83,7 @@ export function useInputRules( props ) { const propsRef = useRef( props ); propsRef.current = props; return useRefEffect( ( element ) => { - function inputRule() { + async function inputRule() { const { getValue, onReplace, selectionChange } = propsRef.current; if ( ! onReplace ) { @@ -102,14 +102,12 @@ export function useInputRules( props ) { } const trimmedTextBefore = text.slice( 0, start ).trim(); - const prefixTransforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'prefix' - ); + const prefixTransforms = ( + await getBlockTransforms( 'from' ) + ).filter( ( { type } ) => type === 'prefix' ); const transformation = findTransform( prefixTransforms, - ( { prefix } ) => { - return trimmedTextBefore === prefix; - } + ( { prefix } ) => trimmedTextBefore === prefix ); if ( ! transformation ) { @@ -128,7 +126,7 @@ export function useInputRules( props ) { return true; } - function onInput( event ) { + async function onInput( event ) { const { inputType, type } = event; const { getValue, @@ -142,8 +140,8 @@ export function useInputRules( props ) { return; } - if ( __unstableAllowPrefixTransformations && inputRule ) { - if ( inputRule() ) return; + if ( __unstableAllowPrefixTransformations ) { + if ( await inputRule() ) return; } const value = getValue(); diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index 1302e2d0dce46..78382eb8d12b1 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -24,7 +24,7 @@ export function usePasteHandler( props ) { const propsRef = useRef( props ); propsRef.current = props; return useRefEffect( ( element ) => { - function _onPaste( event ) { + async function _onPaste( event ) { const { isSelected, disableFormats, @@ -78,7 +78,7 @@ export function usePasteHandler( props ) { // eslint-disable-next-line no-console window.console.log( 'Received items:\n\n', files ); - const fromTransforms = getBlockTransforms( 'from' ); + const fromTransforms = await getBlockTransforms( 'from' ); const blocks = files .reduce( ( accumulator, file ) => { const transformation = findTransform( @@ -127,7 +127,7 @@ export function usePasteHandler( props ) { mode = 'BLOCKS'; } - const content = pasteHandler( { + const content = await pasteHandler( { HTML: html, plainText, mode, diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index ab0da8ad99e2a..134eb4adf9e87 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -149,13 +149,13 @@ export function onFilesDrop( canInsertBlockType, insertOrReplaceBlocks ) { - return ( files ) => { + return async ( files ) => { if ( ! hasUploadPermissions ) { return; } const transformation = findTransform( - getBlockTransforms( 'from' ), + await getBlockTransforms( 'from' ), ( transform ) => transform.type === 'files' && canInsertBlockType( transform.blockName, targetRootClientId ) && @@ -186,8 +186,8 @@ export function onHTMLDrop( targetBlockIndex, insertOrReplaceBlocks ) { - return ( HTML ) => { - const blocks = pasteHandler( { HTML, mode: 'BLOCKS' } ); + return async ( HTML ) => { + const blocks = await pasteHandler( { HTML, mode: 'BLOCKS' } ); if ( blocks.length ) { insertOrReplaceBlocks( blocks ); diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js index 5b78d2f8656b6..f8a2652b694b4 100644 --- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js +++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js @@ -46,7 +46,7 @@ export default function useClipboardHandler() { const notifyCopy = useNotifyCopy(); return useRefEffect( ( node ) => { - function handler( event ) { + async function handler( event ) { if ( event.defaultPrevented ) { // This was likely already handled in rich-text/use-paste-handler.js. return; @@ -157,7 +157,7 @@ export default function useClipboardHandler() { let blocks = []; if ( files.length ) { - const fromTransforms = getBlockTransforms( 'from' ); + const fromTransforms = await getBlockTransforms( 'from' ); blocks = files .reduce( ( accumulator, file ) => { const transformation = findTransform( @@ -175,7 +175,7 @@ export default function useClipboardHandler() { }, [] ) .flat(); } else { - blocks = pasteHandler( { + blocks = await pasteHandler( { HTML: html, plainText, mode: 'BLOCKS', diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index b21436161cb8c..3df8eddaf5b07 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -703,7 +703,7 @@ export const synchronizeTemplate = */ export const __unstableDeleteSelection = ( isForward ) => - ( { registry, select, dispatch } ) => { + async ( { registry, select, dispatch } ) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); @@ -784,7 +784,10 @@ export const __unstableDeleteSelection = const blocksWithTheSameType = blockA.name === blockB.name ? [ followingBlock ] - : switchToBlockType( followingBlock, targetBlockType.name ); + : await switchToBlockType( + followingBlock, + targetBlockType.name + ); // If the block types can not match, do nothing if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { @@ -957,7 +960,7 @@ export const __unstableExpandSelection = */ export const mergeBlocks = ( firstBlockClientId, secondBlockClientId ) => - ( { registry, select, dispatch } ) => { + async ( { registry, select, dispatch } ) => { const blocks = [ firstBlockClientId, secondBlockClientId ]; dispatch( { type: 'MERGE_BLOCKS', blocks } ); @@ -975,7 +978,7 @@ export const mergeBlocks = ) { // If there's no merge function defined, attempt merging inner // blocks. - const blocksWithTheSameType = switchToBlockType( + const blocksWithTheSameType = await switchToBlockType( blockB, blockAType.name ); @@ -1112,7 +1115,7 @@ export const mergeBlocks = const blocksWithTheSameType = blockA.name === blockB.name ? [ cloneB ] - : switchToBlockType( cloneB, blockA.name ); + : await switchToBlockType( cloneB, blockA.name ); // If the block types can not match, do nothing. if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 0bcc00cb5f6ae..fc999278ed68c 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -11,6 +11,7 @@ import * as selectors from './selectors'; import * as privateActions from './private-actions'; import * as privateSelectors from './private-selectors'; import * as actions from './actions'; +import * as resolvers from './resolvers'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -23,6 +24,7 @@ export const storeConfig = { reducer, selectors, actions, + resolvers, }; /** diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 5319a3b255365..6572277217eb0 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1991,6 +1991,17 @@ export function lastFocus( state = false, action ) { return state; } +export function parsedPatterns( state = {}, action ) { + switch ( action.type ) { + case 'SET_PARSED_PATTERN': + return { + ...state, + [ action.patternName ]: action.parsedPattern, + }; + } + return state; +} + const combinedReducers = combineReducers( { blocks, isTyping, @@ -2020,6 +2031,7 @@ const combinedReducers = combineReducers( { blockRemovalRules, openedBlockSettingsMenu, registeredInserterMediaCategories, + parsedPatterns, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/resolvers.js b/packages/block-editor/src/store/resolvers.js new file mode 100644 index 0000000000000..f072b6689706a --- /dev/null +++ b/packages/block-editor/src/store/resolvers.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { parse } from '@wordpress/blocks'; + +export const __experimentalGetParsedPattern = + ( patternName ) => + async ( { select, dispatch } ) => { + const patterns = select( + ( state ) => state.root.settings.__experimentalBlockPatterns + ); + const unsyncedPatterns = select.getUnsyncedPatterns(); + + const pattern = [ ...patterns, ...unsyncedPatterns ].find( + ( { name } ) => name === patternName + ); + + if ( ! pattern ) { + return; + } + + const blocks = await parse( pattern.content, { + __unstableSkipMigrationLogs: true, + } ); + + const parsedPattern = { ...pattern, blocks }; + + dispatch( { + type: 'SET_PARSED_PATTERN', + patternName: pattern.name, + parsedPattern, + } ); + }; + +export const __experimentalGetAllowedPatterns = + () => + async ( { select, dispatch } ) => { + const patterns = select( + ( state ) => state.root.settings.__experimentalBlockPatterns + ); + const unsyncedPatterns = select.getUnsyncedPatterns(); + + const inserterPatterns = [ ...patterns, ...unsyncedPatterns ].filter( + ( { inserter = true } ) => inserter + ); + + for ( const pattern of inserterPatterns ) { + const blocks = await parse( pattern.content, { + __unstableSkipMigrationLogs: true, + } ); + + const parsedPattern = { ...pattern, blocks }; + + dispatch( { + type: 'SET_PARSED_PATTERN', + patternName: pattern.name, + parsedPattern, + } ); + } + }; + +__experimentalGetAllowedPatterns.shouldInvalidate = ( action ) => { + return ( + action.type === 'UPDATE_SETTINGS' && + action.settings.__experimentalBlockPatterns + ); +}; + +export const getPatternsByBlockTypes = __experimentalGetAllowedPatterns; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index b6d455333c7a5..3eb7ccd25866a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -8,10 +8,10 @@ import createSelector from 'rememo'; */ import { getBlockType, - getBlockTypes, + getBootstrappedBlockType, + getBootstrappedBlockTypes, getBlockVariations, hasBlockSupport, - getPossibleBlockTransformations, parse, switchToBlockType, store as blocksStore, @@ -1508,7 +1508,7 @@ const canInsertBlockTypeUnmemoized = ( blockType = blockName; blockName = blockType.name; } else { - blockType = getBlockType( blockName ); + blockType = getBootstrappedBlockType( blockName ); } if ( ! blockType ) { return false; @@ -1975,7 +1975,7 @@ export const getInserterItems = createSelector( buildScope: 'inserter', } ); - const blockTypeInserterItems = getBlockTypes() + const blockTypeInserterItems = getBootstrappedBlockTypes() .filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ) @@ -1987,10 +1987,9 @@ export const getInserterItems = createSelector( if ( ! variations.some( ( { isDefault } ) => isDefault ) ) { accumulator.push( item ); } - if ( variations.length ) { - const variationMapper = getItemFromVariation( state, item ); - accumulator.push( ...variations.map( variationMapper ) ); - } + accumulator.push( + ...variations.map( getItemFromVariation( state, item ) ) + ); return accumulator; }, [] ); @@ -1999,18 +1998,10 @@ export const getInserterItems = createSelector( // the core blocks (usually by using the `init` action), // thus affecting the display order. // We don't sort reusable blocks as they are handled differently. - const groupByType = ( blocks, block ) => { - const { core, noncore } = blocks; - const type = block.name.startsWith( 'core/' ) ? core : noncore; - - type.push( block ); - return blocks; - }; - const { core: coreItems, noncore: nonCoreItems } = items.reduce( - groupByType, - { core: [], noncore: [] } + const sortedBlockTypes = orderBy( + items, + ( block ) => ! block.name.startsWith( 'core/' ) // false < true ); - const sortedBlockTypes = [ ...coreItems, ...nonCoreItems ]; return [ ...sortedBlockTypes, ...syncedPatternInserterItems ]; }, ( state, rootClientId ) => [ @@ -2021,7 +2012,7 @@ export const getInserterItems = createSelector( state.settings.allowedBlockTypes, state.settings.templateLock, getReusableBlocks( state ), - getBlockTypes(), + getBootstrappedBlockTypes(), ] ); @@ -2052,37 +2043,18 @@ export const getInserterItems = createSelector( * @property {number} frecency Heuristic that combines frequency and recency. */ export const getBlockTransformItems = createSelector( - ( state, blocks, rootClientId = null ) => { - const normalizedBlocks = Array.isArray( blocks ) ? blocks : [ blocks ]; + ( state, rootClientId = null ) => { const buildBlockTypeTransformItem = buildBlockTypeItem( state, { buildScope: 'transform', } ); - const blockTypeTransformItems = getBlockTypes() + const blockTypeTransformItems = getBootstrappedBlockTypes() .filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ) - .map( buildBlockTypeTransformItem ); - - const itemsByName = Object.fromEntries( - Object.entries( blockTypeTransformItems ).map( ( [ , value ] ) => [ - value.name, - value, - ] ) - ); + .map( buildBlockTypeTransformItem ) + .map( ( value ) => [ value.name, value ] ); - const possibleTransforms = getPossibleBlockTransformations( - normalizedBlocks - ).reduce( ( accumulator, block ) => { - if ( itemsByName[ block?.name ] ) { - accumulator.push( itemsByName[ block.name ] ); - } - return accumulator; - }, [] ); - return orderBy( - possibleTransforms, - ( block ) => itemsByName[ block.name ].frecency, - 'desc' - ); + return Object.fromEntries( blockTypeTransformItems ); }, ( state, blocks, rootClientId ) => [ state.blockListSettings[ rootClientId ], @@ -2090,7 +2062,7 @@ export const getBlockTransformItems = createSelector( state.preferences.insertUsage, state.settings.allowedBlockTypes, state.settings.templateLock, - getBlockTypes(), + getBootstrappedBlockTypes(), ] ); @@ -2102,29 +2074,21 @@ export const getBlockTransformItems = createSelector( * * @return {boolean} Items that appear in inserter. */ -export const hasInserterItems = createSelector( - ( state, rootClientId = null ) => { - const hasBlockType = getBlockTypes().some( ( blockType ) => - canIncludeBlockTypeInInserter( state, blockType, rootClientId ) - ); - if ( hasBlockType ) { - return true; - } - const hasReusableBlock = - canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) && - getReusableBlocks( state ).length > 0; +export const hasInserterItems = ( state, rootClientId = null ) => { + const hasBlockType = getBootstrappedBlockTypes().some( ( blockType ) => + canIncludeBlockTypeInInserter( state, blockType, rootClientId ) + ); - return hasReusableBlock; - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId, - state.settings.allowedBlockTypes, - state.settings.templateLock, - getReusableBlocks( state ), - getBlockTypes(), - ] -); + if ( hasBlockType ) { + return true; + } + + const hasReusableBlock = + canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) && + getReusableBlocks( state ).length > 0; + + return hasReusableBlock; +}; /** * Returns the list of allowed inserter blocks for inner blocks children. @@ -2140,9 +2104,10 @@ export const getAllowedBlocks = createSelector( return; } - const blockTypes = getBlockTypes().filter( ( blockType ) => + const blockTypes = getBootstrappedBlockTypes().filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ); + const hasReusableBlock = canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) && getReusableBlocks( state ).length > 0; @@ -2158,7 +2123,7 @@ export const getAllowedBlocks = createSelector( state.settings.allowedBlockTypes, state.settings.templateLock, getReusableBlocks( state ), - getBlockTypes(), + getBootstrappedBlockTypes(), ] ); @@ -2321,6 +2286,7 @@ export const __experimentalGetAllowedPatterns = createSelector( state.settings.templateLock, state.blockListSettings[ rootClientId ], state.blocks.byClientId.get( rootClientId ), + state.parsedPatterns, ] ); @@ -2344,6 +2310,11 @@ export const getPatternsByBlockTypes = createSelector( state, rootClientId ); + + if ( ! patterns ) { + return null; + } + const normalizedBlockNames = Array.isArray( blockNames ) ? blockNames : [ blockNames ]; diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index b74079b2b8b79..bcbe08538a26c 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -324,10 +324,10 @@ export default function Image( { const canEditImage = id && naturalWidth && naturalHeight && imageEditing; const allowCrop = ! multiImageSelection && canEditImage && ! isEditingImage; - function switchToCover() { + async function switchToCover() { replaceBlocks( clientId, - switchToBlockType( getBlock( clientId ), 'core/cover' ) + await switchToBlockType( getBlock( clientId ), 'core/cover' ) ); } diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 3f26840ad345f..a529d192986cb 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -90,9 +90,9 @@ export default function ListItemEdit( { onMerge={ onMerge } onReplace={ onReplace - ? ( blocks, ...args ) => { + ? async ( blocks, ...args ) => { onReplace( - convertToListItems( blocks ), + await convertToListItems( blocks ), ...args ); } diff --git a/packages/block-library/src/list-item/edit.native.js b/packages/block-library/src/list-item/edit.native.js index cf2e77c08d2e8..b8b3690a41fa8 100644 --- a/packages/block-library/src/list-item/edit.native.js +++ b/packages/block-library/src/list-item/edit.native.js @@ -130,12 +130,12 @@ export default function ListItemEdit( { [ clientId, onSplit ] ); const onReplaceList = useCallback( - ( blocks, ...args ) => { + async ( blocks, ...args ) => { if ( ! preventDefault.current ) { - onReplace( convertToListItems( blocks ), ...args ); + onReplace( await convertToListItems( blocks ), ...args ); } }, - [ clientId, onReplace, convertToListItems ] + [ onReplace ] ); const onLayout = useCallback( ( { nativeEvent } ) => { setContentWidth( ( prevState ) => { diff --git a/packages/block-library/src/list-item/utils.js b/packages/block-library/src/list-item/utils.js index 5e5b51a8af680..29252fb4f2a12 100644 --- a/packages/block-library/src/list-item/utils.js +++ b/packages/block-library/src/list-item/utils.js @@ -3,19 +3,19 @@ */ import { switchToBlockType } from '@wordpress/blocks'; -function convertBlockToList( block ) { - const list = switchToBlockType( block, 'core/list' ); +async function convertBlockToList( block ) { + const list = await switchToBlockType( block, 'core/list' ); if ( list ) { return list; } - const paragraph = switchToBlockType( block, 'core/paragraph' ); + const paragraph = await switchToBlockType( block, 'core/paragraph' ); if ( ! paragraph ) { return null; } - return switchToBlockType( paragraph, 'core/list' ); + return await switchToBlockType( paragraph, 'core/list' ); } -export function convertToListItems( blocks ) { +export async function convertToListItems( blocks ) { const listItems = []; for ( let block of blocks ) { @@ -23,7 +23,7 @@ export function convertToListItems( blocks ) { listItems.push( block ); } else if ( block.name === 'core/list' ) { listItems.push( ...block.innerBlocks ); - } else if ( ( block = convertBlockToList( block ) ) ) { + } else if ( ( block = await convertBlockToList( block ) ) ) { for ( const { innerBlocks } of block ) { listItems.push( ...innerBlocks ); } diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js index 3c3d91e7b0a05..3b8259e1fb7c1 100644 --- a/packages/block-library/src/navigation-link/link-ui.js +++ b/packages/block-library/src/navigation-link/link-ui.js @@ -113,10 +113,10 @@ function LinkControlTransforms( { clientId } ) { return (