@wordpress/block-library
Version:
Block library for the WordPress editor.
287 lines (259 loc) • 7.45 kB
JavaScript
/**
* 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;