diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 4774934651b139..e8cbad2518c2e5 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -256,6 +256,18 @@ _Returns_ - `string`: Post type. +### getCurrentTemplateId + +Returns the template ID currently being rendered/edited + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `string?`: Template ID. + ### getEditedPostAttribute Returns a single attribute of the post being edited, preferring the unsaved edit if one exists, but falling back to the attribute for the last known saved state of the post. diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 25dcf941970aca..5b9290fff51375 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -6,24 +6,21 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { PostTitle, store as editorStore } from '@wordpress/editor'; import { - BlockList, + store as editorStore, + privateApis as editorPrivateApis, +} from '@wordpress/editor'; +import { BlockTools, - store as blockEditorStore, __unstableUseTypewriter as useTypewriter, - __unstableUseTypingObserver as useTypingObserver, __experimentalUseResizeCanvas as useResizeCanvas, - useSettings, - __experimentalRecursionProvider as RecursionProvider, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { useEffect, useRef, useMemo } from '@wordpress/element'; +import { useRef, useMemo, useEffect } from '@wordpress/element'; import { __unstableMotion as motion } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useMergeRefs } from '@wordpress/compose'; -import { parse, store as blocksStore } from '@wordpress/blocks'; -import { store as coreStore } from '@wordpress/core-data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies @@ -31,128 +28,46 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { - LayoutStyle, - useLayoutClasses, - useLayoutStyles, - ExperimentalBlockCanvas: BlockCanvas, -} = unlock( blockEditorPrivateApis ); +const { ExperimentalBlockCanvas: BlockCanvas } = unlock( + blockEditorPrivateApis +); +const { EditorCanvas } = unlock( editorPrivateApis ); const isGutenbergPlugin = process.env.IS_GUTENBERG_PLUGIN ? true : false; -/** - * Given an array of nested blocks, find the first Post Content - * block inside it, recursing through any nesting levels, - * and return its attributes. - * - * @param {Array} blocks A list of blocks. - * - * @return {Object | undefined} The Post Content block. - */ -function getPostContentAttributes( blocks ) { - for ( let i = 0; i < blocks.length; i++ ) { - if ( blocks[ i ].name === 'core/post-content' ) { - return blocks[ i ].attributes; - } - if ( blocks[ i ].innerBlocks.length ) { - const nestedPostContent = getPostContentAttributes( - blocks[ i ].innerBlocks - ); - - if ( nestedPostContent ) { - return nestedPostContent; - } - } - } -} - -function checkForPostContentAtRootLevel( blocks ) { - for ( let i = 0; i < blocks.length; i++ ) { - if ( blocks[ i ].name === 'core/post-content' ) { - return true; - } - } - return false; -} - export default function VisualEditor( { styles } ) { const { deviceType, isWelcomeGuideVisible, isTemplateMode, - postContentAttributes, - editedPostTemplate = {}, - wrapperBlockName, - wrapperUniqueId, isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { const { isFeatureActive, isEditingTemplate, - getEditedPostTemplate, __experimentalGetPreviewDeviceType, } = select( editPostStore ); - const { getCurrentPostId, getCurrentPostType, getEditorSettings } = - select( editorStore ); + const { getEditorSettings } = select( editorStore ); const { getBlockTypes } = select( blocksStore ); const _isTemplateMode = isEditingTemplate(); - const postTypeSlug = getCurrentPostType(); - let _wrapperBlockName; - - if ( postTypeSlug === 'wp_block' ) { - _wrapperBlockName = 'core/block'; - } else if ( ! _isTemplateMode ) { - _wrapperBlockName = 'core/post-content'; - } - const editorSettings = getEditorSettings(); - const supportsTemplateMode = editorSettings.supportsTemplateMode; - const postType = select( coreStore ).getPostType( postTypeSlug ); - const canEditTemplate = select( coreStore ).canUser( - 'create', - 'templates' - ); return { deviceType: __experimentalGetPreviewDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), isTemplateMode: _isTemplateMode, - postContentAttributes: getEditorSettings().postContentAttributes, - // Post template fetch returns a 404 on classic themes, which - // messes with e2e tests, so check it's a block theme first. - editedPostTemplate: - postType?.viewable && supportsTemplateMode && canEditTemplate - ? getEditedPostTemplate() - : undefined, - wrapperBlockName: _wrapperBlockName, - wrapperUniqueId: getCurrentPostId(), isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, hasV3BlocksOnly: getBlockTypes().every( ( type ) => { return type.apiVersion >= 3; } ), }; }, [] ); - const { isCleanNewPost } = useSelect( editorStore ); const hasMetaBoxes = useSelect( ( select ) => select( editPostStore ).hasMetaBoxes(), [] ); - const { - hasRootPaddingAwareAlignments, - isFocusMode, - themeHasDisabledLayoutStyles, - themeSupportsLayout, - } = useSelect( ( select ) => { - const _settings = select( blockEditorStore ).getSettings(); - return { - themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, - themeSupportsLayout: _settings.supportsLayout, - isFocusMode: _settings.focusMode, - hasRootPaddingAwareAlignments: - _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, - }; - }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); const desktopCanvasStyles = { height: '100%', width: '100%', @@ -171,7 +86,6 @@ export default function VisualEditor( { styles } ) { borderBottom: 0, }; const resizedCanvasStyles = useResizeCanvas( deviceType, isTemplateMode ); - const [ globalLayoutSettings ] = useSettings( 'layout' ); const previewMode = 'is-' + deviceType.toLowerCase() + '-preview'; let animatedStyles = isTemplateMode @@ -192,143 +106,19 @@ export default function VisualEditor( { styles } ) { const ref = useRef(); const contentRef = useMergeRefs( [ ref, useTypewriter() ] ); - // fallbackLayout is used if there is no Post Content, - // and for Post Title. - const fallbackLayout = useMemo( () => { - if ( isTemplateMode ) { - return { type: 'default' }; - } - - if ( themeSupportsLayout ) { - // We need to ensure support for wide and full alignments, - // so we add the constrained type. - return { ...globalLayoutSettings, type: 'constrained' }; - } - // Set default layout for classic themes so all alignments are supported. - return { type: 'default' }; - }, [ isTemplateMode, themeSupportsLayout, globalLayoutSettings ] ); - - const newestPostContentAttributes = useMemo( () => { - if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) { - return postContentAttributes; - } - // When in template editing mode, we can access the blocks directly. - if ( editedPostTemplate?.blocks ) { - return getPostContentAttributes( editedPostTemplate?.blocks ); - } - // If there are no blocks, we have to parse the content string. - // Best double-check it's a string otherwise the parse function gets unhappy. - const parseableContent = - typeof editedPostTemplate?.content === 'string' - ? editedPostTemplate?.content - : ''; - - return getPostContentAttributes( parse( parseableContent ) ) || {}; - }, [ - editedPostTemplate?.content, - editedPostTemplate?.blocks, - postContentAttributes, - ] ); - - const hasPostContentAtRootLevel = useMemo( () => { - if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) { - return false; - } - // When in template editing mode, we can access the blocks directly. - if ( editedPostTemplate?.blocks ) { - return checkForPostContentAtRootLevel( editedPostTemplate?.blocks ); - } - // If there are no blocks, we have to parse the content string. - // Best double-check it's a string otherwise the parse function gets unhappy. - const parseableContent = - typeof editedPostTemplate?.content === 'string' - ? editedPostTemplate?.content - : ''; - - return ( - checkForPostContentAtRootLevel( parse( parseableContent ) ) || false - ); - }, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] ); - - const { layout = {}, align = '' } = newestPostContentAttributes || {}; - - const postContentLayoutClasses = useLayoutClasses( - newestPostContentAttributes, - 'core/post-content' - ); - - const blockListLayoutClass = classnames( - { - 'is-layout-flow': ! themeSupportsLayout, - }, - themeSupportsLayout && postContentLayoutClasses, - align && `align${ align }` - ); - - const postContentLayoutStyles = useLayoutStyles( - newestPostContentAttributes, - 'core/post-content', - '.block-editor-block-list__layout.is-root-container' - ); - - // Update type for blocks using legacy layouts. - const postContentLayout = useMemo( () => { - return layout && - ( layout?.type === 'constrained' || - layout?.inherit || - layout?.contentSize || - layout?.wideSize ) - ? { ...globalLayoutSettings, ...layout, type: 'constrained' } - : { ...globalLayoutSettings, ...layout, type: 'default' }; - }, [ - layout?.type, - layout?.inherit, - layout?.contentSize, - layout?.wideSize, - globalLayoutSettings, - ] ); - - // If there is a Post Content block we use its layout for the block list; - // if not, this must be a classic theme, in which case we use the fallback layout. - const blockListLayout = postContentAttributes - ? postContentLayout - : fallbackLayout; - - const postEditorLayout = - blockListLayout?.type === 'default' && ! hasPostContentAtRootLevel - ? fallbackLayout - : blockListLayout; - - const observeTypingRef = useTypingObserver(); - const titleRef = useRef(); - useEffect( () => { - if ( isWelcomeGuideVisible || ! isCleanNewPost() ) { - return; - } - titleRef?.current?.focus(); - }, [ isWelcomeGuideVisible, isCleanNewPost ] ); - styles = useMemo( () => [ ...styles, { // We should move this in to future to the body. - css: - `.edit-post-visual-editor__post-title-wrapper{margin-top:4rem}` + - ( paddingBottom - ? `body{padding-bottom:${ paddingBottom }}` - : '' ), + css: paddingBottom + ? `body{padding-bottom:${ paddingBottom }}` + : '', }, ], [ styles ] ); - // Add some styles for alignwide/alignfull Post Content and its children. - const alignCSS = `.is-root-container.alignwide { max-width: var(--wp--style--global--wide-size); margin-left: auto; margin-right: auto;} - .is-root-container.alignwide:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: var(--wp--style--global--wide-size);} - .is-root-container.alignfull { max-width: none; margin-left: auto; margin-right: auto;} - .is-root-container.alignfull:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: none;}`; - const isToBeIframed = ( ( hasV3BlocksOnly || ( isGutenbergPlugin && isBlockBasedTheme ) ) && ! hasMetaBoxes ) || @@ -336,6 +126,14 @@ export default function VisualEditor( { styles } ) { deviceType === 'Tablet' || deviceType === 'Mobile'; + useEffect( () => { + if ( isTemplateMode ) { + setRenderingMode( 'all' ); + } else { + setRenderingMode( 'post-only' ); + } + }, [ isTemplateMode, setRenderingMode ] ); + return ( - { themeSupportsLayout && - ! themeHasDisabledLayoutStyles && - ! isTemplateMode && ( - <> - - - { align && ( - - ) } - { postContentLayoutStyles && ( - - ) } - - ) } - { ! isTemplateMode && ( -
- -
- ) } - - - + diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index 237bbf25f2c79a..46838c97f8799c 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -38,21 +38,6 @@ // See also https://www.w3.org/TR/CSS22/visudet.html#the-height-property. } -// Ideally this wrapper div is not needed but if we want to match the positioning of blocks -// .block-editor-block-list__layout and block-editor-block-list__block -// We need to have two DOM elements. -.edit-post-visual-editor__post-title-wrapper { - .editor-post-title { - // Center. - margin-left: auto; - margin-right: auto; - } - - // Add extra margin at the top, to push down the Title area in the post editor. - margin-top: 4rem; - margin-bottom: var(--wp--style--block-gap); -} - .edit-post-visual-editor__content-area { width: 100%; height: 100%; diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index d1ef111e7dc4c3..cff867c3f7a2cb 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -36,13 +36,11 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { hiddenBlockTypes, blockTypes, keepCaretInsideBlock, - isTemplateMode, template, } = useSelect( ( select ) => { const { isFeatureActive, - isEditingTemplate, getEditedPostTemplate, getHiddenBlockTypes, } = select( editPostStore ); @@ -81,7 +79,6 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { hiddenBlockTypes: getHiddenBlockTypes(), blockTypes: getBlockTypes(), keepCaretInsideBlock: isFeatureActive( 'keepCaretInsideBlock' ), - isTemplateMode: isEditingTemplate(), template: supportsTemplateMode && isViewable && canEditTemplate ? getEditedPostTemplate() @@ -156,7 +153,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { post={ post } initialEdits={ initialEdits } useSubRegistry={ false } - __unstableTemplate={ isTemplateMode ? template : undefined } + __unstableTemplate={ template } { ...props } > diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 235eaf6617aa8f..15d638aa329e12 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -15,16 +15,22 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; +import { + FOCUSABLE_ENTITIES, + NAVIGATION_POST_TYPE, +} from '../../utils/constants'; const { ExperimentalBlockCanvas: BlockCanvas } = unlock( blockEditorPrivateApis ); +const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); function EditorCanvas( { enableResizing, @@ -33,17 +39,32 @@ function EditorCanvas( { contentRef, ...props } ) { - const { canvasMode, deviceType, isZoomOutMode } = useSelect( - ( select ) => ( { - deviceType: - select( editSiteStore ).__experimentalGetPreviewDeviceType(), - isZoomOutMode: - select( blockEditorStore ).__unstableGetEditorMode() === - 'zoom-out', - canvasMode: unlock( select( editSiteStore ) ).getCanvasMode(), - } ), - [] - ); + const { + hasBlocks, + isFocusMode, + templateType, + canvasMode, + deviceType, + isZoomOutMode, + } = useSelect( ( select ) => { + const { getBlockCount, __unstableGetEditorMode } = + select( blockEditorStore ); + const { + getEditedPostType, + __experimentalGetPreviewDeviceType, + getCanvasMode, + } = unlock( select( editSiteStore ) ); + const _templateType = getEditedPostType(); + + return { + templateType: _templateType, + isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), + deviceType: __experimentalGetPreviewDeviceType(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + canvasMode: getCanvasMode(), + hasBlocks: !! getBlockCount(), + }; + }, [] ); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const deviceStyles = useResizeCanvas( deviceType ); const [ isFocused, setIsFocused ] = useState( false ); @@ -70,6 +91,15 @@ function EditorCanvas( { onClick: () => setCanvasMode( 'edit' ), readonly: true, }; + const isTemplateTypeNavigation = templateType === NAVIGATION_POST_TYPE; + const isNavigationFocusMode = isTemplateTypeNavigation && isFocusMode; + // Hide the appender when: + // - In navigation focus mode (should only allow the root Nav block). + // - In view mode (i.e. not editing). + const showBlockAppender = + ( isNavigationFocusMode && hasBlocks ) || canvasMode === 'view' + ? false + : undefined; return ( + { children } ); diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index 0d2d522c8b3e18..bfbb2d3eac43ff 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -7,12 +7,9 @@ import classnames from 'classnames'; */ import { useSelect, useDispatch } from '@wordpress/data'; import { useRef } from '@wordpress/element'; -import { - BlockList, - BlockTools, - store as blockEditorStore, -} from '@wordpress/block-editor'; +import { BlockTools, store as blockEditorStore } from '@wordpress/block-editor'; import { useViewportMatch, useResizeObserver } from '@wordpress/compose'; + /** * Internal dependencies */ @@ -29,12 +26,6 @@ import { import { unlock } from '../../lock-unlock'; import PageContentFocusNotifications from '../page-content-focus-notifications'; -const LAYOUT = { - type: 'default', - // At the root level of the site editor, no alignments should be allowed. - alignments: [], -}; - export default function SiteEditorCanvas() { const { clearSelectedBlock } = useDispatch( blockEditorStore ); @@ -56,16 +47,6 @@ export default function SiteEditorCanvas() { const settings = useSiteEditorSettings(); - const { hasBlocks } = useSelect( ( select ) => { - const { getBlockCount } = select( blockEditorStore ); - - const blocks = getBlockCount(); - - return { - hasBlocks: !! blocks, - }; - }, [] ); - const isMobileViewport = useViewportMatch( 'small', '<' ); const enableResizing = isFocusMode && @@ -75,17 +56,7 @@ export default function SiteEditorCanvas() { const contentRef = useRef(); const isTemplateTypeNavigation = templateType === NAVIGATION_POST_TYPE; - const isNavigationFocusMode = isTemplateTypeNavigation && isFocusMode; - - // Hide the appender when: - // - In navigation focus mode (should only allow the root Nav block). - // - In view mode (i.e. not editing). - const showBlockAppender = - ( isNavigationFocusMode && hasBlocks ) || isViewMode - ? false - : undefined; - const forceFullHeight = isNavigationFocusMode; return ( @@ -126,23 +97,6 @@ export default function SiteEditorCanvas() { contentRef={ contentRef } > { resizeObserver } -
diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index b110b1c274e779..6f64acdbccfc8e 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -14,7 +14,7 @@ // Navigation focus mode requires padding around the root Navigation block // for presentational purposes. -.edit-site-block-editor__block-list.is-navigation-block { +.edit-site-editor-canvas__block-list.is-navigation-block { padding: $grid-unit-30; } diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index cb3fb3f1cb3336..3cca41d67985c5 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -138,6 +138,7 @@ export function useSpecificEditorSettings() { return { ...settings, + supportsTemplateMode: true, __experimentalSetIsInserterOpened: setIsInserterOpened, focusMode: canvasMode === 'view' && focusMode ? false : focusMode, isDistractionFree, diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index df59dffe66be69..bbf4b55c052874 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -12,6 +12,7 @@ import { humanTimeDiff } from '@wordpress/date'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -22,28 +23,39 @@ import PageContent from './page-content'; import PageSummary from './page-summary'; export default function PagePanels() { - const { id, type, hasResolved, status, date, password, title, modified } = - useSelect( ( select ) => { - const { getEditedPostContext } = select( editSiteStore ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const context = getEditedPostContext(); - const queryArgs = [ 'postType', context.postType, context.postId ]; - const page = getEditedEntityRecord( ...queryArgs ); - return { - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - title: page?.title, - id: page?.id, - type: page?.type, - status: page?.status, - date: page?.date, - password: page?.password, - modified: page?.modified, - }; - }, [] ); + const { + id, + type, + hasResolved, + status, + date, + password, + title, + modified, + renderingMode, + } = useSelect( ( select ) => { + const { getEditedPostContext } = select( editSiteStore ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); + const { getRenderingMode } = select( editorStore ); + const context = getEditedPostContext(); + const queryArgs = [ 'postType', context.postType, context.postId ]; + const page = getEditedEntityRecord( ...queryArgs ); + return { + hasResolved: hasFinishedResolution( + 'getEditedEntityRecord', + queryArgs + ), + title: page?.title, + id: page?.id, + type: page?.type, + status: page?.status, + date: page?.date, + password: page?.password, + modified: page?.modified, + renderingMode: getRenderingMode(), + }; + }, [] ); if ( ! hasResolved ) { return null; @@ -77,9 +89,11 @@ export default function PagePanels() { postType={ type } /> - - - + { renderingMode !== 'post-only' && ( + + + + ) } ); } diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js new file mode 100644 index 00000000000000..906eb6b272fc78 --- /dev/null +++ b/packages/editor/src/components/editor-canvas/index.js @@ -0,0 +1,334 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + BlockList, + store as blockEditorStore, + __unstableUseTypingObserver as useTypingObserver, + useSettings, + __experimentalRecursionProvider as RecursionProvider, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { useEffect, useRef, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { parse } from '@wordpress/blocks'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import PostTitle from '../post-title'; +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +const { LayoutStyle, useLayoutClasses, useLayoutStyles } = unlock( + blockEditorPrivateApis +); + +/** + * Given an array of nested blocks, find the first Post Content + * block inside it, recursing through any nesting levels, + * and return its attributes. + * + * @param {Array} blocks A list of blocks. + * + * @return {Object | undefined} The Post Content block. + */ +function getPostContentAttributes( blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/post-content' ) { + return blocks[ i ].attributes; + } + if ( blocks[ i ].innerBlocks.length ) { + const nestedPostContent = getPostContentAttributes( + blocks[ i ].innerBlocks + ); + + if ( nestedPostContent ) { + return nestedPostContent; + } + } + } +} + +function checkForPostContentAtRootLevel( blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/post-content' ) { + return true; + } + } + return false; +} + +export default function EditorCanvas( { + // Ideally as we unify post and site editors, we won't need these props. + autoFocus, + dropZoneElement, + className, + renderAppender, +} ) { + const { + renderingMode, + postContentAttributes, + editedPostTemplate = {}, + wrapperBlockName, + wrapperUniqueId, + } = useSelect( ( select ) => { + const { + getCurrentPostId, + getCurrentPostType, + getCurrentTemplateId, + getEditorSettings, + getRenderingMode, + } = select( editorStore ); + const postTypeSlug = getCurrentPostType(); + const _renderingMode = getRenderingMode(); + let _wrapperBlockName; + + if ( postTypeSlug === 'wp_block' ) { + _wrapperBlockName = 'core/block'; + } else if ( ! _renderingMode === 'post-only' ) { + _wrapperBlockName = 'core/post-content'; + } + + const editorSettings = getEditorSettings(); + const supportsTemplateMode = editorSettings.supportsTemplateMode; + const postType = select( coreStore ).getPostType( postTypeSlug ); + const canEditTemplate = select( coreStore ).canUser( + 'create', + 'templates' + ); + const currentTemplateId = getCurrentTemplateId(); + const template = currentTemplateId + ? select( coreStore ).getEditedEntityRecord( + 'postType', + 'wp_template', + currentTemplateId + ) + : undefined; + + return { + renderingMode: _renderingMode, + postContentAttributes: getEditorSettings().postContentAttributes, + // Post template fetch returns a 404 on classic themes, which + // messes with e2e tests, so check it's a block theme first. + editedPostTemplate: + postType?.viewable && supportsTemplateMode && canEditTemplate + ? template + : undefined, + wrapperBlockName: _wrapperBlockName, + wrapperUniqueId: getCurrentPostId(), + }; + }, [] ); + const { isCleanNewPost } = useSelect( editorStore ); + const { + hasRootPaddingAwareAlignments, + themeHasDisabledLayoutStyles, + themeSupportsLayout, + } = useSelect( ( select ) => { + const _settings = select( blockEditorStore ).getSettings(); + return { + themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, + themeSupportsLayout: _settings.supportsLayout, + hasRootPaddingAwareAlignments: + _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, + }; + }, [] ); + + const [ globalLayoutSettings ] = useSettings( 'layout' ); + + // fallbackLayout is used if there is no Post Content, + // and for Post Title. + const fallbackLayout = useMemo( () => { + if ( renderingMode !== 'post-only' ) { + return { type: 'default' }; + } + + if ( themeSupportsLayout ) { + // We need to ensure support for wide and full alignments, + // so we add the constrained type. + return { ...globalLayoutSettings, type: 'constrained' }; + } + // Set default layout for classic themes so all alignments are supported. + return { type: 'default' }; + }, [ renderingMode, themeSupportsLayout, globalLayoutSettings ] ); + + const newestPostContentAttributes = useMemo( () => { + if ( + ! editedPostTemplate?.content && + ! editedPostTemplate?.blocks && + postContentAttributes + ) { + return postContentAttributes; + } + // When in template editing mode, we can access the blocks directly. + if ( editedPostTemplate?.blocks ) { + return getPostContentAttributes( editedPostTemplate?.blocks ); + } + // If there are no blocks, we have to parse the content string. + // Best double-check it's a string otherwise the parse function gets unhappy. + const parseableContent = + typeof editedPostTemplate?.content === 'string' + ? editedPostTemplate?.content + : ''; + + return getPostContentAttributes( parse( parseableContent ) ) || {}; + }, [ + editedPostTemplate?.content, + editedPostTemplate?.blocks, + postContentAttributes, + ] ); + + const hasPostContentAtRootLevel = useMemo( () => { + if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) { + return false; + } + // When in template editing mode, we can access the blocks directly. + if ( editedPostTemplate?.blocks ) { + return checkForPostContentAtRootLevel( editedPostTemplate?.blocks ); + } + // If there are no blocks, we have to parse the content string. + // Best double-check it's a string otherwise the parse function gets unhappy. + const parseableContent = + typeof editedPostTemplate?.content === 'string' + ? editedPostTemplate?.content + : ''; + + return ( + checkForPostContentAtRootLevel( parse( parseableContent ) ) || false + ); + }, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] ); + + const { layout = {}, align = '' } = newestPostContentAttributes || {}; + + const postContentLayoutClasses = useLayoutClasses( + newestPostContentAttributes, + 'core/post-content' + ); + + const blockListLayoutClass = classnames( + { + 'is-layout-flow': ! themeSupportsLayout, + }, + themeSupportsLayout && postContentLayoutClasses, + align && `align${ align }` + ); + + const postContentLayoutStyles = useLayoutStyles( + newestPostContentAttributes, + 'core/post-content', + '.block-editor-block-list__layout.is-root-container' + ); + + // Update type for blocks using legacy layouts. + const postContentLayout = useMemo( () => { + return layout && + ( layout?.type === 'constrained' || + layout?.inherit || + layout?.contentSize || + layout?.wideSize ) + ? { ...globalLayoutSettings, ...layout, type: 'constrained' } + : { ...globalLayoutSettings, ...layout, type: 'default' }; + }, [ + layout?.type, + layout?.inherit, + layout?.contentSize, + layout?.wideSize, + globalLayoutSettings, + ] ); + + // If there is a Post Content block we use its layout for the block list; + // if not, this must be a classic theme, in which case we use the fallback layout. + const blockListLayout = postContentAttributes + ? postContentLayout + : fallbackLayout; + + const postEditorLayout = + blockListLayout?.type === 'default' && ! hasPostContentAtRootLevel + ? fallbackLayout + : blockListLayout; + + const observeTypingRef = useTypingObserver(); + const titleRef = useRef(); + useEffect( () => { + if ( ! autoFocus || ! isCleanNewPost() ) { + return; + } + titleRef?.current?.focus(); + }, [ autoFocus, isCleanNewPost ] ); + + // Add some styles for alignwide/alignfull Post Content and its children. + const alignCSS = `.is-root-container.alignwide { max-width: var(--wp--style--global--wide-size); margin-left: auto; margin-right: auto;} + .is-root-container.alignwide:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: var(--wp--style--global--wide-size);} + .is-root-container.alignfull { max-width: none; margin-left: auto; margin-right: auto;} + .is-root-container.alignfull:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: none;}`; + + return ( + <> + { themeSupportsLayout && + ! themeHasDisabledLayoutStyles && + renderingMode === 'post-only' && ( + <> + + + { align && } + { postContentLayoutStyles && ( + + ) } + + ) } + { renderingMode === 'post-only' && ( +
+ +
+ ) } + + + + + ); +} diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index dda536aec4f733..3bd5860501d4e0 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -23,7 +23,6 @@ import { store as editorStore } from '../../store'; import useBlockEditorSettings from './use-block-editor-settings'; import { unlock } from '../../lock-unlock'; import DisableNonPageContentBlocks from './disable-non-page-content-blocks'; -import { PAGE_CONTENT_BLOCK_TYPES } from './constants'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); @@ -60,36 +59,6 @@ function useForceFocusModeForNavigation( navigationBlockClientId ) { ] ); } -/** - * Helper method to extract the post content block types from a template. - * - * @param {Array} blocks Template blocks. - * - * @return {Array} Flattened object. - */ -function extractPageContentBlockTypesFromTemplateBlocks( blocks ) { - 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 ( blocks[ i ].name === 'core/query' ) { - continue; - } - if ( PAGE_CONTENT_BLOCK_TYPES.includes( blocks[ i ].name ) ) { - result.push( createBlock( blocks[ i ].name ) ); - } - if ( blocks[ i ].innerBlocks.length ) { - result.push( - ...extractPageContentBlockTypesFromTemplateBlocks( - blocks[ i ].innerBlocks - ) - ); - } - } - - return result; -} - /** * Depending on the post, template and template mode, * returns the appropriate blocks and change handlers for the block editor provider. @@ -125,36 +94,6 @@ function useBlockEditorProps( post, template, mode ) { } }, [ post.type, post.id ] ); - const maybePostOnlyBlocks = useMemo( () => { - if ( mode === 'post-only' ) { - const postContentBlocks = - extractPageContentBlockTypesFromTemplateBlocks( - templateBlocks - ); - return [ - createBlock( - 'core/group', - { - layout: { type: 'constrained' }, - style: { - spacing: { - margin: { - top: '4em', // Mimics the post editor. - }, - }, - }, - }, - postContentBlocks.length - ? postContentBlocks - : [ - createBlock( 'core/post-title' ), - createBlock( 'core/post-content' ), - ] - ), - ]; - } - }, [ templateBlocks, mode ] ); - // It is important that we don't create a new instance of blocks on every change // We should only create a new instance if the blocks them selves change, not a dependency of them. const blocks = useMemo( () => { @@ -162,30 +101,19 @@ function useBlockEditorProps( post, template, mode ) { return maybeNavigationBlocks; } - if ( maybePostOnlyBlocks ) { - return maybePostOnlyBlocks; - } - if ( rootLevelPost === 'template' ) { return templateBlocks; } return postBlocks; - }, [ - maybeNavigationBlocks, - maybePostOnlyBlocks, - rootLevelPost, - templateBlocks, - postBlocks, - ] ); + }, [ maybeNavigationBlocks, rootLevelPost, templateBlocks, postBlocks ] ); // Handle fallback to postBlocks outside of the above useMemo, to ensure // that constructed block templates that call `createBlock` are not generated // too frequently. This ensures that clientIds are stable. const disableRootLevelChanges = ( !! template && mode === 'template-locked' ) || - post.type === 'wp_navigation' || - mode === 'post-only'; + post.type === 'wp_navigation'; const navigationBlockClientId = post.type === 'wp_navigation' && blocks && blocks[ 0 ]?.clientId; useForceFocusModeForNavigation( navigationBlockClientId ); @@ -270,7 +198,8 @@ export const ExperimentalEditorProvider = withRegistryProvider( setupEditor, updateEditorSettings, __experimentalTearDownEditor, - } = useDispatch( editorStore ); + setCurrentTemplateId, + } = unlock( useDispatch( editorStore ) ); const { createWarningNotice } = useDispatch( noticesStore ); // Initialize and tear down the editor. @@ -310,6 +239,10 @@ export const ExperimentalEditorProvider = withRegistryProvider( updateEditorSettings( settings ); }, [ settings, updateEditorSettings ] ); + useEffect( () => { + setCurrentTemplateId( template?.id ); + }, [ template?.id, setCurrentTemplateId ] ); + if ( ! isReady ) { return null; } @@ -332,9 +265,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( > { children } - { [ 'post-only', 'template-locked' ].includes( - mode - ) && } + { mode === 'template-locked' && ( + + ) } diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index a44720eb93ac83..046feee5b9c3f6 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import EditorCanvas from './components/editor-canvas'; import { ExperimentalEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; @@ -9,6 +10,7 @@ import PostPanelRow from './components/post-panel-row'; export const privateApis = {}; lock( privateApis, { + EditorCanvas, ExperimentalEditorProvider, EntitiesSavedStatesExtensible, PostPanelRow, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 0c946d4124f49f..4c1170b064202f 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -560,8 +560,12 @@ export function updateEditorSettings( settings ) { */ export const setRenderingMode = ( mode ) => - ( { dispatch, registry } ) => { - registry.dispatch( blockEditorStore ).clearSelectedBlock(); + ( { dispatch, registry, select } ) => { + if ( select.__unstableIsEditorReady() ) { + // We clear the block selection but we also need to clear the selection from the core store. + registry.dispatch( blockEditorStore ).clearSelectedBlock(); + dispatch.editPost( { selection: undefined }, { undoIgnore: true } ); + } dispatch( { type: 'SET_RENDERING_MODE', diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index baee4d9197d0c2..ebd41354308e7a 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -9,7 +9,9 @@ import { createReduxStore, register } from '@wordpress/data'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; +import * as privateActions from './private-actions'; import { STORE_NAME } from './constants'; +import { unlock } from '../lock-unlock'; /** * Post editor data store configuration. @@ -36,3 +38,4 @@ export const store = createReduxStore( STORE_NAME, { } ); register( store ); +unlock( store ).registerPrivateActions( privateActions ); diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js new file mode 100644 index 00000000000000..1af9ff5f6adb92 --- /dev/null +++ b/packages/editor/src/store/private-actions.js @@ -0,0 +1,13 @@ +/** + * Returns an action object used to set which template is currently being used/edited. + * + * @param {string} id Template Id. + * + * @return {Object} Action object. + */ +export function setCurrentTemplateId( id ) { + return { + type: 'SET_CURRENT_TEMPLATE_ID', + id, + }; +} diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 48356fd8e99e3c..7821baf5cdc062 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -90,6 +90,15 @@ export function postId( state = null, action ) { return state; } +export function templateId( state = null, action ) { + switch ( action.type ) { + case 'SET_CURRENT_TEMPLATE_ID': + return action.id; + } + + return state; +} + export function postType( state = null, action ) { switch ( action.type ) { case 'SETUP_EDITOR_STATE': @@ -291,6 +300,7 @@ export function renderingMode( state = 'all', action ) { export default combineReducers( { postId, postType, + templateId, saving, deleting, postLock, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 78944335bd3981..554b3d83e5912a 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -205,6 +205,17 @@ export function getCurrentPostId( state ) { return state.postId; } +/** + * Returns the template ID currently being rendered/edited + * + * @param {Object} state Global application state. + * + * @return {string?} Template ID. + */ +export function getCurrentTemplateId( state ) { + return state.templateId; +} + /** * Returns the number of revisions of the post currently being edited. * diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css index f8f2e8fe2b4cde..b0ce8f3dc948d5 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css @@ -8,7 +8,7 @@ display: none; } -.edit-post-visual-editor__post-title-wrapper { +.editor-editor-canvas__post-title-wrapper { display: none; } diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index af58daeaedbe40..4d40223e0a99c4 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -215,24 +215,13 @@ test.describe( 'Pages', () => { } ) ).toBeHidden(); - // Content blocks are wrapped in a Group block by default. + // Ensure post title component to be visible. await expect( - editor.canvas - .getByRole( 'document', { - name: 'Block: Group', - } ) - .getByRole( 'document', { - name: 'Block: Content', - } ) + editor.canvas.getByRole( 'textbox', { + name: 'Add Title', + } ) ).toBeVisible(); - // Ensure order is preserved between toggling. - await page - .locator( - '[aria-label="Block: Content"] + [aria-label="Block: Title"]' - ) - .isVisible(); - // Remove focus from templateOptionsButton button. await editor.canvas.locator( 'body' ).click();