UNPKG

@wordpress/block-library

Version:
287 lines (259 loc) 7.45 kB
/** * External dependencies */ import classnames from 'classnames'; import { get } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { Notice, PanelBody, RangeControl, ToggleControl, } from '@wordpress/components'; import { InspectorControls, useInnerBlocksProps, BlockControls, BlockVerticalAlignmentToolbar, __experimentalBlockVariationPicker, useBlockProps, store as blockEditorStore, } from '@wordpress/block-editor'; import { withDispatch, useDispatch, useSelect } from '@wordpress/data'; import { createBlock, createBlocksFromInnerBlocksTemplate, store as blocksStore, } from '@wordpress/blocks'; /** * Internal dependencies */ import { hasExplicitPercentColumnWidths, getMappedColumnWidths, getRedistributedColumnWidths, toWidthPrecision, } from './utils'; /** * Allowed blocks constant is passed to InnerBlocks precisely as specified here. * The contents of the array should never change. * The array should contain the name of each block that is allowed. * In columns block, the only block we allow is 'core/column'. * * @constant * @type {string[]} */ const ALLOWED_BLOCKS = [ 'core/column' ]; function ColumnsEditContainer( { attributes, setAttributes, updateAlignment, updateColumns, clientId, } ) { const { isStackedOnMobile, verticalAlignment } = attributes; const { count } = useSelect( ( select ) => { return { count: select( blockEditorStore ).getBlockCount( clientId ), }; }, [ clientId ] ); const classes = classnames( { [ `are-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment, [ `is-not-stacked-on-mobile` ]: ! isStackedOnMobile, } ); const blockProps = useBlockProps( { className: classes, } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: ALLOWED_BLOCKS, orientation: 'horizontal', renderAppender: false, } ); return ( <> <BlockControls> <BlockVerticalAlignmentToolbar onChange={ updateAlignment } value={ verticalAlignment } /> </BlockControls> <InspectorControls> <PanelBody> <RangeControl label={ __( 'Columns' ) } value={ count } onChange={ ( value ) => updateColumns( count, value ) } min={ 1 } max={ Math.max( 6, count ) } /> { count > 6 && ( <Notice status="warning" isDismissible={ false }> { __( 'This column count exceeds the recommended amount and may cause visual breakage.' ) } </Notice> ) } <ToggleControl label={ __( 'Stack on mobile' ) } checked={ isStackedOnMobile } onChange={ () => setAttributes( { isStackedOnMobile: ! isStackedOnMobile, } ) } /> </PanelBody> </InspectorControls> <div { ...innerBlocksProps } /> </> ); } const ColumnsEditContainerWrapper = withDispatch( ( dispatch, ownProps, registry ) => ( { /** * Update all child Column blocks with a new vertical alignment setting * based on whatever alignment is passed in. This allows change to parent * to overide anything set on a individual column basis. * * @param {string} verticalAlignment the vertical alignment setting */ updateAlignment( verticalAlignment ) { const { clientId, setAttributes } = ownProps; const { updateBlockAttributes } = dispatch( blockEditorStore ); const { getBlockOrder } = registry.select( blockEditorStore ); // Update own alignment. setAttributes( { verticalAlignment } ); // Update all child Column Blocks to match. const innerBlockClientIds = getBlockOrder( clientId ); innerBlockClientIds.forEach( ( innerBlockClientId ) => { updateBlockAttributes( innerBlockClientId, { verticalAlignment, } ); } ); }, /** * Updates the column count, including necessary revisions to child Column * blocks to grant required or redistribute available space. * * @param {number} previousColumns Previous column count. * @param {number} newColumns New column count. */ updateColumns( previousColumns, newColumns ) { const { clientId } = ownProps; const { replaceInnerBlocks } = dispatch( blockEditorStore ); const { getBlocks } = registry.select( blockEditorStore ); let innerBlocks = getBlocks( clientId ); const hasExplicitWidths = hasExplicitPercentColumnWidths( innerBlocks ); // Redistribute available width for existing inner blocks. const isAddingColumn = newColumns > previousColumns; if ( isAddingColumn && hasExplicitWidths ) { // If adding a new column, assign width to the new column equal to // as if it were `1 / columns` of the total available space. const newColumnWidth = toWidthPrecision( 100 / newColumns ); // Redistribute in consideration of pending block insertion as // constraining the available working width. const widths = getRedistributedColumnWidths( innerBlocks, 100 - newColumnWidth ); innerBlocks = [ ...getMappedColumnWidths( innerBlocks, widths ), ...Array.from( { length: newColumns - previousColumns, } ).map( () => { return createBlock( 'core/column', { width: `${ newColumnWidth }%`, } ); } ), ]; } else if ( isAddingColumn ) { innerBlocks = [ ...innerBlocks, ...Array.from( { length: newColumns - previousColumns, } ).map( () => { return createBlock( 'core/column' ); } ), ]; } else { // The removed column will be the last of the inner blocks. innerBlocks = innerBlocks.slice( 0, -( previousColumns - newColumns ) ); if ( hasExplicitWidths ) { // Redistribute as if block is already removed. const widths = getRedistributedColumnWidths( innerBlocks, 100 ); innerBlocks = getMappedColumnWidths( innerBlocks, widths ); } } replaceInnerBlocks( clientId, innerBlocks ); }, } ) )( ColumnsEditContainer ); function Placeholder( { clientId, name, setAttributes } ) { const { blockType, defaultVariation, variations } = useSelect( ( select ) => { const { getBlockVariations, getBlockType, getDefaultBlockVariation, } = select( blocksStore ); return { blockType: getBlockType( name ), defaultVariation: getDefaultBlockVariation( name, 'block' ), variations: getBlockVariations( name, 'block' ), }; }, [ name ] ); const { replaceInnerBlocks } = useDispatch( blockEditorStore ); const blockProps = useBlockProps(); return ( <div { ...blockProps }> <__experimentalBlockVariationPicker icon={ get( blockType, [ 'icon', 'src' ] ) } label={ get( blockType, [ 'title' ] ) } variations={ variations } onSelect={ ( nextVariation = defaultVariation ) => { if ( nextVariation.attributes ) { setAttributes( nextVariation.attributes ); } if ( nextVariation.innerBlocks ) { replaceInnerBlocks( clientId, createBlocksFromInnerBlocksTemplate( nextVariation.innerBlocks ), true ); } } } allowSkip /> </div> ); } const ColumnsEdit = ( props ) => { const { clientId } = props; const hasInnerBlocks = useSelect( ( select ) => select( blockEditorStore ).getBlocks( clientId ).length > 0, [ clientId ] ); const Component = hasInnerBlocks ? ColumnsEditContainerWrapper : Placeholder; return <Component { ...props } />; }; export default ColumnsEdit;