diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index b78eeed5b1d6a1..bb8990da56c09a 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -9,6 +9,8 @@ import { __experimentalHStack as HStack, Icon, Navigator, + __experimentalToolsPanel as ToolsPanel, + useNavigator, } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -24,8 +26,11 @@ import { store as blockEditorStore } from '../../store'; import BlockIcon from '../block-icon'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; import useBlockDisplayInformation from '../use-block-display-information'; +import { PrivateListView } from '../list-view'; +import InspectorControls from '../inspector-controls'; const { fieldsKey } = unlock( blocksPrivateApis ); import FieldsDropdownMenu from './fields-dropdown-menu'; +import LeafMoreMenu from './leaf-more-menu'; // controls import RichText from './rich-text'; @@ -173,6 +178,55 @@ function denormalizeLinkValue( value, fieldDef ) { return result; } +function ContentOnlyBackButton() { + return ( +
+ + + +
{ __( 'Back' ) }
+
+
+
+ ); +} + +function NavigationListView( { clientId } ) { + const blockTitle = useBlockDisplayTitle( { + clientId, + context: 'list-view', + } ); + const blockInformation = useBlockDisplayInformation( clientId ); + const { goTo } = useNavigator(); + + const handleSelect = ( block ) => { + goTo( `/${ clientId }/${ block.clientId }` ); + }; + + return ( + + +
{ blockTitle }
+ + } + panelId={ clientId } + resetAll={ () => {} } + > +
+ +
+
+ ); +} + function BlockFields( { clientId } ) { const { attributes, blockType } = useSelect( ( select ) => { @@ -416,12 +470,15 @@ function ContentOnlyControlsScreen( { parentClientIds, isNested, } ) { - const isRootContentBlock = useSelect( + const { isRootContentBlock, isNavigationBlock } = useSelect( ( select ) => { const { getBlockName } = select( blockEditorStore ); const blockName = getBlockName( rootClientId ); const { hasContentRoleAttribute } = unlock( select( blocksStore ) ); - return hasContentRoleAttribute( blockName ); + return { + isRootContentBlock: hasContentRoleAttribute( blockName ), + isNavigationBlock: blockName === 'core/navigation', + }; }, [ rootClientId ] ); @@ -430,18 +487,19 @@ function ContentOnlyControlsScreen( { return null; } + // Special case: If this is a navigation block drilldown, show the NavigationListView + if ( isNavigationBlock && isNested ) { + return ( + <> + + + + ); + } + return ( <> - { isNested && ( -
- - - -
{ __( 'Back' ) }
-
-
-
- ) } + { isNested && } { isRootContentBlock && } { contentClientIds.map( ( clientId ) => { if ( parentClientIds?.[ clientId ] ) { @@ -460,84 +518,124 @@ function ContentOnlyControlsScreen( { } export default function ContentOnlyControls( { rootClientId } ) { - const { updatedRootClientId, nestedContentClientIds, contentClientIds } = - useSelect( - ( select ) => { - const { getClientIdsOfDescendants, getBlockEditingMode } = - select( blockEditorStore ); - - // _nestedContentClientIds is for content blocks within 'drilldowns'. - // It's an object where the key is the parent clientId, and the element is - // an array of child clientIds whose controls are shown within the drilldown. - const _nestedContentClientIds = {}; - - // _contentClientIds is the list of contentClientIds for blocks being - // shown at the root level. Includes parent blocks that might have a drilldown, - // but not the children of those blocks. - const _contentClientIds = []; - - // An array of all nested client ids. Used for ensuring blocks within drilldowns - // don't appear at the root level. - let allNestedClientIds = []; - - // A flattened list of all content clientIds to arrange into the - // groups above. - const allContentClientIds = getClientIdsOfDescendants( - rootClientId - ).filter( - ( clientId ) => - getBlockEditingMode( clientId ) === 'contentOnly' + const { + updatedRootClientId, + nestedContentClientIds, + contentClientIds, + navigationBlockIds, + } = useSelect( + ( select ) => { + const { + getClientIdsOfDescendants, + getBlockEditingMode, + getBlockName, + } = select( blockEditorStore ); + + // _nestedContentClientIds is for content blocks within 'drilldowns'. + // It's an object where the key is the parent clientId, and the element is + // an array of child clientIds whose controls are shown within the drilldown. + const _nestedContentClientIds = {}; + + // _contentClientIds is the list of contentClientIds for blocks being + // shown at the root level. Includes parent blocks that might have a drilldown, + // but not the children of those blocks. + const _contentClientIds = []; + + // An array of all nested client ids. Used for ensuring blocks within drilldowns + // don't appear at the root level. + let allNestedClientIds = []; + + // A flattened list of all content clientIds to arrange into the + // groups above. + const allDescendants = getClientIdsOfDescendants( rootClientId ); + + // Exclude Navigation block children (but not the navigation block itself) + // Navigation blocks will show their own list view controls + const navigationChildren = new Set(); + + // Check if root is a navigation block + if ( getBlockName( rootClientId ) === 'core/navigation' ) { + allDescendants.forEach( ( childId ) => + navigationChildren.add( childId ) ); + } - for ( const clientId of allContentClientIds ) { - const childClientIds = getClientIdsOfDescendants( - clientId - ).filter( - ( childClientId ) => - getBlockEditingMode( childClientId ) === - 'contentOnly' + // Check for navigation blocks within descendants and exclude only their children + allDescendants.forEach( ( clientId ) => { + if ( getBlockName( clientId ) === 'core/navigation' ) { + // Don't exclude the navigation block itself, only its children + const navChildren = getClientIdsOfDescendants( clientId ); + navChildren.forEach( ( childId ) => + navigationChildren.add( childId ) ); + } + } ); - // If there's more than one child block, use a drilldown. - if ( - childClientIds.length > 1 && - ! allNestedClientIds.includes( clientId ) - ) { - _nestedContentClientIds[ clientId ] = childClientIds; - allNestedClientIds = [ - allNestedClientIds, - ...childClientIds, - ]; - } + const allContentClientIds = allDescendants.filter( + ( clientId ) => + getBlockEditingMode( clientId ) === 'contentOnly' && + ! navigationChildren.has( clientId ) + ); - if ( ! allNestedClientIds.includes( clientId ) ) { - _contentClientIds.push( clientId ); - } - } + for ( const clientId of allContentClientIds ) { + const childClientIds = getClientIdsOfDescendants( + clientId + ).filter( + ( childClientId ) => + getBlockEditingMode( childClientId ) === 'contentOnly' + ); - // Avoid showing only one drilldown block at the root. + // If there's more than one child block, use a drilldown. + // For navigation blocks, we'll show the NavigationListView in the drilldown. if ( - _contentClientIds.length === 1 && - Object.keys( _nestedContentClientIds ).length === 1 + childClientIds.length > 1 && + ! allNestedClientIds.includes( clientId ) ) { - const onlyParentClientId = Object.keys( - _nestedContentClientIds - )[ 0 ]; - return { - updatedRootClientId: onlyParentClientId, - contentClientIds: - _nestedContentClientIds[ onlyParentClientId ], - nestedContentClientIds: {}, - }; + _nestedContentClientIds[ clientId ] = childClientIds; + allNestedClientIds = [ + allNestedClientIds, + ...childClientIds, + ]; } + if ( ! allNestedClientIds.includes( clientId ) ) { + _contentClientIds.push( clientId ); + } + } + + // Identify which parent blocks are navigation blocks + const _navigationBlockIds = new Set(); + Object.keys( _nestedContentClientIds ).forEach( ( parentId ) => { + if ( getBlockName( parentId ) === 'core/navigation' ) { + _navigationBlockIds.add( parentId ); + } + } ); + + // Avoid showing only one drilldown block at the root. + if ( + _contentClientIds.length === 1 && + Object.keys( _nestedContentClientIds ).length === 1 + ) { + const onlyParentClientId = Object.keys( + _nestedContentClientIds + )[ 0 ]; return { - nestedContentClientIds: _nestedContentClientIds, - contentClientIds: _contentClientIds, + updatedRootClientId: onlyParentClientId, + contentClientIds: + _nestedContentClientIds[ onlyParentClientId ], + nestedContentClientIds: {}, + navigationBlockIds: _navigationBlockIds, }; - }, - [ rootClientId ] - ); + } + + return { + nestedContentClientIds: _nestedContentClientIds, + contentClientIds: _contentClientIds, + navigationBlockIds: _navigationBlockIds, + }; + }, + [ rootClientId ] + ); return ( @@ -564,6 +662,23 @@ export default function ContentOnlyControls( { rootClientId } ) { /> ) ) } + { /* Create screens for navigation children to show their inspectors */ } + { Array.from( navigationBlockIds ).flatMap( ( parentId ) => + nestedContentClientIds[ parentId ].map( ( childId ) => ( + +
+ +
+ +
+
+
+ ) ) + ) }
); } diff --git a/packages/block-editor/src/components/content-only-controls/leaf-more-menu.js b/packages/block-editor/src/components/content-only-controls/leaf-more-menu.js new file mode 100644 index 00000000000000..804edcf6061ae8 --- /dev/null +++ b/packages/block-editor/src/components/content-only-controls/leaf-more-menu.js @@ -0,0 +1,201 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; +import { + addSubmenu, + chevronUp, + chevronDown, + moreVertical, +} from '@wordpress/icons'; +import { DropdownMenu, MenuItem, MenuGroup } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BlockTitle from '../block-title'; +import { store as blockEditorStore } from '../../store'; + +const POPOVER_PROPS = { + className: 'block-editor-block-settings-menu__popover', + placement: 'bottom-start', +}; + +const BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU = [ + 'core/navigation-link', + 'core/navigation-submenu', +]; + +const DEFAULT_BLOCK = { + name: 'core/navigation-link', + attributes: { + kind: 'post-type', + type: 'page', + }, +}; + +function AddSubmenuItem( { + block, + onClose, + expandedState, + expand, + setInsertedBlock, +} ) { + const { insertBlock, replaceBlock, replaceInnerBlocks } = + useDispatch( blockEditorStore ); + + const clientId = block.clientId; + const isDisabled = ! BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU.includes( + block.name + ); + return ( + { + const updateSelectionOnInsert = false; + const newLink = createBlock( + DEFAULT_BLOCK.name, + DEFAULT_BLOCK.attributes + ); + + if ( block.name === 'core/navigation-submenu' ) { + insertBlock( + newLink, + block.innerBlocks.length, + clientId, + updateSelectionOnInsert + ); + } else { + // Convert to a submenu if the block currently isn't one. + const newSubmenu = createBlock( + 'core/navigation-submenu', + block.attributes, + block.innerBlocks + ); + + // The following must happen as two independent actions. + // Why? Because the offcanvas editor relies on the getLastInsertedBlocksClientIds + // selector to determine which block is "active". As the UX needs the newLink to be + // the "active" block it must be the last block to be inserted. + // Therefore the Submenu is first created and **then** the newLink is inserted + // thus ensuring it is the last inserted block. + replaceBlock( clientId, newSubmenu ); + + replaceInnerBlocks( + newSubmenu.clientId, + [ newLink ], + updateSelectionOnInsert + ); + } + + // This call sets the local List View state for the "last inserted block". + // This is required for the Nav Block to determine whether or not to display + // the Link UI for this new block. + setInsertedBlock( newLink ); + + if ( ! expandedState[ block.clientId ] ) { + expand( block.clientId ); + } + onClose(); + } } + > + { __( 'Add submenu link' ) } + + ); +} + +export default function LeafMoreMenu( props ) { + const { + block, + expand, + expandedState, + setInsertedBlock, + icon = moreVertical, + label = __( 'Options' ), + popoverProps, + toggleProps, + ...restProps + } = props; + const { clientId } = block; + + const { moveBlocksDown, moveBlocksUp, removeBlocks } = + useDispatch( blockEditorStore ); + + const removeLabel = sprintf( + /* translators: %s: block name */ + __( 'Remove %s' ), + BlockTitle( { clientId, maximumLength: 25 } ) + ); + + const rootClientId = useSelect( + ( select ) => { + const { getBlockRootClientId } = select( blockEditorStore ); + + return getBlockRootClientId( clientId ); + }, + [ clientId ] + ); + + // Merge default popover props with passed-in props + const mergedPopoverProps = { + ...POPOVER_PROPS, + ...popoverProps, + }; + + return ( + + { ( { onClose } ) => ( + <> + + { + moveBlocksUp( [ clientId ], rootClientId ); + onClose(); + } } + > + { __( 'Move up' ) } + + { + moveBlocksDown( [ clientId ], rootClientId ); + onClose(); + } } + > + { __( 'Move down' ) } + + + + + { + removeBlocks( [ clientId ], false ); + onClose(); + } } + > + { removeLabel } + + + + ) } + + ); +} diff --git a/packages/block-editor/src/components/content-only-controls/styles.scss b/packages/block-editor/src/components/content-only-controls/styles.scss index 105dc3c0c11f1e..648c184052f987 100644 --- a/packages/block-editor/src/components/content-only-controls/styles.scss +++ b/packages/block-editor/src/components/content-only-controls/styles.scss @@ -42,3 +42,8 @@ padding: $grid-unit-10 0; margin-bottom: $grid-unit-05; } + +.block-editor-content-only-controls__navigation-list-view { + // Ensure the list view takes full width + grid-column: 1/-1; +}