diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 60a34f6ea55fb0..f6fa73a053ac58 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -20,6 +20,7 @@ import './metadata'; import './metadata-name'; export { useCustomSides } from './dimensions'; +export { useLayoutClasses, useLayoutStyles } from './layout'; export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index ac24fa6f4e232a..73d655c57c2acb 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -37,58 +37,101 @@ import { getLayoutType, getLayoutTypes } from '../layouts'; const layoutBlockSupportKey = '__experimentalLayout'; /** - * Generates the utility classnames for the given blocks layout attributes. - * This method was primarily added to reintroduce classnames that were removed - * in the 5.9 release (https://github.com/WordPress/gutenberg/issues/38719), rather - * than providing an extensive list of all possible layout classes. The plan is to - * have the style engine generate a more extensive list of utility classnames which - * will then replace this method. + * Generates the utility classnames for the given block's layout attributes. * - * @param { Object } layout Layout object. - * @param { Object } layoutDefinitions An object containing layout definitions, stored in theme.json. + * @param { Object } block Block object. * * @return { Array } Array of CSS classname strings. */ -function useLayoutClasses( layout, layoutDefinitions ) { +export function useLayoutClasses( block = {} ) { const rootPaddingAlignment = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); return getSettings().__experimentalFeatures ?.useRootPaddingAwareAlignments; }, [] ); + const globalLayoutSettings = useSetting( 'layout' ) || {}; + + const { attributes = {}, name } = block; + const { layout } = attributes; + + const { default: defaultBlockLayout } = + getBlockSupport( name, layoutBlockSupportKey ) || {}; + const usedLayout = + layout?.inherit || layout?.contentSize || layout?.wideSize + ? { ...layout, type: 'constrained' } + : layout || defaultBlockLayout || {}; + const layoutClassnames = []; - if ( layoutDefinitions?.[ layout?.type || 'default' ]?.className ) { + if ( + globalLayoutSettings?.definitions?.[ usedLayout?.type || 'default' ] + ?.className + ) { layoutClassnames.push( - layoutDefinitions?.[ layout?.type || 'default' ]?.className + globalLayoutSettings?.definitions?.[ usedLayout?.type || 'default' ] + ?.className ); } if ( - ( layout?.inherit || - layout?.contentSize || - layout?.type === 'constrained' ) && + ( usedLayout?.inherit || + usedLayout?.contentSize || + usedLayout?.type === 'constrained' ) && rootPaddingAlignment ) { layoutClassnames.push( 'has-global-padding' ); } - if ( layout?.orientation ) { - layoutClassnames.push( `is-${ kebabCase( layout.orientation ) }` ); + if ( usedLayout?.orientation ) { + layoutClassnames.push( `is-${ kebabCase( usedLayout.orientation ) }` ); } - if ( layout?.justifyContent ) { + if ( usedLayout?.justifyContent ) { layoutClassnames.push( - `is-content-justification-${ kebabCase( layout.justifyContent ) }` + `is-content-justification-${ kebabCase( + usedLayout.justifyContent + ) }` ); } - if ( layout?.flexWrap && layout.flexWrap === 'nowrap' ) { + if ( usedLayout?.flexWrap && usedLayout.flexWrap === 'nowrap' ) { layoutClassnames.push( 'is-nowrap' ); } return layoutClassnames; } +/** + * Generates a CSS rule with the given block's layout styles. + * + * @param { Object } block Block object. + * @param { string } selector A selector to use in generating the CSS rule. + * + * @return { string } CSS rule. + */ +export function useLayoutStyles( block = {}, selector ) { + const { attributes = {}, name } = block; + const { layout = {}, style = {} } = attributes; + // Update type for blocks using legacy layouts. + const usedLayout = + layout?.inherit || layout?.contentSize || layout?.wideSize + ? { ...layout, type: 'constrained' } + : layout || {}; + const fullLayoutType = getLayoutType( usedLayout?.type || 'default' ); + const globalLayoutSettings = useSetting( 'layout' ) || {}; + const blockGapSupport = useSetting( 'spacing.blockGap' ); + const hasBlockGapSupport = blockGapSupport !== null; + const css = fullLayoutType?.getLayoutStyle?.( { + blockName: name, + selector, + layout, + layoutDefinitions: globalLayoutSettings?.definitions, + style, + hasBlockGapSupport, + } ); + return css; +} + function LayoutPanel( { setAttributes, attributes, name: blockName } ) { const { layout } = attributes; const defaultThemeLayout = useSetting( 'layout' ); @@ -299,7 +342,7 @@ export const withInspectorControls = createHigherOrderComponent( */ export const withLayoutStyles = createHigherOrderComponent( ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; + const { name, attributes, block } = props; const hasLayoutBlockSupport = hasBlockSupport( name, layoutBlockSupportKey @@ -321,7 +364,7 @@ export const withLayoutStyles = createHigherOrderComponent( ? { ...layout, type: 'constrained' } : layout || defaultBlockLayout || {}; const layoutClasses = hasLayoutBlockSupport - ? useLayoutClasses( usedLayout, defaultThemeLayout?.definitions ) + ? useLayoutClasses( block ) : null; const selector = `.${ getBlockDefaultClassName( name diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index ec0f20a8f9c892..1c81c910b21e12 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -12,6 +12,8 @@ export { getSpacingClassesAndStyles as __experimentalGetSpacingClassesAndStyles, getGapCSSValue as __experimentalGetGapCSSValue, useCachedTruthy, + useLayoutClasses as __experimentaluseLayoutClasses, + useLayoutStyles as __experimentaluseLayoutStyles, } from './hooks'; export * from './components'; export * from './elements'; diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js index 233399a63f8d55..1949553b2540f6 100644 --- a/packages/block-library/src/post-content/edit.js +++ b/packages/block-library/src/post-content/edit.js @@ -46,7 +46,9 @@ function EditableContent( { layout, context = {} } ) { return getSettings()?.supportsLayout; }, [] ); const defaultLayout = useSetting( 'layout' ) || {}; - const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const usedLayout = ! layout?.type + ? { ...defaultLayout, ...layout, type: 'default' } + : { ...defaultLayout, ...layout }; const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', postType, diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 8f6d54f064c38b..6ba8db74122d6c 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -28,6 +28,8 @@ import { __unstableUseMouseMoveTypingReset as useMouseMoveTypingReset, __unstableIframe as Iframe, __experimentalRecursionProvider as RecursionProvider, + __experimentaluseLayoutClasses as useLayoutClasses, + __experimentaluseLayoutStyles as useLayoutStyles, } from '@wordpress/block-editor'; import { useEffect, useRef, useMemo } from '@wordpress/element'; import { Button, __unstableMotion as motion } from '@wordpress/components'; @@ -35,6 +37,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useMergeRefs } from '@wordpress/compose'; import { arrowLeft } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import { parse } from '@wordpress/blocks'; /** * Internal dependencies @@ -82,11 +85,37 @@ function MaybeIframe( { ); } +/** + * Given an array of nested blocks, find the first Post Content + * block inside it, recursing through any nesting levels. + * + * @param {Array} blocks A list of blocks. + * + * @return {Object} The Post Content block. + */ +function findPostContent( blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/post-content' ) { + return blocks[ i ]; + } + if ( blocks[ i ].innerBlocks.length ) { + const nestedPostContent = findPostContent( + blocks[ i ].innerBlocks + ); + + if ( nestedPostContent ) { + return nestedPostContent; + } + } + } +} + export default function VisualEditor( { styles } ) { const { deviceType, isWelcomeGuideVisible, isTemplateMode, + editedPostTemplate = {}, wrapperBlockName, wrapperUniqueId, } = useSelect( ( select ) => { @@ -94,8 +123,10 @@ export default function VisualEditor( { styles } ) { isFeatureActive, isEditingTemplate, __experimentalGetPreviewDeviceType, + getEditedPostTemplate, } = select( editPostStore ); - const { getCurrentPostId, getCurrentPostType } = select( editorStore ); + const { getCurrentPostId, getCurrentPostType, getEditorSettings } = + select( editorStore ); const _isTemplateMode = isEditingTemplate(); let _wrapperBlockName; @@ -105,10 +136,17 @@ export default function VisualEditor( { styles } ) { _wrapperBlockName = 'core/post-content'; } + const supportsTemplateMode = getEditorSettings().supportsTemplateMode; + return { deviceType: __experimentalGetPreviewDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), isTemplateMode: _isTemplateMode, + // Post template fetch returns a 404 on classic themes, which + // messes with e2e tests, so we check it's a block theme first. + editedPostTemplate: supportsTemplateMode + ? getEditedPostTemplate() + : {}, wrapperBlockName: _wrapperBlockName, wrapperUniqueId: getCurrentPostId(), }; @@ -122,7 +160,6 @@ export default function VisualEditor( { styles } ) { themeHasDisabledLayoutStyles, themeSupportsLayout, assets, - useRootPaddingAwareAlignments, isFocusMode, } = useSelect( ( select ) => { const _settings = select( blockEditorStore ).getSettings(); @@ -130,8 +167,6 @@ export default function VisualEditor( { styles } ) { themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, themeSupportsLayout: _settings.supportsLayout, assets: _settings.__unstableResolvedAssets, - useRootPaddingAwareAlignments: - _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, isFocusMode: _settings.focusMode, }; }, [] ); @@ -154,7 +189,7 @@ export default function VisualEditor( { styles } ) { borderBottom: 0, }; const resizedCanvasStyles = useResizeCanvas( deviceType, isTemplateMode ); - const defaultLayout = useSetting( 'layout' ); + const globalLayoutSettings = useSetting( 'layout' ); const previewMode = 'is-' + deviceType.toLowerCase() + '-preview'; let animatedStyles = isTemplateMode @@ -183,7 +218,9 @@ export default function VisualEditor( { styles } ) { const blockSelectionClearerRef = useBlockSelectionClearer(); - const layout = useMemo( () => { + // fallbackLayout is used if there is no Post Content, + // and for Post Title. + const fallbackLayout = useMemo( () => { if ( isTemplateMode ) { return { type: 'default' }; } @@ -191,17 +228,58 @@ export default function VisualEditor( { styles } ) { if ( themeSupportsLayout ) { // We need to ensure support for wide and full alignments, // so we add the constrained type. - return { ...defaultLayout, type: 'constrained' }; + return { ...globalLayoutSettings, type: 'constrained' }; } // Set default layout for classic themes so all alignments are supported. return { type: 'default' }; - }, [ isTemplateMode, themeSupportsLayout, defaultLayout ] ); + }, [ isTemplateMode, themeSupportsLayout, globalLayoutSettings ] ); + + const postContentBlock = useMemo( () => { + // When in template editing mode, we can access the blocks directly. + if ( editedPostTemplate?.blocks ) { + return findPostContent( 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 findPostContent( parse( parseableContent ) ) || {}; + }, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] ); + + const postContentLayoutClasses = useLayoutClasses( postContentBlock ); + + const blockListLayoutClass = classnames( + { + 'is-layout-flow': ! themeSupportsLayout, + }, + themeSupportsLayout && postContentLayoutClasses + ); - const blockListLayoutClass = classnames( { - 'is-layout-constrained': themeSupportsLayout, - 'is-layout-flow': ! themeSupportsLayout, - 'has-global-padding': useRootPaddingAwareAlignments, - } ); + const postContentLayoutStyles = useLayoutStyles( + postContentBlock, + '.block-editor-block-list__layout.is-root-container' + ); + + const layout = postContentBlock?.attributes?.layout || {}; + + // Update type for blocks using legacy layouts. + const postContentLayout = + layout && + ( layout?.type === 'constrained' || + layout?.inherit || + layout?.contentSize || + layout?.wideSize ) + ? { ...globalLayoutSettings, ...layout, type: 'constrained' } + : { ...globalLayoutSettings, ...layout, type: 'default' }; + + // 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 = postContentBlock + ? postContentLayout + : fallbackLayout; const titleRef = useRef(); useEffect( () => { @@ -257,13 +335,24 @@ export default function VisualEditor( { styles } ) { { themeSupportsLayout && ! themeHasDisabledLayoutStyles && ! isTemplateMode && ( - + <> + + { postContentLayoutStyles && ( + + ) } + ) } { ! isTemplateMode && (