From efac9ebfc40b9e1160bdf0a88262316a0094c4d1 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 31 Aug 2023 12:51:25 +0200 Subject: [PATCH] Async block loading --- .../data/data-core-block-editor.md | 4 + .../reference-guides/data/data-core-blocks.md | 8 + lib/blocks.php | 7 + lib/init.php | 52 +++ .../block-editor/src/autocompleters/block.js | 5 +- .../src/components/block-actions/index.js | 7 +- .../src/components/block-list/block.js | 4 +- .../src/components/block-list/block.native.js | 4 +- .../block-styles/use-styles-for-block.js | 13 +- .../block-transformations-menu.js | 26 +- .../block-transformations-menu.native.js | 4 +- .../src/components/block-switcher/index.js | 110 ++--- .../convert-to-group-buttons/index.js | 4 +- .../convert-to-group-buttons/toolbar.js | 14 +- .../src/components/copy-handler/index.js | 6 +- .../components/inserter-list-item/index.js | 28 +- .../block-patterns-explorer/patterns-list.js | 25 +- .../components/inserter/block-patterns-tab.js | 70 ++- .../inserter/hooks/use-block-types-state.js | 41 +- .../src/components/inserter/menu.js | 29 +- .../src/components/inserter/quick-inserter.js | 2 +- .../src/components/inserter/tabs.js | 24 +- .../src/components/media-placeholder/index.js | 2 +- .../src/components/rich-text/index.native.js | 31 +- .../src/components/rich-text/use-enter.js | 8 +- .../components/rich-text/use-input-rules.js | 18 +- .../components/rich-text/use-paste-handler.js | 6 +- .../src/components/use-on-block-drop/index.js | 8 +- packages/block-editor/src/store/actions.js | 13 +- packages/block-editor/src/store/index.js | 2 + packages/block-editor/src/store/reducer.js | 12 + packages/block-editor/src/store/resolvers.js | 69 +++ packages/block-editor/src/store/selectors.js | 401 ++++++++---------- packages/block-library/src/image/image.js | 4 +- packages/block-library/src/list-item/edit.js | 4 +- .../src/list-item/edit.native.js | 6 +- packages/block-library/src/list-item/utils.js | 12 +- .../src/navigation-link/link-ui.js | 4 +- packages/block-library/src/page-list/edit.js | 60 ++- .../template-part/edit/utils/transformers.js | 36 +- packages/blocks/README.md | 12 + packages/blocks/src/api/factory.js | 141 +++--- packages/blocks/src/api/index.js | 3 + packages/blocks/src/api/parser/index.js | 30 +- .../api/raw-handling/get-raw-transforms.js | 5 +- packages/blocks/src/api/raw-handling/index.js | 4 +- .../src/api/raw-handling/paste-handler.js | 6 +- .../api/raw-handling/shortcode-converter.js | 14 +- packages/blocks/src/api/registration.js | 34 +- packages/blocks/src/api/utils.js | 12 +- packages/blocks/src/store/index.js | 2 + .../blocks/src/store/private-selectors.js | 12 - packages/blocks/src/store/resolvers.js | 46 ++ packages/blocks/src/store/selectors.js | 17 +- .../components/src/autocomplete/index.tsx | 7 +- packages/core-data/src/entity-provider.js | 28 +- .../src/editor/transform-block-to.ts | 7 +- .../editor/various/inserting-blocks.test.js | 3 + .../components/start-page-options/index.js | 37 +- .../src/components/visual-editor/index.js | 2 +- packages/edit-post/src/editor.js | 4 +- packages/edit-post/src/index.js | 26 +- .../src/components/style-book/index.js | 4 +- packages/edit-site/src/store/actions.js | 2 +- .../local-autosave-monitor/index.js | 4 +- test/performance/utils.js | 4 +- tools/webpack/blocks.js | 32 ++ 67 files changed, 966 insertions(+), 715 deletions(-) create mode 100644 packages/block-editor/src/store/resolvers.js create mode 100644 packages/blocks/src/store/resolvers.js diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index fbdf0c5dfd81b..780e3e54f14df 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -834,6 +834,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 f160d2a7080d3..5950ca8305d76 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -224,6 +224,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 b672bee63fc5b..f9656a5644f83 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 4bf9bb634dbf6..3bedcadd68544 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -357,7 +357,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { ) { removeBlock( _clientId ); } else { - registry.batch( () => { + registry.batch( async () => { if ( canInsertBlockType( getBlockName( firstClientId ), @@ -371,7 +371,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 405a883ed3b23..f8d4a7726c76f 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -468,7 +468,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { ) { removeBlock( _clientId ); } else { - registry.batch( () => { + registry.batch( async () => { if ( canInsertBlockType( getBlockName( firstClientId ), @@ -482,7 +482,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 51346e14b1003..1be7850235969 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, @@ -106,8 +110,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 ); } @@ -132,7 +136,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 && ( { - function handler( event ) { + async function handler( event ) { if ( event.defaultPrevented ) { // This was likely already handled in rich-text/use-paste-handler.js. return; @@ -210,7 +210,7 @@ export function useClipboardHandler() { let blocks = []; if ( files.length ) { - const fromTransforms = getBlockTransforms( 'from' ); + const fromTransforms = await getBlockTransforms( 'from' ); blocks = files .reduce( ( accumulator, file ) => { const transformation = findTransform( @@ -228,7 +228,7 @@ export function useClipboardHandler() { }, [] ) .flat(); } else { - blocks = pasteHandler( { + blocks = await pasteHandler( { HTML: html, plainText, mode: 'BLOCKS', diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js index dc755e8767623..b0929b023d9e6 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -6,8 +6,10 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { useSelect } from '@wordpress/data'; import { useMemo, useRef, memo } from '@wordpress/element'; import { + store as blocksStore, createBlock, createBlocksFromInnerBlocksTemplate, isReusableBlock, @@ -26,20 +28,32 @@ import InserterDraggableBlocks from '../inserter-draggable-blocks'; function InserterListItem( { className, isFirst, - item, + item: { name, isDisabled }, onSelect, onHover, isDraggable, ...props } ) { const isDragging = useRef( false ); - const itemIconStyle = item.icon + + const { item } = useSelect( + ( select ) => ( { + 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/block-patterns-explorer/patterns-list.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js index fda1a00c1a07d..01dae5c80591c 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js @@ -62,20 +62,19 @@ function PatternList( { filterValue, selectedCategory, patternCategories } ) { ); const filteredBlockPatterns = useMemo( () => { - if ( ! filterValue ) { - return allPatterns.filter( ( pattern ) => - selectedCategory === 'uncategorized' - ? ! pattern.categories?.length || - pattern.categories.every( - ( category ) => - ! registeredPatternCategories.includes( - category - ) - ) - : pattern.categories?.includes( selectedCategory ) - ); + if ( filterValue ) { + return searchItems( allPatterns, filterValue ); } - return searchItems( allPatterns, filterValue ); + + return allPatterns.filter( ( pattern ) => + selectedCategory === 'uncategorized' + ? ! pattern.categories?.length || + pattern.categories.every( + ( category ) => + ! registeredPatternCategories.includes( category ) + ) + : pattern.categories?.includes( selectedCategory ) + ); }, [ filterValue, allPatterns, diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js index f66d27ac06170..4b6131a478563 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js @@ -1,13 +1,7 @@ /** * WordPress dependencies */ -import { - useMemo, - useState, - useCallback, - useRef, - useEffect, -} from '@wordpress/element'; +import { useMemo, useState, useRef, useEffect } from '@wordpress/element'; import { _x, __, isRTL } from '@wordpress/i18n'; import { useAsyncList, useViewportMatch } from '@wordpress/compose'; import { @@ -44,27 +38,19 @@ const patternCategoriesOrder = [ 'footer', ]; -function usePatternsCategories( rootClientId ) { - const [ allPatterns, allCategories ] = usePatternsState( - undefined, - rootClientId - ); - - const hasRegisteredCategory = useCallback( - ( pattern ) => { - if ( ! pattern.categories || ! pattern.categories.length ) { - return false; - } +function hasRegisteredCategory( pattern, categories ) { + if ( ! pattern.categories || ! pattern.categories.length ) { + return false; + } - return pattern.categories.some( ( cat ) => - allCategories.some( ( category ) => category.name === cat ) - ); - }, - [ allCategories ] + return pattern.categories.some( ( cat ) => + categories.some( ( category ) => category.name === cat ) ); +} +function usePopulatedCategories( allPatterns, allCategories ) { // Remove any empty categories. - const populatedCategories = useMemo( () => { + return useMemo( () => { const categories = allCategories .filter( ( category ) => allPatterns.some( ( pattern ) => @@ -83,7 +69,7 @@ function usePatternsCategories( rootClientId ) { if ( allPatterns.some( - ( pattern ) => ! hasRegisteredCategory( pattern ) + ( pattern ) => ! hasRegisteredCategory( pattern, allCategories ) ) && ! categories.find( ( category ) => category.name === 'uncategorized' @@ -96,9 +82,7 @@ function usePatternsCategories( rootClientId ) { } return categories; - }, [ allCategories, allPatterns, hasRegisteredCategory ] ); - - return populatedCategories; + }, [ allPatterns, allCategories ] ); } export function BlockPatternsCategoryDialog( { @@ -141,12 +125,16 @@ export function BlockPatternsCategoryPanel( { category, showTitlesAsTooltip, } ) { - const [ allPatterns, , onClick ] = usePatternsState( + const [ allPatterns, allCategories, onClick ] = usePatternsState( onInsert, rootClientId ); - const availableCategories = usePatternsCategories( rootClientId ); + const availableCategories = usePopulatedCategories( + allPatterns, + allCategories + ); + const currentCategoryPatterns = useMemo( () => allPatterns.filter( ( pattern ) => { @@ -156,15 +144,15 @@ export function BlockPatternsCategoryPanel( { // The uncategorized category should show all the patterns without any category // or with no available category. - const availablePatternCategories = - pattern.categories?.filter( ( cat ) => - availableCategories.find( - ( availableCategory ) => - availableCategory.name === cat - ) - ) ?? []; + if ( ! pattern.categories ) { + return true; + } - return availablePatternCategories.length === 0; + return ( + ! pattern.categories.some( ( cat ) => + availableCategories.find( ( { name } ) => name === cat ) + ) ?? [] + ); } ), [ allPatterns, availableCategories, category.name ] ); @@ -206,7 +194,11 @@ function BlockPatternsTabs( { rootClientId, } ) { const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false ); - const categories = usePatternsCategories( rootClientId ); + const [ allPatterns, allCategories ] = usePatternsState( + null, + rootClientId + ); + const categories = usePopulatedCategories( allPatterns, allCategories ); const initialCategory = selectedCategory || categories[ 0 ]; const isMobile = useViewportMatch( 'medium', '<' ); return ( 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 c2257cf237669..334f4b3ff0ed8 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -67,27 +67,25 @@ function InserterMenu( insertionIndex: __experimentalInsertionIndex, shouldFocusBlock, } ); - const { showPatterns, inserterItems } = useSelect( + const { showPatterns, hasReusableBlocks } = useSelect( ( select ) => { const { __experimentalGetAllowedPatterns, getInserterItems } = select( blockEditorStore ); + const patterns = __experimentalGetAllowedPatterns( + destinationRootClientId + ); return { - showPatterns: !! __experimentalGetAllowedPatterns( + showPatterns: patterns ? patterns.length > 0 : null, + hasReusableBlocks: getInserterItems( destinationRootClientId - ).length, - inserterItems: getInserterItems( destinationRootClientId ), + ).some( ( item ) => item.category === 'reusable' ), }; }, [ destinationRootClientId ] ); - const hasReusableBlocks = useMemo( () => { - return inserterItems.some( - ( { category } ) => category === 'reusable' - ); - }, [ inserterItems ] ); const mediaCategories = useMediaCategories( destinationRootClientId ); - const showMedia = !! mediaCategories.length; + const showMedia = mediaCategories.length > 0; const onInsert = useCallback( ( blocks, meta, shouldForceFocusBlock ) => { @@ -224,17 +222,22 @@ function InserterMenu( }, } ) ); + const readyToShow = showPatterns !== null; + const showPatternPanel = selectedTab === 'patterns' && ! delayedFilterValue && selectedPatternCategory; + const showAsTabs = ! delayedFilterValue && ( showPatterns || hasReusableBlocks || showMedia ); + const showMediaPanel = selectedTab === 'media' && ! delayedFilterValue && selectedMediaCategory; + return (
- { !! 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/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index 1ff8b529707a4..3001bf49f3a86 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -34,24 +34,16 @@ function InserterTabs( { showReusableBlocks = false, showMedia = false, onSelect, - prioritizePatterns, + prioritizePatterns = false, } ) { const tabs = useMemo( () => { - const tempTabs = []; - if ( prioritizePatterns && showPatterns ) { - tempTabs.push( patternsTab ); - } - tempTabs.push( blocksTab ); - if ( ! prioritizePatterns && showPatterns ) { - tempTabs.push( patternsTab ); - } - if ( showMedia ) { - tempTabs.push( mediaTab ); - } - if ( showReusableBlocks ) { - tempTabs.push( reusableBlocksTab ); - } - return tempTabs; + return [ + prioritizePatterns && showPatterns && patternsTab, + blocksTab, + ! prioritizePatterns && showPatterns && patternsTab, + showMedia && mediaTab, + showReusableBlocks && reusableBlocksTab, + ].filter( Boolean ); }, [ prioritizePatterns, showPatterns, showReusableBlocks, showMedia ] ); return ( diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 4208e5665cfd4..2a11d14a1afce 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 b0c82848db687..143b77ff7431d 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -315,16 +315,17 @@ 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( [ @@ -378,7 +379,7 @@ function RichTextWrapper( ); const onPaste = useCallback( - ( { + async ( { value, onChange, html, @@ -411,7 +412,7 @@ 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, @@ -462,7 +463,7 @@ function RichTextWrapper( mode = 'BLOCKS'; } - const content = pasteHandler( { + const content = await pasteHandler( { HTML: html, plainText, mode, @@ -528,7 +529,7 @@ function RichTextWrapper( ); const inputRule = useCallback( - ( value, valueToFormat ) => { + async ( value, valueToFormat ) => { if ( ! onReplace ) { return; } @@ -545,14 +546,12 @@ 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 ad51128172582..b78fcf0c54b41 100644 --- a/packages/block-editor/src/components/rich-text/use-enter.js +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -23,7 +23,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; } @@ -51,9 +51,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 58432c01f9683..d32d6af7289b1 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 @@ -50,7 +50,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 ) { @@ -69,14 +69,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 ) { @@ -95,7 +93,7 @@ export function useInputRules( props ) { return true; } - function onInput( event ) { + async function onInput( event ) { const { inputType, type } = event; const { getValue, @@ -109,8 +107,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 d86691ced7097..92b36584d4060 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 @@ -48,7 +48,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, @@ -150,7 +150,7 @@ export function usePasteHandler( props ) { files?.length && ! shouldDismissPastedFiles( files, html, plainText ) ) { - const fromTransforms = getBlockTransforms( 'from' ); + const fromTransforms = await getBlockTransforms( 'from' ); const blocks = files .reduce( ( accumulator, file ) => { const transformation = findTransform( @@ -209,7 +209,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 6b0a930012449..23bd5437b34e3 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/store/actions.js b/packages/block-editor/src/store/actions.js index 32108de713f75..23368d2d90e33 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -697,7 +697,7 @@ export const synchronizeTemplate = */ export const __unstableDeleteSelection = ( isForward ) => - ( { registry, select, dispatch } ) => { + async ( { registry, select, dispatch } ) => { const selectionAnchor = select.getSelectionStart(); const selectionFocus = select.getSelectionEnd(); @@ -798,7 +798,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 ) { @@ -997,7 +1000,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 } ); @@ -1020,7 +1023,7 @@ export const mergeBlocks = if ( ! blockAType.merge ) { // If there's no merge function defined, attempt merging inner // blocks. - const blocksWithTheSameType = switchToBlockType( + const blocksWithTheSameType = await switchToBlockType( blockB, blockAType.name ); @@ -1104,7 +1107,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 245aaf7adb0fd..42a229617ec74 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1913,6 +1913,17 @@ export function blockEditingModes( state = new Map(), 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, @@ -1938,6 +1949,7 @@ const combinedReducers = combineReducers( { blockEditingModes, removalPromptData, blockRemovalRules, + 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 3c961c130b78a..e1c44262b9356 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -8,11 +8,9 @@ import createSelector from 'rememo'; */ import { getBlockType, - getBlockTypes, + getBootstrappedBlockTypes, getBlockVariations, hasBlockSupport, - getPossibleBlockTransformations, - parse, switchToBlockType, store as blocksStore, } from '@wordpress/blocks'; @@ -1525,119 +1523,125 @@ const checkAllowList = ( list, item, defaultResult = null ) => { * * @return {boolean} Whether the given block type is allowed to be inserted. */ -const canInsertBlockTypeUnmemoized = ( - state, - blockName, - rootClientId = null -) => { - let blockType; - if ( blockName && 'object' === typeof blockName ) { - blockType = blockName; - blockName = blockType.name; - } else { - blockType = getBlockType( blockName ); - } - if ( ! blockType ) { - return false; - } +const canInsertBlockTypeUnmemoized = createRegistrySelector( + ( select ) => + ( state, blockName, rootClientId = null ) => { + let blockType; + if ( blockName && 'object' === typeof blockName ) { + blockType = blockName; + blockName = blockType.name; + } else { + blockType = + select( blocksStore ).getBootstrappedBlockType( blockName ); + } + if ( ! blockType ) { + return false; + } - const { allowedBlockTypes } = getSettings( state ); + const { allowedBlockTypes } = getSettings( state ); - const isBlockAllowedInEditor = checkAllowList( - allowedBlockTypes, - blockName, - true - ); - if ( ! isBlockAllowedInEditor ) { - return false; - } - - const isLocked = !! getTemplateLock( state, rootClientId ); - if ( isLocked ) { - return false; - } + const isBlockAllowedInEditor = checkAllowList( + allowedBlockTypes, + blockName, + true + ); + if ( ! isBlockAllowedInEditor ) { + return false; + } - if ( getBlockEditingMode( state, rootClientId ?? '' ) === 'disabled' ) { - return false; - } + const isLocked = !! getTemplateLock( state, rootClientId ); + if ( isLocked ) { + return false; + } - const parentBlockListSettings = getBlockListSettings( state, rootClientId ); + if ( + getBlockEditingMode( state, rootClientId ?? '' ) === 'disabled' + ) { + return false; + } - // The parent block doesn't have settings indicating it doesn't support - // inner blocks, return false. - if ( rootClientId && parentBlockListSettings === undefined ) { - return false; - } + const parentBlockListSettings = getBlockListSettings( + state, + rootClientId + ); - const parentAllowedBlocks = parentBlockListSettings?.allowedBlocks; - const hasParentAllowedBlock = checkAllowList( - parentAllowedBlocks, - blockName - ); + // The parent block doesn't have settings indicating it doesn't support + // inner blocks, return false. + if ( rootClientId && parentBlockListSettings === undefined ) { + return false; + } - const blockAllowedParentBlocks = blockType.parent; - const parentName = getBlockName( state, rootClientId ); - const hasBlockAllowedParent = checkAllowList( - blockAllowedParentBlocks, - parentName - ); + const parentAllowedBlocks = parentBlockListSettings?.allowedBlocks; + const hasParentAllowedBlock = checkAllowList( + parentAllowedBlocks, + blockName + ); - let hasBlockAllowedAncestor = true; - const blockAllowedAncestorBlocks = blockType.ancestor; - if ( blockAllowedAncestorBlocks ) { - const ancestors = [ - rootClientId, - ...getBlockParents( state, rootClientId ), - ]; + const blockAllowedParentBlocks = blockType.parent; + const parentName = getBlockName( state, rootClientId ); + const hasBlockAllowedParent = checkAllowList( + blockAllowedParentBlocks, + parentName + ); - hasBlockAllowedAncestor = ancestors.some( ( ancestorClientId ) => - checkAllowList( - blockAllowedAncestorBlocks, - getBlockName( state, ancestorClientId ) - ) - ); - } + let hasBlockAllowedAncestor = true; + const blockAllowedAncestorBlocks = blockType.ancestor; + if ( blockAllowedAncestorBlocks ) { + const ancestors = [ + rootClientId, + ...getBlockParents( state, rootClientId ), + ]; + + hasBlockAllowedAncestor = ancestors.some( + ( ancestorClientId ) => + checkAllowList( + blockAllowedAncestorBlocks, + getBlockName( state, ancestorClientId ) + ) + ); + } - const canInsert = - hasBlockAllowedAncestor && - ( ( hasParentAllowedBlock === null && - hasBlockAllowedParent === null ) || - hasParentAllowedBlock === true || - hasBlockAllowedParent === true ); + const canInsert = + hasBlockAllowedAncestor && + ( ( hasParentAllowedBlock === null && + hasBlockAllowedParent === null ) || + hasParentAllowedBlock === true || + hasBlockAllowedParent === true ); - if ( ! canInsert ) { - return canInsert; - } + if ( ! canInsert ) { + return canInsert; + } - /** - * This filter is an ad-hoc solution to prevent adding template parts inside post content. - * Conceptually, having a filter inside a selector is bad pattern so this code will be - * replaced by a declarative API that doesn't the following drawbacks: - * - * Filters are not reactive: Upon switching between "template mode" and non "template mode", - * the filter and selector won't necessarily be executed again. For now, it doesn't matter much - * because you can't switch between the two modes while the inserter stays open. - * - * Filters are global: Once they're defined, they will affect all editor instances and all registries. - * An ideal API would only affect specific editor instances. - */ - return applyFilters( - 'blockEditor.__unstableCanInsertBlockType', - canInsert, - blockType, - rootClientId, - { - // Pass bound selectors of the current registry. If we're in a nested - // context, the data will differ from the one selected from the root - // registry. - getBlock: getBlock.bind( null, state ), - getBlockParentsByBlockName: getBlockParentsByBlockName.bind( - null, - state - ), + /** + * This filter is an ad-hoc solution to prevent adding template parts inside post content. + * Conceptually, having a filter inside a selector is bad pattern so this code will be + * replaced by a declarative API that doesn't the following drawbacks: + * + * Filters are not reactive: Upon switching between "template mode" and non "template mode", + * the filter and selector won't necessarily be executed again. For now, it doesn't matter much + * because you can't switch between the two modes while the inserter stays open. + * + * Filters are global: Once they're defined, they will affect all editor instances and all registries. + * An ideal API would only affect specific editor instances. + */ + return applyFilters( + 'blockEditor.__unstableCanInsertBlockType', + canInsert, + blockType, + rootClientId, + { + // Pass bound selectors of the current registry. If we're in a nested + // context, the data will differ from the one selected from the root + // registry. + getBlock: getBlock.bind( null, state ), + getBlockParentsByBlockName: getBlockParentsByBlockName.bind( + null, + state + ), + } + ); } - ); -}; +); /** * Determines if the given block type is allowed to be inserted into the block list. @@ -1648,16 +1652,7 @@ const canInsertBlockTypeUnmemoized = ( * * @return {boolean} Whether the given block type is allowed to be inserted. */ -export const canInsertBlockType = createSelector( - canInsertBlockTypeUnmemoized, - ( state, blockName, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - state.settings.allowedBlockTypes, - state.settings.templateLock, - state.blockEditingModes, - ] -); +export const canInsertBlockType = canInsertBlockTypeUnmemoized; /** * Determines if the given blocks are allowed to be inserted into the block @@ -2002,7 +1997,7 @@ export const getInserterItems = createSelector( buildScope: 'inserter', } ); - const blockTypeInserterItems = getBlockTypes() + const blockTypeInserterItems = getBootstrappedBlockTypes() .filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ) @@ -2014,10 +2009,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; }, [] ); @@ -2026,18 +2020,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 ) => [ @@ -2048,7 +2034,7 @@ export const getInserterItems = createSelector( state.settings.allowedBlockTypes, state.settings.templateLock, getReusableBlocks( state ), - getBlockTypes(), + getBootstrappedBlockTypes(), ] ); @@ -2079,37 +2065,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 ], @@ -2117,7 +2084,7 @@ export const getBlockTransformItems = createSelector( state.preferences.insertUsage, state.settings.allowedBlockTypes, state.settings.templateLock, - getBlockTypes(), + getBootstrappedBlockTypes(), ] ); @@ -2129,29 +2096,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. @@ -2167,9 +2126,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; @@ -2185,7 +2145,7 @@ export const getAllowedBlocks = createSelector( state.settings.allowedBlockTypes, state.settings.templateLock, getReusableBlocks( state ), - getBlockTypes(), + getBootstrappedBlockTypes(), ] ); @@ -2290,7 +2250,7 @@ const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => { return true; }; -function getUnsyncedPatterns( state ) { +export function getUnsyncedPatterns( state ) { const reusableBlocks = state?.settings?.__experimentalReusableBlocks ?? EMPTY_ARRAY; @@ -2309,53 +2269,9 @@ function getUnsyncedPatterns( state ) { } ); } -export const __experimentalGetParsedPattern = createSelector( - ( state, patternName ) => { - const patterns = state.settings.__experimentalBlockPatterns; - const unsyncedPatterns = getUnsyncedPatterns( state ); - - const pattern = [ ...patterns, ...unsyncedPatterns ].find( - ( { name } ) => name === patternName - ); - if ( ! pattern ) { - return null; - } - return { - ...pattern, - blocks: parse( pattern.content, { - __unstableSkipMigrationLogs: true, - } ), - }; - }, - ( state ) => [ - state.settings.__experimentalBlockPatterns, - state.settings.__experimentalReusableBlocks, - ] -); - -const getAllAllowedPatterns = createSelector( - ( state ) => { - const patterns = state.settings.__experimentalBlockPatterns; - const unsyncedPatterns = getUnsyncedPatterns( state ); - - const { allowedBlockTypes } = getSettings( state ); - - const parsedPatterns = [ ...patterns, ...unsyncedPatterns ] - .filter( ( { inserter = true } ) => !! inserter ) - .map( ( { name } ) => - __experimentalGetParsedPattern( state, name ) - ); - const allowedPatterns = parsedPatterns.filter( ( { blocks } ) => - checkAllowListRecursive( blocks, allowedBlockTypes ) - ); - return allowedPatterns; - }, - ( state ) => [ - state.settings.__experimentalBlockPatterns, - state.settings.__experimentalReusableBlocks, - state.settings.allowedBlockTypes, - ] -); +export const __experimentalGetParsedPattern = ( state, patternName ) => { + return state.parsedPatterns[ patternName ] ?? null; +}; /** * Returns the list of allowed patterns for inner blocks children. @@ -2367,15 +2283,30 @@ const getAllAllowedPatterns = createSelector( */ export const __experimentalGetAllowedPatterns = createSelector( ( state, rootClientId = null ) => { - const availableParsedPatterns = getAllAllowedPatterns( state ); - const patternsAllowed = availableParsedPatterns.filter( + const patterns = state.settings.__experimentalBlockPatterns; + const unsyncedPatterns = getUnsyncedPatterns( state ); + + const inserterPatterns = [ ...patterns, ...unsyncedPatterns ].filter( + ( { inserter = true } ) => inserter + ); + + const parsedPatterns = inserterPatterns.map( ( pattern ) => + __experimentalGetParsedPattern( state, pattern.name ) + ); + + if ( parsedPatterns.some( ( p ) => ! p ) ) { + return null; + } + const { allowedBlockTypes } = getSettings( state ); + const allowedPatterns = parsedPatterns.filter( ( { blocks } ) => - blocks.every( ( { name } ) => - canInsertBlockType( state, name, rootClientId ) + checkAllowListRecursive( blocks, allowedBlockTypes ) && + blocks.every( ( block ) => + canInsertBlockType( state, block.name, rootClientId ) ) ); - return patternsAllowed; + return allowedPatterns; }, ( state, rootClientId ) => [ state.settings.__experimentalBlockPatterns, @@ -2384,6 +2315,7 @@ export const __experimentalGetAllowedPatterns = createSelector( state.settings.templateLock, state.blockListSettings[ rootClientId ], state.blocks.byClientId.get( rootClientId ), + state.parsedPatterns, ] ); @@ -2407,6 +2339,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 9bb2541cc7af2..06813057f2542 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -351,10 +351,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 3a7b7d28f9e80..09ba889bdd4f5 100644 --- a/packages/block-library/src/navigation-link/link-ui.js +++ b/packages/block-library/src/navigation-link/link-ui.js @@ -105,10 +105,10 @@ function LinkControlTransforms( { clientId } ) { return (