diff --git a/packages/edit-site/src/components/block-editor/block-editor-provider/default-block-editor-provider.js b/packages/edit-site/src/components/block-editor/block-editor-provider/default-block-editor-provider.js new file mode 100644 index 00000000000000..9ffee1ca687228 --- /dev/null +++ b/packages/edit-site/src/components/block-editor/block-editor-provider/default-block-editor-provider.js @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { useEntityBlockEditor } from '@wordpress/core-data'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; +import useSiteEditorSettings from '../use-site-editor-settings'; +import usePageContentBlocks from './use-page-content-blocks'; + +const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); + +const noop = () => {}; + +/** + * The default block editor provider for the site editor. Typically used when + * the post type is `'wp_template_part'` or `'wp_template'` and allows editing + * of the template and its nested entities. + * + * If the page content focus type is `'hideTemplate'`, the provider will provide + * a set of page content blocks wrapped in a container that, together, + * mimic the look and feel of the post editor and + * allow editing of the page content only. + * + * @param {Object} props + * @param {WPElement} props.children + */ +export default function DefaultBlockEditorProvider( { children } ) { + const settings = useSiteEditorSettings(); + + const { templateType, isTemplateHidden } = useSelect( ( select ) => { + const { getEditedPostType } = select( editSiteStore ); + const { getPageContentFocusType, getCanvasMode } = unlock( + select( editSiteStore ) + ); + return { + templateType: getEditedPostType(), + isTemplateHidden: + getCanvasMode() === 'edit' && + getPageContentFocusType() === 'hideTemplate', + canvasMode: unlock( select( editSiteStore ) ).getCanvasMode(), + }; + }, [] ); + + const [ blocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + templateType + ); + const pageContentBlock = usePageContentBlocks( blocks, isTemplateHidden ); + return ( + + { children } + + ); +} diff --git a/packages/edit-site/src/components/block-editor/block-editor-provider/index.js b/packages/edit-site/src/components/block-editor/block-editor-provider/index.js new file mode 100644 index 00000000000000..36c7c72066a88d --- /dev/null +++ b/packages/edit-site/src/components/block-editor/block-editor-provider/index.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; +import DefaultBlockEditorProvider from './default-block-editor-provider'; +import NavigationBlockEditorProvider from './navigation-block-editor-provider'; + +export default function BlockEditorProvider( { children } ) { + const entityType = useSelect( + ( select ) => select( editSiteStore ).getEditedPostType(), + [] + ); + if ( entityType === 'wp_navigation' ) { + return ( + + { children } + + ); + } + return ( + { children } + ); +} diff --git a/packages/edit-site/src/components/block-editor/providers/navigation-block-editor-provider.js b/packages/edit-site/src/components/block-editor/block-editor-provider/navigation-block-editor-provider.js similarity index 100% rename from packages/edit-site/src/components/block-editor/providers/navigation-block-editor-provider.js rename to packages/edit-site/src/components/block-editor/block-editor-provider/navigation-block-editor-provider.js diff --git a/packages/edit-site/src/components/block-editor/block-editor-provider/test/use-page-content-blocks.js b/packages/edit-site/src/components/block-editor/block-editor-provider/test/use-page-content-blocks.js new file mode 100644 index 00000000000000..533ed0f8f3d100 --- /dev/null +++ b/packages/edit-site/src/components/block-editor/block-editor-provider/test/use-page-content-blocks.js @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import { renderHook } from '@testing-library/react'; +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import usePageContentBlocks from '../use-page-content-blocks'; + +jest.mock( '@wordpress/blocks', () => { + return { + __esModule: true, + ...jest.requireActual( '@wordpress/blocks' ), + createBlock( name, attributes = {}, innerBlocks = [] ) { + return { + name, + attributes, + innerBlocks, + }; + }, + }; +} ); + +describe( 'usePageContentBlocks', () => { + const blocksList = [ + createBlock( 'core/group', {}, [ + createBlock( 'core/post-title' ), + createBlock( 'core/post-featured-image' ), + createBlock( 'core/query', {}, [ + createBlock( 'core/post-title' ), + createBlock( 'core/post-featured-image' ), + createBlock( 'core/post-content' ), + ] ), + createBlock( 'core/post-content' ), + ] ), + createBlock( 'core/query' ), + createBlock( 'core/paragraph' ), + createBlock( 'core/post-content' ), + ]; + it( 'should return empty array if `isPageContentFocused` is `false`', () => { + const { result } = renderHook( () => + usePageContentBlocks( blocksList, false ) + ); + expect( result.current ).toEqual( [] ); + } ); + it( 'should return empty array if `blocks` is undefined', () => { + const { result } = renderHook( () => + usePageContentBlocks( undefined, true ) + ); + expect( result.current ).toEqual( [] ); + } ); + it( 'should return empty array if `blocks` is an empty array', () => { + const { result } = renderHook( () => usePageContentBlocks( [], true ) ); + expect( result.current ).toEqual( [] ); + } ); + it( 'should return new block list', () => { + const { result } = renderHook( () => + usePageContentBlocks( blocksList, true ) + ); + expect( result.current ).toEqual( [ + { + name: 'core/group', + attributes: { + layout: { type: 'constrained' }, + style: { + spacing: { + margin: { + top: '4em', // Mimics the post editor. + }, + }, + }, + }, + innerBlocks: [ + createBlock( 'core/post-title' ), + createBlock( 'core/post-featured-image' ), + createBlock( 'core/post-content' ), + createBlock( 'core/post-content' ), + ], + }, + ] ); + } ); +} ); diff --git a/packages/edit-site/src/components/block-editor/block-editor-provider/use-page-content-blocks.js b/packages/edit-site/src/components/block-editor/block-editor-provider/use-page-content-blocks.js new file mode 100644 index 00000000000000..17e907113c5155 --- /dev/null +++ b/packages/edit-site/src/components/block-editor/block-editor-provider/use-page-content-blocks.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { PAGE_CONTENT_BLOCK_TYPES } from '../../../utils/constants'; + +/** + * Helper method to iterate through all blocks, recursing into allowed inner blocks. + * Returns a flattened object of transformed blocks. + * + * @param {Array} blocks Blocks to flatten. + * @param {Function} transform Transforming function to be applied to each block. If transform returns `undefined`, the block is skipped. + * + * @return {Array} Flattened object. + */ +function flattenBlocks( blocks, transform ) { + const result = []; + for ( let i = 0; i < blocks.length; i++ ) { + // Since the Query Block could contain PAGE_CONTENT_BLOCK_TYPES block types, + // we skip it because we only want to render stand-alone page content blocks in the block list. + if ( [ 'core/query' ].includes( blocks[ i ].name ) ) { + continue; + } + const transformedBlock = transform( blocks[ i ] ); + if ( transformedBlock ) { + result.push( transformedBlock ); + } + result.push( ...flattenBlocks( blocks[ i ].innerBlocks, transform ) ); + } + + return result; +} + +/** + * Returns a memoized array of blocks that contain only page content blocks, + * surrounded by a group block to mimic the post editor. + * + * @param {Array} blocks Block list. + * @param {boolean} isPageContentFocused Whether the page content has focus. If `true` return page content blocks. Default `false`. + * + * @return {Array} Page content blocks. + */ +export default function usePageContentBlocks( + blocks, + isPageContentFocused = false +) { + return useMemo( () => { + if ( ! isPageContentFocused || ! blocks || ! blocks.length ) { + return []; + } + return [ + createBlock( + 'core/group', + { + layout: { type: 'constrained' }, + style: { + spacing: { + margin: { + top: '4em', // Mimics the post editor. + }, + }, + }, + }, + flattenBlocks( blocks, ( block ) => { + if ( PAGE_CONTENT_BLOCK_TYPES[ block.name ] ) { + return createBlock( block.name ); + } + } ) + ), + ]; + }, [ blocks, isPageContentFocused ] ); +} diff --git a/packages/edit-site/src/components/block-editor/get-block-editor-provider.js b/packages/edit-site/src/components/block-editor/get-block-editor-provider.js deleted file mode 100644 index df8185605f13aa..00000000000000 --- a/packages/edit-site/src/components/block-editor/get-block-editor-provider.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Internal dependencies - */ -import DefaultBlockEditor from './providers/default-block-editor-provider'; -import NavigationBlockEditor from './providers/navigation-block-editor-provider'; - -/** - * Factory to isolate choosing the appropriate block editor - * component to handle a given entity type. - * - * @param {string} entityType the entity type being edited - * @return {JSX.Element} the block editor component to use. - */ -export default function getBlockEditorProvider( entityType ) { - let Provider = null; - - switch ( entityType ) { - case 'wp_navigation': - Provider = NavigationBlockEditor; - break; - case 'wp_template': - case 'wp_template_part': - default: - Provider = DefaultBlockEditor; - break; - } - - return Provider; -} diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 5bfcdb012d1f7e..2c635ff860a5bd 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; import { BlockInspector } from '@wordpress/block-editor'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; @@ -10,31 +9,19 @@ import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; */ import TemplatePartConverter from '../template-part-converter'; import { SidebarInspectorFill } from '../sidebar-edit-mode'; -import { store as editSiteStore } from '../../store'; import SiteEditorCanvas from './site-editor-canvas'; -import getBlockEditorProvider from './get-block-editor-provider'; +import BlockEditorProvider from './block-editor-provider'; import { unlock } from '../../lock-unlock'; const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); export default function BlockEditor() { - const entityType = useSelect( - ( select ) => select( editSiteStore ).getEditedPostType(), - [] - ); - - // Choose the provider based on the entity type currently - // being edited. - const BlockEditorProvider = getBlockEditorProvider( entityType ); - return ( - - ); diff --git a/packages/edit-site/src/components/block-editor/providers/default-block-editor-provider.js b/packages/edit-site/src/components/block-editor/providers/default-block-editor-provider.js deleted file mode 100644 index 2ee0ae467f8d65..00000000000000 --- a/packages/edit-site/src/components/block-editor/providers/default-block-editor-provider.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * WordPress dependencies - */ -import { useEntityBlockEditor } from '@wordpress/core-data'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../../store'; -import { unlock } from '../../../lock-unlock'; -import useSiteEditorSettings from '../use-site-editor-settings'; - -const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); - -export default function DefaultBlockEditorProvider( { children } ) { - const settings = useSiteEditorSettings(); - - const { templateType } = useSelect( ( select ) => { - const { getEditedPostType } = unlock( select( editSiteStore ) ); - - return { - templateType: getEditedPostType(), - }; - }, [] ); - - const [ blocks, onInput, onChange ] = useEntityBlockEditor( - 'postType', - templateType - ); - - return ( - - { children } - - ); -} diff --git a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js index 9d28c01164f29b..f3e021ba885244 100644 --- a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js +++ b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js @@ -9,12 +9,7 @@ import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ - -const PAGE_CONTENT_BLOCK_TYPES = [ - 'core/post-title', - 'core/post-featured-image', - 'core/post-content', -]; +import { PAGE_CONTENT_BLOCK_TYPES } from '../../utils/constants'; /** * Component that when rendered, makes it so that the site editor allows only @@ -48,8 +43,7 @@ const withDisableNonPageContentBlocks = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const isDescendentOfQueryLoop = props.context.queryId !== undefined; const isPageContent = - PAGE_CONTENT_BLOCK_TYPES.includes( props.name ) && - ! isDescendentOfQueryLoop; + PAGE_CONTENT_BLOCK_TYPES[ props.name ] && ! isDescendentOfQueryLoop; const mode = isPageContent ? 'contentOnly' : undefined; useBlockEditingMode( mode ); return ; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js index 2295ee12f45049..9e0b9e37c33ff6 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js @@ -12,6 +12,7 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; +import { check } from '@wordpress/icons'; /** * Internal dependencies @@ -19,6 +20,7 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editSiteStore } from '../../../store'; import SwapTemplateButton from './swap-template-button'; import ResetDefaultTemplate from './reset-default-template'; +import { unlock } from '../../../lock-unlock'; const POPOVER_PROPS = { className: 'edit-site-page-panels-edit-template__dropdown', @@ -26,28 +28,40 @@ const POPOVER_PROPS = { }; export default function EditTemplate() { - const { hasResolved, template } = useSelect( ( select ) => { - const { getEditedPostContext, getEditedPostType, getEditedPostId } = - select( editSiteStore ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const _context = getEditedPostContext(); - const queryArgs = [ - 'postType', - getEditedPostType(), - getEditedPostId(), - ]; - return { - context: _context, - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - template: getEditedEntityRecord( ...queryArgs ), - }; - }, [] ); + const { hasResolved, template, isTemplateHidden } = useSelect( + ( select ) => { + const { getEditedPostContext, getEditedPostType, getEditedPostId } = + select( editSiteStore ); + const { getCanvasMode, getPageContentFocusType } = unlock( + select( editSiteStore ) + ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); + const _context = getEditedPostContext(); + const queryArgs = [ + 'postType', + getEditedPostType(), + getEditedPostId(), + ]; + return { + context: _context, + hasResolved: hasFinishedResolution( + 'getEditedEntityRecord', + queryArgs + ), + template: getEditedEntityRecord( ...queryArgs ), + isTemplateHidden: + getCanvasMode() === 'edit' && + getPageContentFocusType() === 'hideTemplate', + }; + }, + [] + ); const { setHasPageContentFocus } = useDispatch( editSiteStore ); + // Disable reason: `useDispatch` can't be called conditionally. + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const { setPageContentFocusType } = unlock( useDispatch( editSiteStore ) ); if ( ! hasResolved ) { return null; @@ -83,6 +97,20 @@ export default function EditTemplate() { + + { + setPageContentFocusType( + isTemplateHidden + ? 'disableTemplate' + : 'hideTemplate' + ); + } } + > + { __( 'Template preview' ) } + + ) } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index aedcf5e46ca9ea..5501fe49e5876b 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -79,3 +79,9 @@ width: 30%; } } + +.edit-site-page-panels-edit-template__dropdown { + .components-popover__content { + min-width: 240px; + } +} diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index f3dd4c10cec43e..3e2bfe2ee47b24 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -49,3 +49,22 @@ export const setEditorCanvasContainerView = view, } ); }; + +/** + * Sets the type of page content focus. Can be one of: + * + * - `'disableTemplate'`: Disable the blocks belonging to the page's template. + * - `'hideTemplate'`: Hide the blocks belonging to the page's template. + * + * @param {'disableTemplate'|'hideTemplate'} pageContentFocusType The type of page content focus. + * + * @return {Object} Action object. + */ +export const setPageContentFocusType = + ( pageContentFocusType ) => + ( { dispatch } ) => { + dispatch( { + type: 'SET_PAGE_CONTENT_FOCUS_TYPE', + pageContentFocusType, + } ); + }; diff --git a/packages/edit-site/src/store/private-selectors.js b/packages/edit-site/src/store/private-selectors.js index 1f1f6e999fdb29..0d4cf2b3eefdaa 100644 --- a/packages/edit-site/src/store/private-selectors.js +++ b/packages/edit-site/src/store/private-selectors.js @@ -1,3 +1,8 @@ +/** + * Internal dependencies + */ +import { hasPageContentFocus } from './selectors'; + /** * Returns the current canvas mode. * @@ -19,3 +24,20 @@ export function getCanvasMode( state ) { export function getEditorCanvasContainerView( state ) { return state.editorCanvasContainerView; } + +/** + * Returns the type of the current page content focus, or null if there is no + * page content focus. + * + * Possible values are: + * + * - `'disableTemplate'`: Disable the blocks belonging to the page's template. + * - `'hideTemplate'`: Hide the blocks belonging to the page's template. + * + * @param {Object} state Global application state. + * + * @return {'disableTemplate'|'hideTemplate'|null} Type of the current page content focus. + */ +export function getPageContentFocusType( state ) { + return hasPageContentFocus( state ) ? state.pageContentFocusType : null; +} diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index 4b4689e26c561e..e99c6dda1fc1d0 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -177,6 +177,23 @@ export function hasPageContentFocus( state = false, action ) { return state; } +/** + * Reducer used to track the type of page content focus. + * + * @param {string} state Current state. + * @param {Object} action Dispatched action. + * + * @return {string} Updated state. + */ +export function pageContentFocusType( state = 'disableTemplate', action ) { + switch ( action.type ) { + case 'SET_PAGE_CONTENT_FOCUS_TYPE': + return action.pageContentFocusType; + } + + return state; +} + export default combineReducers( { deviceType, settings, @@ -187,4 +204,5 @@ export default combineReducers( { canvasMode, editorCanvasContainerView, hasPageContentFocus, + pageContentFocusType, } ); diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js index d3816f6ac0ac2a..a5e47ec5bbbaf3 100644 --- a/packages/edit-site/src/store/test/reducer.js +++ b/packages/edit-site/src/store/test/reducer.js @@ -12,6 +12,7 @@ import { blockInserterPanel, listViewPanel, hasPageContentFocus, + pageContentFocusType, } from '../reducer'; import { setIsInserterOpened } from '../actions'; @@ -191,4 +192,21 @@ describe( 'state', () => { ).toBe( false ); } ); } ); + + describe( 'pageContentFocusType', () => { + it( 'defaults to disableTemplate', () => { + expect( pageContentFocusType( undefined, {} ) ).toBe( + 'disableTemplate' + ); + } ); + + it( 'can be set', () => { + expect( + pageContentFocusType( 'disableTemplate', { + type: 'SET_PAGE_CONTENT_FOCUS_TYPE', + pageContentFocusType: 'enableTemplate', + } ) + ).toBe( 'enableTemplate' ); + } ); + } ); } ); diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index d12c7f84cc356c..1dcbdc0cc6fa3b 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -32,6 +32,16 @@ export const FOCUSABLE_ENTITIES = [ PATTERN_TYPES.user, ]; +/** + * Block types that are considered to be page content. These are the only blocks + * editable when hasPageContentFocus() is true. + */ +export const PAGE_CONTENT_BLOCK_TYPES = { + 'core/post-title': true, + 'core/post-featured-image': true, + 'core/post-content': true, +}; + export const POST_TYPE_LABELS = { [ TEMPLATE_POST_TYPE ]: __( 'Template' ), [ TEMPLATE_PART_POST_TYPE ]: __( 'Template Part' ),