diff --git a/packages/block-editor/src/components/off-canvas-editor/block-edit-button.js b/packages/block-editor/src/components/off-canvas-editor/block-edit-button.js new file mode 100644 index 0000000000000..4d44bc658c74d --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/block-edit-button.js @@ -0,0 +1,215 @@ +/** + * WordPress dependencies + */ +import { edit } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { useMemo, useState } from '@wordpress/element'; +import { Button, Modal } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { createBlock as create } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +/** + * External dependencies + */ +// import InspectorControls from '../../components/inspector-controls'; + +// copied from packages/block-library/src/page-list/edit.js + +// We only show the edit option when page count is <= MAX_PAGE_COUNT +// Performance of Navigation Links is not good past this value. +const MAX_PAGE_COUNT = 100; + +const usePageData = () => { + // 1. Grab editor settings + // 2. Call the selector when we need it + const { pages } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + + return { + pages: getSettings().__experimentalFetchPageEntities( { + orderby: 'menu_order', + order: 'asc', + _fields: [ 'id', 'link', 'parent', 'title', 'menu_order' ], + per_page: -1, + context: 'view', + } ), + }; + }, [] ); + + return useMemo( () => { + // TODO: Once the REST API supports passing multiple values to + // 'orderby', this can be removed. + // https://core.trac.wordpress.org/ticket/39037 + const sortedPages = [ ...( pages ?? [] ) ].sort( ( a, b ) => { + if ( a.menu_order === b.menu_order ) { + return a.title.rendered.localeCompare( b.title.rendered ); + } + return a.menu_order - b.menu_order; + } ); + const pagesByParentId = sortedPages.reduce( ( accumulator, page ) => { + const { parent } = page; + if ( accumulator.has( parent ) ) { + accumulator.get( parent ).push( page ); + } else { + accumulator.set( parent, [ page ] ); + } + return accumulator; + }, new Map() ); + + return { + pages, // necessary for access outside the hook + pagesByParentId, + totalPages: pages?.length ?? null, + }; + }, [ pages ] ); +}; + +// copied from convert-to-links-modal.js +/** + * WordPress dependencies + */ + +const convertSelectedBlockToNavigationLinks = + ( { pages, clientId, replaceBlock, createBlock } ) => + () => { + if ( ! pages?.length ) { + return; + } + + const linkMap = {}; + const navigationLinks = []; + pages.forEach( ( { id, title, link: url, type, parent } ) => { + // See if a placeholder exists. This is created if children appear before parents in list. + const innerBlocks = linkMap[ id ]?.innerBlocks ?? []; + linkMap[ id ] = createBlock( + 'core/navigation-link', + { + id, + label: title.rendered, + url, + type, + kind: 'post-type', + }, + innerBlocks + ); + + if ( ! parent ) { + navigationLinks.push( linkMap[ id ] ); + } else { + if ( ! linkMap[ parent ] ) { + // Use a placeholder if the child appears before parent in list. + linkMap[ parent ] = { innerBlocks: [] }; + } + const parentLinkInnerBlocks = linkMap[ parent ].innerBlocks; + parentLinkInnerBlocks.push( linkMap[ id ] ); + } + } ); + + // Transform all links with innerBlocks into Submenus. This can't be done + // sooner because page objects have no information on their children. + + const transformSubmenus = ( listOfLinks ) => { + listOfLinks.forEach( ( block, index, listOfLinksArray ) => { + const { attributes, innerBlocks } = block; + if ( innerBlocks.length !== 0 ) { + transformSubmenus( innerBlocks ); + const transformedBlock = createBlock( + 'core/navigation-submenu', + attributes, + innerBlocks + ); + listOfLinksArray[ index ] = transformedBlock; + } + } ); + }; + + transformSubmenus( navigationLinks ); + + replaceBlock( clientId, navigationLinks ); + }; + +const ConvertToLinksModal = ( { onClose, clientId, pages } ) => { + const hasPages = !! pages?.length; + + const { replaceBlock } = useDispatch( blockEditorStore ); + + return ( + <Modal + closeLabel={ __( 'Close' ) } + onRequestClose={ onClose } + title={ __( 'Convert to links' ) } + className={ 'wp-block-page-list-modal' } + aria={ { describedby: 'wp-block-page-list-modal__description' } } + > + <p id={ 'wp-block-page-list-modal__description' }> + { __( + 'To edit this navigation menu, convert it to single page links. This allows you to add, re-order, remove items, or edit their labels.' + ) } + </p> + <p> + { __( + "Note: if you add new pages to your site, you'll need to add them to your navigation menu." + ) } + </p> + <div className="wp-block-page-list-modal-buttons"> + <Button variant="tertiary" onClick={ onClose }> + { __( 'Cancel' ) } + </Button> + <Button + variant="primary" + disabled={ ! hasPages } + onClick={ convertSelectedBlockToNavigationLinks( { + pages, + replaceBlock, + clientId, + createBlock: create, + } ) } + > + { __( 'Convert' ) } + </Button> + </div> + </Modal> + ); +}; + +const BlockEditButton = ( { label, clientId } ) => { + const { toggleBlockHighlight } = useDispatch( blockEditorStore ); + const [ convertModalOpen, setConvertModalOpen ] = useState( false ); + const { pages, totalPages } = usePageData(); + + const block = useSelect( + ( select ) => { + return select( blockEditorStore ).getBlock( clientId ); + }, + [ clientId ] + ); + + const onClick = () => { + toggleBlockHighlight( clientId, true ); + setConvertModalOpen( ! convertModalOpen ); + }; + + const allowConvertToLinks = + 'core/page-list' === block.name && totalPages <= MAX_PAGE_COUNT; + + return ( + <> + { convertModalOpen && ( + <ConvertToLinksModal + onClose={ () => setConvertModalOpen( false ) } + clientId={ clientId } + pages={ pages } + /> + ) } + { allowConvertToLinks && ( + <Button icon={ edit } label={ label } onClick={ onClick } /> + ) } + </> + ); +}; + +export default BlockEditButton; diff --git a/packages/block-editor/src/components/off-canvas-editor/block.js b/packages/block-editor/src/components/off-canvas-editor/block.js index e338c4c35c957..50119ebddfb57 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block.js +++ b/packages/block-editor/src/components/off-canvas-editor/block.js @@ -33,6 +33,7 @@ import { } from '../block-mover/button'; import ListViewBlockContents from './block-contents'; import BlockSettingsDropdown from '../block-settings-menu/block-settings-dropdown'; +import BlockEditButton from './block-edit-button'; import { useListViewContext } from './context'; import { getBlockPositionDescription } from './utils'; import { store as blockEditorStore } from '../../store'; @@ -132,6 +133,14 @@ function ListViewBlock( { ) : __( 'Options' ); + const editAriaLabel = blockInformation + ? sprintf( + // translators: %s: The title of the block. + __( 'Edit %s block' ), + blockInformation.title + ) + : __( 'Edit' ); + const { isTreeGridMounted, expand, collapse } = useListViewContext(); const hasSiblings = siblingBlockCount > 0; @@ -146,6 +155,11 @@ function ListViewBlock( { { 'is-visible': isHovered || isFirstSelectedBlock } ); + const listViewBlockEditClassName = classnames( + 'block-editor-list-view-block__menu-cell', + { 'is-visible': isHovered || isFirstSelectedBlock } + ); + // If ListView has experimental features related to the Persistent List View, // only focus the selected list item on mount; otherwise the list would always // try to steal the focus from the editor canvas. @@ -307,26 +321,44 @@ function ListViewBlock( { ) } { showBlockActions && ( - <TreeGridCell - className={ listViewBlockSettingsClassName } - aria-selected={ !! isSelected || forceSelectionContentLock } - > - { ( { ref, tabIndex, onFocus } ) => ( - <BlockSettingsDropdown - clientIds={ dropdownClientIds } - icon={ moreVertical } - label={ settingsAriaLabel } - toggleProps={ { - ref, - className: 'block-editor-list-view-block__menu', - tabIndex, - onFocus, - } } - disableOpenOnArrowDown - __experimentalSelectBlock={ updateSelection } - /> - ) } - </TreeGridCell> + <> + <TreeGridCell + className={ listViewBlockEditClassName } + aria-selected={ + !! isSelected || forceSelectionContentLock + } + > + { () => ( + <BlockEditButton + label={ editAriaLabel } + clientId={ clientId } + /> + ) } + </TreeGridCell> + <TreeGridCell + className={ listViewBlockSettingsClassName } + aria-selected={ + !! isSelected || forceSelectionContentLock + } + > + { ( { ref, tabIndex, onFocus } ) => ( + <BlockSettingsDropdown + clientIds={ dropdownClientIds } + icon={ moreVertical } + label={ settingsAriaLabel } + toggleProps={ { + ref, + className: + 'block-editor-list-view-block__menu', + tabIndex, + onFocus, + } } + disableOpenOnArrowDown + __experimentalSelectBlock={ updateSelection } + /> + ) } + </TreeGridCell> + </> ) } </ListViewLeaf> ); diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 0f42214b45442..62e79ac14cefa 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -122,6 +122,16 @@ export default function BlockEditor( { setIsInserterOpen } ) { [ settingsBlockPatternCategories, restBlockPatternCategories ] ); + const { fetchPagesEntities } = useSelect( ( select ) => { + const { getEntityRecords } = select( coreStore ); + + return { + fetchPagesEntities: ( options = {} ) => { + return getEntityRecords( 'postType', 'page', options ); + }, + }; + }, [] ); + const settings = useMemo( () => { const { __experimentalAdditionalBlockPatterns, @@ -133,6 +143,7 @@ export default function BlockEditor( { setIsInserterOpen } ) { ...restStoredSettings, __experimentalBlockPatterns: blockPatterns, __experimentalBlockPatternCategories: blockPatternCategories, + __experimentalFetchPageEntities: fetchPagesEntities, }; }, [ storedSettings, blockPatterns, blockPatternCategories ] ); diff --git a/packages/edit-site/src/components/sidebar-edit-mode/navigation-item-editor/index.js b/packages/edit-site/src/components/sidebar-edit-mode/navigation-item-editor/index.js new file mode 100644 index 0000000000000..bb289e7b31983 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/navigation-item-editor/index.js @@ -0,0 +1,36 @@ +/** + * WordPress dependencies + */ +import { FlexBlock, Flex } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { navigation } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import DefaultSidebar from '../default-sidebar'; + +export default function NavigationItemSidebar() { + return ( + <DefaultSidebar + className="edit-site-navigation-item-sidebar" + identifier="edit-site/navigation-item" + title={ __( 'Navigation Item' ) } + icon={ navigation } + closeLabel={ __( 'Close navigation menu sidebar' ) } + panelClassName="edit-site-navigation-menu-sidebar__panel" + header={ + <Flex> + <FlexBlock> + <strong>{ __( 'Navigation Menus' ) }</strong> + <span className="edit-site-navigation-sidebar__beta"> + { __( 'Beta' ) } + </span> + </FlexBlock> + </Flex> + } + > + <p>Hello, world</p> + </DefaultSidebar> + ); +}