UNPKG

@wordpress/block-editor

Version:
486 lines (458 loc) 12.4 kB
/** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { BaseControl, Flex, FlexItem, RangeControl, __experimentalNumberControl as NumberControl, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalUnitControl as UnitControl, __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, __experimentalVStack as VStack, } from '@wordpress/components'; import { useState } from '@wordpress/element'; /** * Internal dependencies */ import { appendSelectors, getBlockGapCSS } from './utils'; import { getGapCSSValue } from '../hooks/gap'; import { shouldSkipSerialization } from '../hooks/utils'; import { LAYOUT_DEFINITIONS } from './definitions'; const RANGE_CONTROL_MAX_VALUES = { px: 600, '%': 100, vw: 100, vh: 100, em: 38, rem: 38, svw: 100, lvw: 100, dvw: 100, svh: 100, lvh: 100, dvh: 100, vi: 100, svi: 100, lvi: 100, dvi: 100, vb: 100, svb: 100, lvb: 100, dvb: 100, vmin: 100, svmin: 100, lvmin: 100, dvmin: 100, vmax: 100, svmax: 100, lvmax: 100, dvmax: 100, }; const units = [ { value: 'px', label: 'px', default: 0 }, { value: 'rem', label: 'rem', default: 0 }, { value: 'em', label: 'em', default: 0 }, ]; export default { name: 'grid', label: __( 'Grid' ), inspectorControls: function GridLayoutInspectorControls( { layout = {}, onChange, layoutBlockSupport = {}, } ) { const { allowSizingOnChildren = false } = layoutBlockSupport; // In the experiment we want to also show column control in Auto mode, and // the minimum width control in Manual mode. const showColumnsControl = window.__experimentalEnableGridInteractivity || !! layout?.columnCount; const showMinWidthControl = window.__experimentalEnableGridInteractivity || ! layout?.columnCount; return ( <> <GridLayoutTypeControl layout={ layout } onChange={ onChange } /> <VStack spacing={ 4 }> { showColumnsControl && ( <GridLayoutColumnsAndRowsControl layout={ layout } onChange={ onChange } allowSizingOnChildren={ allowSizingOnChildren } /> ) } { showMinWidthControl && ( <GridLayoutMinimumWidthControl layout={ layout } onChange={ onChange } /> ) } </VStack> </> ); }, toolBarControls: function GridLayoutToolbarControls() { return null; }, getLayoutStyle: function getLayoutStyle( { selector, layout, style, blockName, hasBlockGapSupport, layoutDefinitions = LAYOUT_DEFINITIONS, } ) { const { minimumColumnWidth = null, columnCount = null, rowCount = null, } = layout; // Check that the grid layout attributes are of the correct type, so that we don't accidentally // write code that stores a string attribute instead of a number. if ( process.env.NODE_ENV === 'development' ) { if ( minimumColumnWidth && typeof minimumColumnWidth !== 'string' ) { throw new Error( 'minimumColumnWidth must be a string' ); } if ( columnCount && typeof columnCount !== 'number' ) { throw new Error( 'columnCount must be a number' ); } if ( rowCount && typeof rowCount !== 'number' ) { throw new Error( 'rowCount must be a number' ); } } // If a block's block.json skips serialization for spacing or spacing.blockGap, // don't apply the user-defined value to the styles. const blockGapValue = style?.spacing?.blockGap && ! shouldSkipSerialization( blockName, 'spacing', 'blockGap' ) ? getGapCSSValue( style?.spacing?.blockGap, '0.5em' ) : undefined; let output = ''; const rules = []; if ( minimumColumnWidth && columnCount > 0 ) { const maxValue = `max(${ minimumColumnWidth }, ( 100% - (${ blockGapValue || '1.2rem' }*${ columnCount - 1 }) ) / ${ columnCount })`; rules.push( `grid-template-columns: repeat(auto-fill, minmax(${ maxValue }, 1fr))`, `container-type: inline-size` ); if ( rowCount ) { rules.push( `grid-template-rows: repeat(${ rowCount }, minmax(1rem, auto))` ); } } else if ( columnCount ) { rules.push( `grid-template-columns: repeat(${ columnCount }, minmax(0, 1fr))` ); if ( rowCount ) { rules.push( `grid-template-rows: repeat(${ rowCount }, minmax(1rem, auto))` ); } } else { rules.push( `grid-template-columns: repeat(auto-fill, minmax(min(${ minimumColumnWidth || '12rem' }, 100%), 1fr))`, 'container-type: inline-size' ); } if ( rules.length ) { // Reason to disable: the extra line breaks added by prettier mess with the unit tests. // eslint-disable-next-line prettier/prettier output = `${ appendSelectors( selector ) } { ${ rules.join( '; ' ) }; }`; } // Output blockGap styles based on rules contained in layout definitions in theme.json. if ( hasBlockGapSupport && blockGapValue ) { output += getBlockGapCSS( selector, layoutDefinitions, 'grid', blockGapValue ); } return output; }, getOrientation() { return 'horizontal'; }, getAlignments() { return []; }, }; // Enables setting minimum width of grid items. function GridLayoutMinimumWidthControl( { layout, onChange } ) { const { minimumColumnWidth, columnCount, isManualPlacement } = layout; const defaultValue = isManualPlacement || columnCount ? null : '12rem'; const value = minimumColumnWidth || defaultValue; const [ quantity, unit = 'rem' ] = parseQuantityAndUnitFromRawValue( value ); const handleSliderChange = ( next ) => { onChange( { ...layout, minimumColumnWidth: [ next, unit ].join( '' ), } ); }; // Mostly copied from HeightControl. const handleUnitChange = ( newUnit ) => { // Attempt to smooth over differences between currentUnit and newUnit. // This should slightly improve the experience of switching between unit types. let newValue; if ( [ 'em', 'rem' ].includes( newUnit ) && unit === 'px' ) { // Convert pixel value to an approximate of the new unit, assuming a root size of 16px. newValue = ( quantity / 16 ).toFixed( 2 ) + newUnit; } else if ( [ 'em', 'rem' ].includes( unit ) && newUnit === 'px' ) { // Convert to pixel value assuming a root size of 16px. newValue = Math.round( quantity * 16 ) + newUnit; } onChange( { ...layout, minimumColumnWidth: newValue, } ); }; return ( <fieldset> <BaseControl.VisualLabel as="legend"> { __( 'Minimum column width' ) } </BaseControl.VisualLabel> <Flex gap={ 4 }> <FlexItem isBlock> <UnitControl size="__unstable-large" onChange={ ( newValue ) => { onChange( { ...layout, minimumColumnWidth: newValue === '' ? undefined : newValue, } ); } } onUnitChange={ handleUnitChange } value={ value } units={ units } min={ 0 } label={ __( 'Minimum column width' ) } hideLabelFromVision /> </FlexItem> <FlexItem isBlock> <RangeControl __next40pxDefaultSize __nextHasNoMarginBottom onChange={ handleSliderChange } value={ quantity || 0 } min={ 0 } max={ RANGE_CONTROL_MAX_VALUES[ unit ] || 600 } withInputField={ false } label={ __( 'Minimum column width' ) } hideLabelFromVision /> </FlexItem> </Flex> </fieldset> ); } // Enables setting number of grid columns function GridLayoutColumnsAndRowsControl( { layout, onChange, allowSizingOnChildren, } ) { // If the grid interactivity experiment is enabled, allow unsetting the column count. const defaultColumnCount = window.__experimentalEnableGridInteractivity ? undefined : 3; const { columnCount = defaultColumnCount, rowCount, isManualPlacement, } = layout; return ( <> <fieldset> { ( ! window.__experimentalEnableGridInteractivity || ! isManualPlacement ) && ( <BaseControl.VisualLabel as="legend"> { __( 'Columns' ) } </BaseControl.VisualLabel> ) } <Flex gap={ 4 }> <FlexItem isBlock> <NumberControl size="__unstable-large" onChange={ ( value ) => { if ( window.__experimentalEnableGridInteractivity ) { // Allow unsetting the column count when in auto mode. const defaultNewColumnCount = isManualPlacement ? 1 : undefined; const newColumnCount = value === '' || value === '0' ? defaultNewColumnCount : parseInt( value, 10 ); onChange( { ...layout, columnCount: newColumnCount, } ); } else { // Don't allow unsetting the column count. const newColumnCount = value === '' || value === '0' ? 1 : parseInt( value, 10 ); onChange( { ...layout, columnCount: newColumnCount, } ); } } } value={ columnCount } min={ 1 } label={ __( 'Columns' ) } hideLabelFromVision={ ! window.__experimentalEnableGridInteractivity || ! isManualPlacement } /> </FlexItem> <FlexItem isBlock> { window.__experimentalEnableGridInteractivity && allowSizingOnChildren && isManualPlacement ? ( <NumberControl size="__unstable-large" onChange={ ( value ) => { // Don't allow unsetting the row count. const newRowCount = value === '' || value === '0' ? 1 : parseInt( value, 10 ); onChange( { ...layout, rowCount: newRowCount, } ); } } value={ rowCount } min={ 1 } label={ __( 'Rows' ) } /> ) : ( <RangeControl __next40pxDefaultSize __nextHasNoMarginBottom value={ columnCount ?? 1 } onChange={ ( value ) => onChange( { ...layout, columnCount: value === '' || value === '0' ? 1 : value, } ) } min={ 1 } max={ 16 } withInputField={ false } label={ __( 'Columns' ) } hideLabelFromVision /> ) } </FlexItem> </Flex> </fieldset> </> ); } // Enables switching between grid types function GridLayoutTypeControl( { layout, onChange } ) { const { columnCount, rowCount, minimumColumnWidth, isManualPlacement } = layout; /** * When switching, temporarily save any custom values set on the * previous type so we can switch back without loss. */ const [ tempColumnCount, setTempColumnCount ] = useState( columnCount || 3 ); const [ tempRowCount, setTempRowCount ] = useState( rowCount ); const [ tempMinimumColumnWidth, setTempMinimumColumnWidth ] = useState( minimumColumnWidth || '12rem' ); const gridPlacement = isManualPlacement || ( !! columnCount && ! window.__experimentalEnableGridInteractivity ) ? 'manual' : 'auto'; const onChangeType = ( value ) => { if ( value === 'manual' ) { setTempMinimumColumnWidth( minimumColumnWidth || '12rem' ); } else { setTempColumnCount( columnCount || 3 ); setTempRowCount( rowCount ); } onChange( { ...layout, columnCount: value === 'manual' ? tempColumnCount : null, rowCount: value === 'manual' && window.__experimentalEnableGridInteractivity ? tempRowCount : undefined, isManualPlacement: value === 'manual' && window.__experimentalEnableGridInteractivity ? true : undefined, minimumColumnWidth: value === 'auto' ? tempMinimumColumnWidth : null, } ); }; const helpText = gridPlacement === 'manual' ? __( 'Grid items can be manually placed in any position on the grid.' ) : __( 'Grid items are placed automatically depending on their order.' ); return ( <ToggleGroupControl __next40pxDefaultSize __nextHasNoMarginBottom label={ __( 'Grid item position' ) } value={ gridPlacement } onChange={ onChangeType } isBlock help={ window.__experimentalEnableGridInteractivity ? helpText : undefined } > <ToggleGroupControlOption key="auto" value="auto" label={ __( 'Auto' ) } /> <ToggleGroupControlOption key="manual" value="manual" label={ __( 'Manual' ) } /> </ToggleGroupControl> ); }