diff --git a/lib/blocks.php b/lib/blocks.php index d1428770e3540e..a7442d71e84098 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -63,6 +63,7 @@ function gutenberg_reregister_core_block_types() { 'shortcode.php' => 'core/shortcode', 'social-link.php' => 'core/social-link', 'tag-cloud.php' => 'core/tag-cloud', + 'page-list.php' => 'core/page-list', 'post-author.php' => 'core/post-author', 'post-comment.php' => 'core/post-comment', 'post-comment-author.php' => 'core/post-comment-author', diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 45449c927bac0d..2113c72dade398 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -27,6 +27,7 @@ @import "./navigation/editor.scss"; @import "./navigation-link/editor.scss"; @import "./nextpage/editor.scss"; +@import "./page-list/editor.scss"; @import "./paragraph/editor.scss"; @import "./post-content/editor.scss"; @import "./post-excerpt/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 376757f2bfb876..d470f555e20ba4 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -42,6 +42,7 @@ import * as list from './list'; import * as missing from './missing'; import * as more from './more'; import * as nextpage from './nextpage'; +import * as pageList from './page-list'; import * as preformatted from './preformatted'; import * as pullquote from './pullquote'; import * as reusableBlock from './block'; @@ -148,6 +149,7 @@ export const __experimentalGetCoreBlocks = () => [ missing, more, nextpage, + pageList, preformatted, pullquote, rss, diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js index c22be04525c8cd..9c801008d5bf3e 100644 --- a/packages/block-library/src/navigation/edit.js +++ b/packages/block-library/src/navigation/edit.js @@ -79,6 +79,7 @@ function Navigation( { 'core/navigation-link', 'core/search', 'core/social-links', + 'core/page-list', ], orientation: attributes.orientation || 'horizontal', renderAppender: diff --git a/packages/block-library/src/navigation/placeholder.js b/packages/block-library/src/navigation/placeholder.js index 28dc39fe0142c0..9f6c58267caab3 100644 --- a/packages/block-library/src/navigation/placeholder.js +++ b/packages/block-library/src/navigation/placeholder.js @@ -97,29 +97,6 @@ function convertMenuItemsToBlocks( menuItems ) { return mapMenuItemsToBlocks( menuTree ); } -/** - * Convert pages to blocks. - * - * @param {Object[]} pages An array of pages. - * - * @return {WPBlock[]} An array of blocks. - */ -function convertPagesToBlocks( pages ) { - if ( ! pages ) { - return null; - } - - return pages.map( ( { title, type, link: url, id } ) => - createBlock( 'core/navigation-link', { - type, - id, - url, - label: ! title.rendered ? __( '(no title)' ) : title.rendered, - opensInNewTab: false, - } ) - ); -} - function NavigationPlaceholder( { onCreate }, ref ) { const [ selectedMenu, setSelectedMenu ] = useState(); @@ -220,9 +197,9 @@ function NavigationPlaceholder( { onCreate }, ref ) { }; const onCreateAllPages = () => { - const blocks = convertPagesToBlocks( pages ); + const block = [ createBlock( 'core/page-list' ) ]; const selectNavigationBlock = true; - onCreate( blocks, selectNavigationBlock ); + onCreate( block, selectNavigationBlock ); }; useEffect( () => { diff --git a/packages/block-library/src/page-list/block.json b/packages/block-library/src/page-list/block.json new file mode 100644 index 00000000000000..1f7a1fe9993502 --- /dev/null +++ b/packages/block-library/src/page-list/block.json @@ -0,0 +1,20 @@ +{ + "apiVersion": 2, + "name": "core/page-list", + "category": "widgets", + "usesContext": [ + "textColor", + "customTextColor", + "backgroundColor", + "customBackgroundColor", + "fontSize", + "customFontSize", + "showSubmenuIcon" + ], + "supports": { + "reusable": false, + "html": false + }, + "editorStyle": "wp-block-page-list-editor", + "style": "wp-block-page-list" +} diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js new file mode 100644 index 00000000000000..8d9b42603b3545 --- /dev/null +++ b/packages/block-library/src/page-list/edit.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ + +import { useBlockProps } from '@wordpress/block-editor'; +import ServerSideRender from '@wordpress/server-side-render'; + +export default function PageListEdit( { context } ) { + const { textColor, backgroundColor, showSubmenuIcon } = context || {}; + + const blockProps = useBlockProps( { + className: classnames( { + 'has-text-color': !! textColor, + [ `has-${ textColor }-color` ]: !! textColor, + 'has-background': !! backgroundColor, + [ `has-${ backgroundColor }-background-color` ]: !! backgroundColor, + 'show-submenu-icons': !! showSubmenuIcon, + } ), + } ); + + return ( +
+ +
+ ); +} diff --git a/packages/block-library/src/page-list/editor.scss b/packages/block-library/src/page-list/editor.scss new file mode 100644 index 00000000000000..1cda12d87ac8ca --- /dev/null +++ b/packages/block-library/src/page-list/editor.scss @@ -0,0 +1,29 @@ +.wp-block-navigation { + // Block wrapper gets the classes in the editor, and there's an extra div wrapper for now, so background styles need to be inherited. + .wp-block-page-list > div, + .wp-block-page-list { + background-color: inherit; + } + // Make the dropdown background white if there's no background color set. + &:not(.has-background) { + .submenu-container { + color: $gray-900; + background-color: $white; + } + } +} + +// Make links unclickable in the editor +.wp-block-pages-list__item__link { + pointer-events: none; +} + +.wp-block-page-list .components-placeholder { + min-height: 0; + padding: 0; + background-color: inherit; + + .components-spinner { + margin-top: 0; + } +} diff --git a/packages/block-library/src/page-list/index.js b/packages/block-library/src/page-list/index.js new file mode 100644 index 00000000000000..f75a945716b501 --- /dev/null +++ b/packages/block-library/src/page-list/index.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { pages as icon } from '@wordpress/icons'; +import { __, _x } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit.js'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + title: _x( 'Page List', 'block title' ), + description: __( 'Display a list of all pages.' ), + keywords: [ __( 'menu' ), __( 'navigation' ) ], + icon, + example: {}, + edit, +}; diff --git a/packages/block-library/src/page-list/index.php b/packages/block-library/src/page-list/index.php new file mode 100644 index 00000000000000..bd607ad4879d9b --- /dev/null +++ b/packages/block-library/src/page-list/index.php @@ -0,0 +1,217 @@ + array(), + 'inline_styles' => '', + ); + + // Text color. + $has_named_text_color = array_key_exists( 'textColor', $context ); + $has_custom_text_color = array_key_exists( 'customTextColor', $context ); + + // If has text color. + if ( $has_custom_text_color || $has_named_text_color ) { + // Add has-text-color class. + $colors['css_classes'][] = 'has-text-color'; + } + + if ( $has_named_text_color ) { + // Add the color class. + $colors['css_classes'][] = sprintf( 'has-%s-color', $context['textColor'] ); + } elseif ( $has_custom_text_color ) { + // Add the custom color inline style. + $colors['inline_styles'] .= sprintf( 'color: %s;', $context['customTextColor'] ); + } + + // Background color. + $has_named_background_color = array_key_exists( 'backgroundColor', $context ); + $has_custom_background_color = array_key_exists( 'customBackgroundColor', $context ); + + // If has background color. + if ( $has_custom_background_color || $has_named_background_color ) { + // Add has-background class. + $colors['css_classes'][] = 'has-background'; + } + + if ( $has_named_background_color ) { + // Add the background-color class. + $colors['css_classes'][] = sprintf( 'has-%s-background-color', $context['backgroundColor'] ); + } elseif ( $has_custom_background_color ) { + // Add the custom background-color inline style. + $colors['inline_styles'] .= sprintf( 'background-color: %s;', $context['customBackgroundColor'] ); + } + + return $colors; +} + +/** + * Build an array with CSS classes and inline styles defining the font sizes + * which will be applied to the pages markup in the front-end when it is a descendant of navigation. + * + * @param array $context Navigation block context. + * @return array Font size CSS classes and inline styles. + */ +function block_core_page_list_build_css_font_sizes( $context ) { + // CSS classes. + $font_sizes = array( + 'css_classes' => array(), + 'inline_styles' => '', + ); + + $has_named_font_size = array_key_exists( 'fontSize', $context ); + $has_custom_font_size = array_key_exists( 'customFontSize', $context ); + + if ( $has_named_font_size ) { + // Add the font size class. + $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $context['fontSize'] ); + } elseif ( $has_custom_font_size ) { + // Add the custom font size inline style. + $font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $context['customFontSize'] ); + } + + return $font_sizes; +} + +/** + * Outputs Page list markup from an array of pages with nested children. + * + * @param array $nested_pages The array of nested pages. + * + * @return string List markup. + */ +function render_nested_page_list( $nested_pages ) { + if ( empty( $nested_pages ) ) { + return; + } + $markup = ''; + foreach ( (array) $nested_pages as $page ) { + $css_class = 'wp-block-pages-list__item'; + if ( isset( $page['children'] ) ) { + $css_class .= ' has-child'; + } + $markup .= '
  • '; + $markup .= '' . wp_kses( + $page['title'], + wp_kses_allowed_html( 'post' ) + ) . ''; + if ( isset( $page['children'] ) ) { + $markup .= ''; + $markup .= ''; + } + $markup .= '
  • '; + } + return $markup; +} + +/** + * Outputs nested array of pages + * + * @param array $current_level The level being iterated through. + * @param array $children The children grouped by parent post ID. + * + * @return array The nested array of pages. + */ +function nest_pages( $current_level, $children ) { + if ( empty( $current_level ) ) { + return; + } + foreach ( (array) $current_level as $key => $current ) { + if ( isset( $children[ $key ] ) ) { + $current_level[ $key ]['children'] = nest_pages( $children[ $key ], $children ); + } + } + return $current_level; +} + +/** + * Renders the `core/page-list` block on server. + * + * @param array $attributes The block attributes. + * @param array $content The saved content. + * @param array $block The parsed block. + * + * @return string Returns the page list markup. + */ +function render_block_core_page_list( $attributes, $content, $block ) { + static $block_id = 0; + $block_id++; + + $all_pages = get_pages( array( 'sort_column' => 'menu_order' ) ); + + $top_level_pages = array(); + + $pages_with_children = array(); + + foreach ( (array) $all_pages as $page ) { + if ( $page->post_parent ) { + $pages_with_children[ $page->post_parent ][ $page->ID ] = array( + 'title' => $page->post_title, + 'link' => get_permalink( $page->ID ), + ); + } else { + $top_level_pages[ $page->ID ] = array( + 'title' => $page->post_title, + 'link' => get_permalink( $page->ID ), + ); + + } + } + + $nested_pages = nest_pages( $top_level_pages, $pages_with_children ); + + $wrapper_markup = ''; + + $items_markup = render_nested_page_list( $nested_pages ); + + $colors = block_core_page_list_build_css_colors( $block->context ); + $font_sizes = block_core_page_list_build_css_font_sizes( $block->context ); + $classes = array_merge( + $colors['css_classes'], + $font_sizes['css_classes'] + ); + $style_attribute = ( $colors['inline_styles'] || $font_sizes['inline_styles'] ); + $css_classes = trim( implode( ' ', $classes ) ); + + if ( $block->context && $block->context['showSubmenuIcon'] ) { + $css_classes .= ' show-submenu-icons'; + } + + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => $css_classes, + 'style' => $style_attribute, + ) + ); + + return sprintf( + $wrapper_markup, + $wrapper_attributes, + $items_markup + ); +} + + /** + * Registers the `core/pages` block on server. + */ +function register_block_core_page_list() { + register_block_type_from_metadata( + __DIR__ . '/page-list', + array( + 'render_callback' => 'render_block_core_page_list', + ) + ); +} + add_action( 'init', 'register_block_core_page_list' ); diff --git a/packages/block-library/src/page-list/style.scss b/packages/block-library/src/page-list/style.scss new file mode 100644 index 00000000000000..9c65b730255b2d --- /dev/null +++ b/packages/block-library/src/page-list/style.scss @@ -0,0 +1,103 @@ +.wp-block-page-list__submenu-icon { + display: none; +} + +.show-submenu-icons { + .wp-block-page-list__submenu-icon { + display: block; + padding: 0.375em 1em 0.375em 0; + + svg { + fill: currentColor; + } + } +} + +// The Pages block should inherit navigation styles when nested within it +.wp-block-navigation { + .wp-block-page-list { + display: flex; + flex-wrap: wrap; + } + .wp-block-pages-list__item__link { + display: block; + color: inherit; + padding: 0.5em 1em; + } + + .wp-block-pages-list__item.has-child { + display: flex; + position: relative; + background-color: inherit; + + > a { + padding-right: 0.5em; + } + > .submenu-container { + border: $border-width solid rgba(0, 0, 0, 0.15); + background-color: inherit; + color: inherit; + position: absolute; + left: 0; + top: 100%; + width: fit-content; + z-index: 2; + opacity: 0; + transition: opacity 0.1s linear; + visibility: hidden; + + @include break-medium { + left: 1.5em; + + // Nested submenus sit to the left on large breakpoints + .submenu-container { + left: 100%; + top: -1px; + + // Prevent the menu from disappearing when the mouse is over the gap + &::before { + content: ""; + position: absolute; + right: 100%; + height: 100%; + display: block; + width: 0.5em; + background: transparent; + } + } + + .wp-block-navigation-link__submenu-icon svg { + transform: rotate(0); + } + } + } + + &:hover { + cursor: pointer; + + > .submenu-container { + visibility: visible; + opacity: 1; + } + } + + &:focus-within { + cursor: pointer; + + > .submenu-container { + visibility: visible; + opacity: 1; + } + } + } + + .submenu-container { + padding: 0; + } +} + +.is-vertical .wp-block-navigation__container { + .wp-block-page-list { + display: block; + } +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index a8d23e1563793d..f4c68ba875bb4f 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -23,6 +23,7 @@ @import "./media-text/style.scss"; @import "./navigation/style.scss"; @import "./navigation-link/style.scss"; +@import "./page-list/style.scss"; @import "./paragraph/style.scss"; @import "./post-author/style.scss"; @import "./post-comments-form/style.scss"; diff --git a/packages/e2e-tests/fixtures/blocks/core__page-list.html b/packages/e2e-tests/fixtures/blocks/core__page-list.html new file mode 100644 index 00000000000000..c17450f4f34a99 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__page-list.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/blocks/core__page-list.json b/packages/e2e-tests/fixtures/blocks/core__page-list.json new file mode 100644 index 00000000000000..df189be152f5bf --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__page-list.json @@ -0,0 +1,10 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/page-list", + "isValid": true, + "attributes": {}, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__page-list.parsed.json b/packages/e2e-tests/fixtures/blocks/core__page-list.parsed.json new file mode 100644 index 00000000000000..3cc84a0f5c4edd --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__page-list.parsed.json @@ -0,0 +1,9 @@ +[ + { + "blockName": "core/page-list", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__page-list.serialized.html b/packages/e2e-tests/fixtures/blocks/core__page-list.serialized.html new file mode 100644 index 00000000000000..8f4e614a2eaa09 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__page-list.serialized.html @@ -0,0 +1 @@ + diff --git a/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap b/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap index a611ece15ba24f..67b195b021c0e1 100644 --- a/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap +++ b/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap @@ -2,11 +2,7 @@ exports[`Navigation editor allows creation of a menu 1`] = ` " - - - - - + " `; diff --git a/packages/e2e-tests/specs/experiments/navigation-editor.test.js b/packages/e2e-tests/specs/experiments/navigation-editor.test.js index 44a6d5a8029867..757f7e753523b4 100644 --- a/packages/e2e-tests/specs/experiments/navigation-editor.test.js +++ b/packages/e2e-tests/specs/experiments/navigation-editor.test.js @@ -29,21 +29,6 @@ const menusFixture = [ }, ]; -const pagesFixture = [ - { - title: 'Home', - slug: 'home', - }, - { - title: 'About', - slug: 'about', - }, - { - title: 'Contact', - slug: 'contact', - }, -]; - // Matching against variations of the same URL encoded and non-encoded // produces the most reliable mocking. const REST_MENUS_ROUTES = [ @@ -54,10 +39,6 @@ const REST_MENU_ITEMS_ROUTES = [ '/__experimental/menu-items', `rest_route=${ encodeURIComponent( '/__experimental/menu-items' ) }`, ]; -const REST_PAGES_ROUTES = [ - '/wp/v2/pages', - `rest_route=${ encodeURIComponent( '/wp/v2/pages' ) }`, -]; /** * Determines if a given URL matches any of a given collection of @@ -99,18 +80,6 @@ function assignMockMenuIds( menus ) { : []; } -function createMockPages( pages ) { - return pages.map( ( { title, slug }, index ) => ( { - id: index + 1, - type: 'page', - link: `https://this/is/a/test/page/${ slug }`, - title: { - rendered: title, - raw: title, - }, - } ) ); -} - function getMenuMocks( responsesByMethod ) { return getEndpointMocks( REST_MENUS_ROUTES, responsesByMethod ); } @@ -119,10 +88,6 @@ function getMenuItemMocks( responsesByMethod ) { return getEndpointMocks( REST_MENU_ITEMS_ROUTES, responsesByMethod ); } -function getPagesMocks( responsesByMethod ) { - return getEndpointMocks( REST_PAGES_ROUTES, responsesByMethod ); -} - async function visitNavigationEditor() { const query = addQueryArgs( '', { page: 'gutenberg-navigation', @@ -143,8 +108,6 @@ describe( 'Navigation editor', () => { } ); it( 'allows creation of a menu', async () => { - const pagesResponse = createMockPages( pagesFixture ); - const menuResponse = { id: 4, description: '', @@ -158,7 +121,6 @@ describe( 'Navigation editor', () => { await setUpResponseMocking( [ ...getMenuMocks( { GET: [] } ), ...getMenuItemMocks( { GET: [] } ), - ...getPagesMocks( { GET: pagesResponse } ), ] ); await visitNavigationEditor(); @@ -172,7 +134,6 @@ describe( 'Navigation editor', () => { POST: menuResponse, } ), ...getMenuItemMocks( { GET: [] } ), - ...getPagesMocks( { GET: pagesResponse } ), ] ); // Add a new menu. diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 346c1cb42eba3a..3e8aa25dd641dc 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -119,6 +119,7 @@ export { default as moreVertical } from './library/more-vertical'; export { default as navigation } from './library/navigation'; export { default as pageBreak } from './library/page-break'; export { default as page } from './library/page'; +export { default as pages } from './library/pages'; export { default as paragraph } from './library/paragraph'; export { default as payment } from './library/payment'; export { default as percent } from './library/percent'; diff --git a/packages/icons/src/library/pages.js b/packages/icons/src/library/pages.js new file mode 100644 index 00000000000000..7e7dff92cc71d6 --- /dev/null +++ b/packages/icons/src/library/pages.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const pages = ( + + + +); + +export default pages;