UNPKG

@wordpress/block-editor

Version:
507 lines (467 loc) 14.2 kB
/** * 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 );