@wordpress/block-editor
Version:
507 lines (467 loc) • 14.2 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
import { kebabCase } from 'lodash';
/**
* WordPress dependencies
*/
import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import {
Button,
ButtonGroup,
ToggleControl,
PanelBody,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useContext, createPortal } from '@wordpress/element';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../store';
import { InspectorControls } from '../components';
import useSetting from '../components/use-setting';
import { LayoutStyle } from '../components/block-list/layout';
import BlockList from '../components/block-list';
import { getLayoutType, getLayoutTypes } from '../layouts';
import { useBlockEditingMode } from '../components/block-editing-mode';
const layoutBlockSupportKey = '__experimentalLayout';
/**
* Generates the utility classnames for the given block's layout attributes.
*
* @param { Object } blockAttributes Block attributes.
* @param { string } blockName Block name.
*
* @return { Array } Array of CSS classname strings.
*/
export function useLayoutClasses( blockAttributes = {}, blockName = '' ) {
const rootPaddingAlignment = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return getSettings().__experimentalFeatures
?.useRootPaddingAwareAlignments;
}, [] );
const globalLayoutSettings = useSetting( 'layout' ) || {};
const { layout } = blockAttributes;
const { default: defaultBlockLayout } =
getBlockSupport( blockName, layoutBlockSupportKey ) || {};
const usedLayout =
layout?.inherit || layout?.contentSize || layout?.wideSize
? { ...layout, type: 'constrained' }
: layout || defaultBlockLayout || {};
const layoutClassnames = [];
if (
globalLayoutSettings?.definitions?.[ usedLayout?.type || 'default' ]
?.className
) {
const baseClassName =
globalLayoutSettings?.definitions?.[ usedLayout?.type || 'default' ]
?.className;
const compoundClassName = `wp-block-${ blockName
.split( '/' )
.pop() }-${ baseClassName }`;
layoutClassnames.push( baseClassName, compoundClassName );
}
if (
( usedLayout?.inherit ||
usedLayout?.contentSize ||
usedLayout?.type === 'constrained' ) &&
rootPaddingAlignment
) {
layoutClassnames.push( 'has-global-padding' );
}
if ( usedLayout?.orientation ) {
layoutClassnames.push( `is-${ kebabCase( usedLayout.orientation ) }` );
}
if ( usedLayout?.justifyContent ) {
layoutClassnames.push(
`is-content-justification-${ kebabCase(
usedLayout.justifyContent
) }`
);
}
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 } blockAttributes Block attributes.
* @param { string } blockName Block name.
* @param { string } selector A selector to use in generating the CSS rule.
*
* @return { string } CSS rule.
*/
export function useLayoutStyles( blockAttributes = {}, blockName, selector ) {
const { layout = {}, style = {} } = blockAttributes;
// 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,
selector,
layout,
layoutDefinitions: globalLayoutSettings?.definitions,
style,
hasBlockGapSupport,
} );
return css;
}
function LayoutPanel( { setAttributes, attributes, name: blockName } ) {
const { layout } = attributes;
const defaultThemeLayout = useSetting( 'layout' );
const { themeSupportsLayout } = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return {
themeSupportsLayout: getSettings().supportsLayout,
};
}, [] );
const blockEditingMode = useBlockEditingMode();
const layoutBlockSupport = getBlockSupport(
blockName,
layoutBlockSupportKey,
{}
);
const {
allowSwitching,
allowEditing = true,
allowInheriting = true,
default: defaultBlockLayout,
} = layoutBlockSupport;
if ( ! allowEditing ) {
return null;
}
// Only show the inherit toggle if it's supported,
// a default theme layout is set (e.g. one that provides `contentSize` and/or `wideSize` values),
// and either the default / flow or the constrained layout type is in use, as the toggle switches from one to the other.
const showInheritToggle = !! (
allowInheriting &&
!! defaultThemeLayout &&
( ! layout?.type ||
layout?.type === 'default' ||
layout?.type === 'constrained' ||
layout?.inherit )
);
const usedLayout = layout || defaultBlockLayout || {};
const {
inherit = false,
type = 'default',
contentSize = null,
} = usedLayout;
/**
* `themeSupportsLayout` is only relevant to the `default/flow` or
* `constrained` layouts and it should not be taken into account when other
* `layout` types are used.
*/
if (
( type === 'default' || type === 'constrained' ) &&
! themeSupportsLayout
) {
return null;
}
const layoutType = getLayoutType( type );
const constrainedType = getLayoutType( 'constrained' );
const displayControlsForLegacyLayouts =
! usedLayout.type && ( contentSize || inherit );
const hasContentSizeOrLegacySettings = !! inherit || !! contentSize;
const onChangeType = ( newType ) =>
setAttributes( { layout: { type: newType } } );
const onChangeLayout = ( newLayout ) =>
setAttributes( { layout: newLayout } );
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Layout' ) }>
{ showInheritToggle && (
<>
<ToggleControl
__nextHasNoMarginBottom
className="block-editor-hooks__toggle-control"
label={ __( 'Inner blocks use content width' ) }
checked={
layoutType?.name === 'constrained' ||
hasContentSizeOrLegacySettings
}
onChange={ () =>
setAttributes( {
layout: {
type:
layoutType?.name ===
'constrained' ||
hasContentSizeOrLegacySettings
? 'default'
: 'constrained',
},
} )
}
help={
layoutType?.name === 'constrained' ||
hasContentSizeOrLegacySettings
? __(
'Nested blocks use content width with options for full and wide widths.'
)
: __(
'Nested blocks will fill the width of this container. Toggle to constrain.'
)
}
/>
</>
) }
{ ! inherit && allowSwitching && (
<LayoutTypeSwitcher
type={ type }
onChange={ onChangeType }
/>
) }
{ layoutType && layoutType.name !== 'default' && (
<layoutType.inspectorControls
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ layoutBlockSupport }
/>
) }
{ constrainedType && displayControlsForLegacyLayouts && (
<constrainedType.inspectorControls
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ layoutBlockSupport }
/>
) }
</PanelBody>
</InspectorControls>
{ ! inherit && blockEditingMode === 'default' && layoutType && (
<layoutType.toolBarControls
layout={ usedLayout }
onChange={ onChangeLayout }
layoutBlockSupport={ layoutBlockSupport }
/>
) }
</>
);
}
function LayoutTypeSwitcher( { type, onChange } ) {
return (
<ButtonGroup>
{ getLayoutTypes().map( ( { name, label } ) => {
return (
<Button
key={ name }
isPressed={ type === name }
onClick={ () => onChange( name ) }
>
{ label }
</Button>
);
} ) }
</ButtonGroup>
);
}
/**
* Filters registered block settings, extending attributes to include `layout`.
*
* @param {Object} settings Original block settings.
*
* @return {Object} Filtered block settings.
*/
export function addAttribute( settings ) {
if ( 'type' in ( settings.attributes?.layout ?? {} ) ) {
return settings;
}
if ( hasBlockSupport( settings, layoutBlockSupportKey ) ) {
settings.attributes = {
...settings.attributes,
layout: {
type: 'object',
},
};
}
return settings;
}
/**
* Override the default edit UI to include layout controls
*
* @param {Function} BlockEdit Original component.
*
* @return {Function} Wrapped component.
*/
export const withInspectorControls = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const { name: blockName } = props;
const supportLayout = hasBlockSupport(
blockName,
layoutBlockSupportKey
);
const blockEditingMode = useBlockEditingMode();
return [
supportLayout && blockEditingMode === 'default' && (
<LayoutPanel key="layout" { ...props } />
),
<BlockEdit key="edit" { ...props } />,
];
},
'withInspectorControls'
);
/**
* Override the default block element to add the layout styles.
*
* @param {Function} BlockListBlock Original component.
*
* @return {Function} Wrapped component.
*/
export const withLayoutStyles = createHigherOrderComponent(
( BlockListBlock ) => ( props ) => {
const { name, attributes } = props;
const hasLayoutBlockSupport = hasBlockSupport(
name,
layoutBlockSupportKey
);
const disableLayoutStyles = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return !! getSettings().disableLayoutStyles;
} );
const shouldRenderLayoutStyles =
hasLayoutBlockSupport && ! disableLayoutStyles;
const id = useInstanceId( BlockListBlock );
const defaultThemeLayout = useSetting( 'layout' ) || {};
const element = useContext( BlockList.__unstableElementContext );
const { layout } = attributes;
const { default: defaultBlockLayout } =
getBlockSupport( name, layoutBlockSupportKey ) || {};
const usedLayout =
layout?.inherit || layout?.contentSize || layout?.wideSize
? { ...layout, type: 'constrained' }
: layout || defaultBlockLayout || {};
const layoutClasses = hasLayoutBlockSupport
? useLayoutClasses( attributes, name )
: null;
// Higher specificity to override defaults from theme.json.
const selector = `.wp-container-${ id }.wp-container-${ id }`;
const blockGapSupport = useSetting( 'spacing.blockGap' );
const hasBlockGapSupport = blockGapSupport !== null;
// Get CSS string for the current layout type.
// The CSS and `style` element is only output if it is not empty.
let css;
if ( shouldRenderLayoutStyles ) {
const fullLayoutType = getLayoutType(
usedLayout?.type || 'default'
);
css = fullLayoutType?.getLayoutStyle?.( {
blockName: name,
selector,
layout: usedLayout,
layoutDefinitions: defaultThemeLayout?.definitions,
style: attributes?.style,
hasBlockGapSupport,
} );
}
// Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`.
const layoutClassNames = classnames(
{
[ `wp-container-${ id }` ]: shouldRenderLayoutStyles && !! css, // Only attach a container class if there is generated CSS to be attached.
},
layoutClasses
);
return (
<>
{ shouldRenderLayoutStyles &&
element &&
!! css &&
createPortal(
<LayoutStyle
blockName={ name }
selector={ selector }
css={ css }
layout={ usedLayout }
style={ attributes?.style }
/>,
element
) }
<BlockListBlock
{ ...props }
__unstableLayoutClassNames={ layoutClassNames }
/>
</>
);
},
'withLayoutStyles'
);
/**
* Override the default block element to add the child layout styles.
*
* @param {Function} BlockListBlock Original component.
*
* @return {Function} Wrapped component.
*/
export const withChildLayoutStyles = createHigherOrderComponent(
( BlockListBlock ) => ( props ) => {
const { attributes } = props;
const { style: { layout = {} } = {} } = attributes;
const { selfStretch, flexSize } = layout;
const hasChildLayout = selfStretch || flexSize;
const disableLayoutStyles = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return !! getSettings().disableLayoutStyles;
} );
const shouldRenderChildLayoutStyles =
hasChildLayout && ! disableLayoutStyles;
const element = useContext( BlockList.__unstableElementContext );
const id = useInstanceId( BlockListBlock );
const selector = `.wp-container-content-${ id }`;
let css = '';
if ( selfStretch === 'fixed' && flexSize ) {
css += `${ selector } {
flex-basis: ${ flexSize };
box-sizing: border-box;
}`;
} else if ( selfStretch === 'fill' ) {
css += `${ selector } {
flex-grow: 1;
}`;
}
// Attach a `wp-container-content` id-based classname.
const className = classnames( props?.className, {
[ `wp-container-content-${ id }` ]:
shouldRenderChildLayoutStyles && !! css, // Only attach a container class if there is generated CSS to be attached.
} );
return (
<>
{ shouldRenderChildLayoutStyles &&
element &&
!! css &&
createPortal( <style>{ css }</style>, element ) }
<BlockListBlock { ...props } className={ className } />
</>
);
},
'withChildLayoutStyles'
);
addFilter(
'blocks.registerBlockType',
'core/layout/addAttribute',
addAttribute
);
addFilter(
'editor.BlockListBlock',
'core/editor/layout/with-layout-styles',
withLayoutStyles
);
addFilter(
'editor.BlockListBlock',
'core/editor/layout/with-child-layout-styles',
withChildLayoutStyles
);
addFilter(
'editor.BlockEdit',
'core/editor/layout/with-inspector-controls',
withInspectorControls
);