diff --git a/package-lock.json b/package-lock.json index 11a42c2e047a3..32b6636a8ec71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54532,6 +54532,7 @@ "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", "@wordpress/blocks": "file:../blocks", + "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", @@ -67221,6 +67222,7 @@ "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", "@wordpress/blocks": "file:../blocks", + "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 30e0cc9f422e3..e4653791f999a 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -680,6 +680,10 @@ _Related_ Private @wordpress/block-editor APIs. +### ReusableBlocksRenameHint + +Undocumented declaration. + ### RichText _Related_ @@ -789,6 +793,10 @@ _Related_ - +### useBlockCommands + +Undocumented declaration. + ### useBlockDisplayInformation Hook used to try to find a matching block variation and return the appropriate information for display reasons. In order to to try to find a match we need to things: 1. Block's client id to extract it's current attributes. 2. A block variation should have set `isActive` prop to a proper function. diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 0f982a3119da6..69dcb602dad1c 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -39,6 +39,7 @@ "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", "@wordpress/blocks": "file:../blocks", + "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 6a4b5d8b98088..7e46698e2b61b 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -166,3 +166,9 @@ export { useBlockEditingMode } from './block-editing-mode'; export { default as BlockEditorProvider } from './provider'; export { default as useSetting } from './use-setting'; +export { useBlockCommands } from './use-block-commands'; + +/* + * The following rename hint component can be removed in 6.4. + */ +export { default as ReusableBlocksRenameHint } from './inserter/reusable-block-rename-hint'; diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js new file mode 100644 index 0000000000000..bb7b7d97c3190 --- /dev/null +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -0,0 +1,284 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + hasBlockSupport, + store as blocksStore, + switchToBlockType, + isTemplatePart, +} from '@wordpress/blocks'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useCommandLoader } from '@wordpress/commands'; +import { + copy, + edit as remove, + create as add, + group, + ungroup, + moveTo as move, +} from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export const useTransformCommands = () => { + const { clientIds } = useSelect( ( select ) => { + const { getSelectedBlockClientIds } = select( blockEditorStore ); + const selectedBlockClientIds = getSelectedBlockClientIds(); + + return { + clientIds: selectedBlockClientIds, + }; + }, [] ); + const blocks = useSelect( + ( select ) => + select( blockEditorStore ).getBlocksByClientId( clientIds ), + [ clientIds ] + ); + const { replaceBlocks, multiSelect } = useDispatch( blockEditorStore ); + const { possibleBlockTransformations, canRemove } = useSelect( + ( select ) => { + const { + getBlockRootClientId, + getBlockTransformItems, + canRemoveBlocks, + } = select( blockEditorStore ); + const rootClientId = getBlockRootClientId( + Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds + ); + return { + possibleBlockTransformations: getBlockTransformItems( + blocks, + rootClientId + ), + canRemove: canRemoveBlocks( clientIds, rootClientId ), + }; + }, + [ clientIds, blocks ] + ); + + const isTemplate = blocks.length === 1 && isTemplatePart( blocks[ 0 ] ); + + function selectForMultipleBlocks( insertedBlocks ) { + if ( insertedBlocks.length > 1 ) { + multiSelect( + insertedBlocks[ 0 ].clientId, + insertedBlocks[ insertedBlocks.length - 1 ].clientId + ); + } + } + + // Simple block tranformation based on the `Block Transforms` API. + function onBlockTransform( name ) { + const newBlocks = switchToBlockType( blocks, name ); + replaceBlocks( clientIds, newBlocks ); + selectForMultipleBlocks( newBlocks ); + } + + /** + * The `isTemplate` check is a stopgap solution here. + * Ideally, the Transforms API should handle this + * by allowing to exclude blocks from wildcard transformations. + */ + const hasPossibleBlockTransformations = + !! possibleBlockTransformations.length && canRemove && ! isTemplate; + + if ( + ! clientIds || + clientIds.length < 1 || + ! hasPossibleBlockTransformations + ) { + return { isLoading: false, commands: [] }; + } + + const commands = possibleBlockTransformations.map( ( transformation ) => { + const { name, title, icon } = transformation; + return { + name: 'core/block-editor/transform-to-' + name.replace( '/', '-' ), + // translators: %s: block title/name. + label: sprintf( __( 'Transform to %s' ), title ), + icon: icon.src, + callback: ( { close } ) => { + onBlockTransform( name ); + close(); + }, + }; + } ); + + return { isLoading: false, commands }; +}; + +const useActionsCommands = () => { + const { clientIds } = useSelect( ( select ) => { + const { getSelectedBlockClientIds } = select( blockEditorStore ); + const selectedBlockClientIds = getSelectedBlockClientIds(); + + return { + clientIds: selectedBlockClientIds, + }; + }, [] ); + const { + canInsertBlockType, + getBlockRootClientId, + getBlocksByClientId, + canMoveBlocks, + canRemoveBlocks, + } = useSelect( blockEditorStore ); + const { getDefaultBlockName, getGroupingBlockName } = + useSelect( blocksStore ); + + const blocks = getBlocksByClientId( clientIds ); + const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); + + const canDuplicate = blocks.every( ( block ) => { + return ( + !! block && + hasBlockSupport( block.name, 'multiple', true ) && + canInsertBlockType( block.name, rootClientId ) + ); + } ); + + const canInsertDefaultBlock = canInsertBlockType( + getDefaultBlockName(), + rootClientId + ); + + const canMove = canMoveBlocks( clientIds, rootClientId ); + const canRemove = canRemoveBlocks( clientIds, rootClientId ); + + const { + removeBlocks, + replaceBlocks, + duplicateBlocks, + insertAfterBlock, + insertBeforeBlock, + setBlockMovingClientId, + setNavigationMode, + selectBlock, + } = useDispatch( blockEditorStore ); + + const onDuplicate = () => { + if ( ! canDuplicate ) { + return; + } + return duplicateBlocks( clientIds, true ); + }; + const onRemove = () => { + if ( ! canRemove ) { + return; + } + return removeBlocks( clientIds, true ); + }; + const onAddBefore = () => { + if ( ! canInsertDefaultBlock ) { + return; + } + const clientId = Array.isArray( clientIds ) ? clientIds[ 0 ] : clientId; + insertBeforeBlock( clientId ); + }; + const onAddAfter = () => { + if ( ! canInsertDefaultBlock ) { + return; + } + const clientId = Array.isArray( clientIds ) + ? clientIds[ clientIds.length - 1 ] + : clientId; + insertAfterBlock( clientId ); + }; + const onMoveTo = () => { + if ( ! canMove ) { + return; + } + setNavigationMode( true ); + selectBlock( clientIds[ 0 ] ); + setBlockMovingClientId( clientIds[ 0 ] ); + }; + const onGroup = () => { + if ( ! blocks.length ) { + return; + } + + const groupingBlockName = getGroupingBlockName(); + + // Activate the `transform` on `core/group` which does the conversion. + const newBlocks = switchToBlockType( blocks, groupingBlockName ); + + if ( ! newBlocks ) { + return; + } + replaceBlocks( clientIds, newBlocks ); + }; + const onUngroup = () => { + if ( ! blocks.length ) { + return; + } + + const innerBlocks = blocks[ 0 ].innerBlocks; + + if ( ! innerBlocks.length ) { + return; + } + + replaceBlocks( clientIds, innerBlocks ); + }; + + if ( ! clientIds || clientIds.length < 1 ) { + return { isLoading: false, commands: [] }; + } + + const icons = { + ungroup, + group, + move, + add, + remove, + duplicate: copy, + }; + + const commands = [ + onUngroup, + onGroup, + onMoveTo, + onAddAfter, + onAddBefore, + onRemove, + onDuplicate, + ].map( ( callback ) => { + const action = callback.name + .replace( 'on', '' ) + .replace( /([a-z])([A-Z])/g, '$1 $2' ); + + return { + name: 'core/block-editor/action-' + callback.name, + // translators: %s: type of the command. + label: action, + icon: icons[ + callback.name + .replace( 'on', '' ) + .match( /[A-Z]{1}[a-z]*/ ) + .toString() + .toLowerCase() + ], + callback: ( { close } ) => { + callback(); + close(); + }, + }; + } ); + + return { isLoading: false, commands }; +}; + +export const useBlockCommands = () => { + useCommandLoader( { + name: 'core/block-editor/blockTransforms', + hook: useTransformCommands, + } ); + useCommandLoader( { + name: 'core/block-editor/blockActions', + hook: useActionsCommands, + } ); +}; diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index e3000ae77b863..eb7a245d96a79 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -19,6 +19,7 @@ import { } from '@wordpress/editor'; import { useSelect, useDispatch } from '@wordpress/data'; import { + useBlockCommands, BlockBreadcrumb, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; @@ -72,6 +73,7 @@ const interfaceLabels = { }; function Layout() { + useBlockCommands(); const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); const isLargeViewport = useViewportMatch( 'large' ); diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 54ac50a02165a..80950d130a0a1 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -26,7 +26,10 @@ import { privateApis as commandsPrivateApis, } from '@wordpress/commands'; import { store as preferencesStore } from '@wordpress/preferences'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { + privateApis as blockEditorPrivateApis, + useBlockCommands, +} from '@wordpress/block-editor'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; @@ -66,6 +69,7 @@ export default function Layout() { useCommands(); useEditModeCommands(); useCommonCommands(); + useBlockCommands(); const hubRef = useRef(); const { params } = useLocation();