@wordpress/block-editor
Version:
476 lines (432 loc) • 12.4 kB
JavaScript
/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
import {
getBlockSupport,
hasBlockSupport,
__EXPERIMENTAL_ELEMENTS as ELEMENTS,
} from '@wordpress/blocks';
import { useInstanceId } from '@wordpress/compose';
import { getCSSRules, compileCSS } from '@wordpress/style-engine';
/**
* Internal dependencies
*/
import { BACKGROUND_SUPPORT_KEY, BackgroundImagePanel } from './background';
import { BORDER_SUPPORT_KEY, BorderPanel, SHADOW_SUPPORT_KEY } from './border';
import { COLOR_SUPPORT_KEY, ColorEdit } from './color';
import {
TypographyPanel,
TYPOGRAPHY_SUPPORT_KEY,
TYPOGRAPHY_SUPPORT_KEYS,
} from './typography';
import {
DIMENSIONS_SUPPORT_KEY,
SPACING_SUPPORT_KEY,
DimensionsPanel,
} from './dimensions';
import {
shouldSkipSerialization,
useStyleOverride,
useBlockSettings,
} from './utils';
import { scopeSelector } from '../components/global-styles/utils';
import { useBlockEditingMode } from '../components/block-editing-mode';
const styleSupportKeys = [
...TYPOGRAPHY_SUPPORT_KEYS,
BORDER_SUPPORT_KEY,
COLOR_SUPPORT_KEY,
DIMENSIONS_SUPPORT_KEY,
BACKGROUND_SUPPORT_KEY,
SPACING_SUPPORT_KEY,
SHADOW_SUPPORT_KEY,
];
const hasStyleSupport = ( nameOrType ) =>
styleSupportKeys.some( ( key ) => hasBlockSupport( nameOrType, key ) );
/**
* Returns the inline styles to add depending on the style object
*
* @param {Object} styles Styles configuration.
*
* @return {Object} Flattened CSS variables declaration.
*/
export function getInlineStyles( styles = {} ) {
const output = {};
// The goal is to move everything to server side generated engine styles
// This is temporary as we absorb more and more styles into the engine.
getCSSRules( styles ).forEach( ( rule ) => {
output[ rule.key ] = rule.value;
} );
return output;
}
/**
* Filters registered block settings, extending attributes to include `style` attribute.
*
* @param {Object} settings Original block settings.
*
* @return {Object} Filtered block settings.
*/
function addAttribute( settings ) {
if ( ! hasStyleSupport( settings ) ) {
return settings;
}
// Allow blocks to specify their own attribute definition with default values if needed.
if ( ! settings.attributes.style ) {
Object.assign( settings.attributes, {
style: {
type: 'object',
},
} );
}
return settings;
}
/**
* A dictionary of paths to flag skipping block support serialization as the key,
* with values providing the style paths to be omitted from serialization.
*
* @constant
* @type {Record<string, string[]>}
*/
const skipSerializationPathsEdit = {
[ `${ BORDER_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ 'border' ],
[ `${ COLOR_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
COLOR_SUPPORT_KEY,
],
[ `${ TYPOGRAPHY_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
TYPOGRAPHY_SUPPORT_KEY,
],
[ `${ DIMENSIONS_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
DIMENSIONS_SUPPORT_KEY,
],
[ `${ SPACING_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
SPACING_SUPPORT_KEY,
],
[ `${ SHADOW_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
SHADOW_SUPPORT_KEY,
],
};
/**
* A dictionary of paths to flag skipping block support serialization as the key,
* with values providing the style paths to be omitted from serialization.
*
* Extends the Edit skip paths to enable skipping additional paths in just
* the Save component. This allows a block support to be serialized within the
* editor, while using an alternate approach, such as server-side rendering, when
* the support is saved.
*
* @constant
* @type {Record<string, string[]>}
*/
const skipSerializationPathsSave = {
...skipSerializationPathsEdit,
[ `${ DIMENSIONS_SUPPORT_KEY }.aspectRatio` ]: [
`${ DIMENSIONS_SUPPORT_KEY }.aspectRatio`,
], // Skip serialization of aspect ratio in save mode.
[ `${ BACKGROUND_SUPPORT_KEY }` ]: [ BACKGROUND_SUPPORT_KEY ], // Skip serialization of background support in save mode.
};
const skipSerializationPathsSaveChecks = {
[ `${ DIMENSIONS_SUPPORT_KEY }.aspectRatio` ]: true,
[ `${ BACKGROUND_SUPPORT_KEY }` ]: true,
};
/**
* A dictionary used to normalize feature names between support flags, style
* object properties and __experimentSkipSerialization configuration arrays.
*
* This allows not having to provide a migration for a support flag and possible
* backwards compatibility bridges, while still achieving consistency between
* the support flag and the skip serialization array.
*
* @constant
* @type {Record<string, string>}
*/
const renamedFeatures = { gradients: 'gradient' };
/**
* A utility function used to remove one or more paths from a style object.
* Works in a way similar to Lodash's `omit()`. See unit tests and examples below.
*
* It supports a single string path:
*
* ```
* omitStyle( { color: 'red' }, 'color' ); // {}
* ```
*
* or an array of paths:
*
* ```
* omitStyle( { color: 'red', background: '#fff' }, [ 'color', 'background' ] ); // {}
* ```
*
* It also allows you to specify paths at multiple levels in a string.
*
* ```
* omitStyle( { typography: { textDecoration: 'underline' } }, 'typography.textDecoration' ); // {}
* ```
*
* You can remove multiple paths at the same time:
*
* ```
* omitStyle(
* {
* typography: {
* textDecoration: 'underline',
* textTransform: 'uppercase',
* }
* },
* [
* 'typography.textDecoration',
* 'typography.textTransform',
* ]
* );
* // {}
* ```
*
* You can also specify nested paths as arrays:
*
* ```
* omitStyle(
* {
* typography: {
* textDecoration: 'underline',
* textTransform: 'uppercase',
* }
* },
* [
* [ 'typography', 'textDecoration' ],
* [ 'typography', 'textTransform' ],
* ]
* );
* // {}
* ```
*
* With regards to nesting of styles, infinite depth is supported:
*
* ```
* omitStyle(
* {
* border: {
* radius: {
* topLeft: '10px',
* topRight: '0.5rem',
* }
* }
* },
* [
* [ 'border', 'radius', 'topRight' ],
* ]
* );
* // { border: { radius: { topLeft: '10px' } } }
* ```
*
* The third argument, `preserveReference`, defines how to treat the input style object.
* It is mostly necessary to properly handle mutation when recursively handling the style object.
* Defaulting to `false`, this will always create a new object, avoiding to mutate `style`.
* However, when recursing, we change that value to `true` in order to work with a single copy
* of the original style object.
*
* @see https://lodash.com/docs/4.17.15#omit
*
* @param {Object} style Styles object.
* @param {Array|string} paths Paths to remove.
* @param {boolean} preserveReference True to mutate the `style` object, false otherwise.
* @return {Object} Styles object with the specified paths removed.
*/
export function omitStyle( style, paths, preserveReference = false ) {
if ( ! style ) {
return style;
}
let newStyle = style;
if ( ! preserveReference ) {
newStyle = JSON.parse( JSON.stringify( style ) );
}
if ( ! Array.isArray( paths ) ) {
paths = [ paths ];
}
paths.forEach( ( path ) => {
if ( ! Array.isArray( path ) ) {
path = path.split( '.' );
}
if ( path.length > 1 ) {
const [ firstSubpath, ...restPath ] = path;
omitStyle( newStyle[ firstSubpath ], [ restPath ], true );
} else if ( path.length === 1 ) {
delete newStyle[ path[ 0 ] ];
}
} );
return newStyle;
}
/**
* Override props assigned to save component to inject the CSS variables definition.
*
* @param {Object} props Additional props applied to save element.
* @param {Object|string} blockNameOrType Block type.
* @param {Object} attributes Block attributes.
* @param {?Record<string, string[]>} skipPaths An object of keys and paths to skip serialization.
*
* @return {Object} Filtered props applied to save element.
*/
export function addSaveProps(
props,
blockNameOrType,
attributes,
skipPaths = skipSerializationPathsSave
) {
if ( ! hasStyleSupport( blockNameOrType ) ) {
return props;
}
let { style } = attributes;
Object.entries( skipPaths ).forEach( ( [ indicator, path ] ) => {
const skipSerialization =
skipSerializationPathsSaveChecks[ indicator ] ||
getBlockSupport( blockNameOrType, indicator );
if ( skipSerialization === true ) {
style = omitStyle( style, path );
}
if ( Array.isArray( skipSerialization ) ) {
skipSerialization.forEach( ( featureName ) => {
const feature = renamedFeatures[ featureName ] || featureName;
style = omitStyle( style, [ [ ...path, feature ] ] );
} );
}
} );
props.style = {
...getInlineStyles( style ),
...props.style,
};
return props;
}
function BlockStyleControls( {
clientId,
name,
setAttributes,
__unstableParentLayout,
} ) {
const settings = useBlockSettings( name, __unstableParentLayout );
const blockEditingMode = useBlockEditingMode();
const passedProps = {
clientId,
name,
setAttributes,
settings: {
...settings,
typography: {
...settings.typography,
// The text alignment UI for individual blocks is rendered in
// the block toolbar, so disable it here.
textAlign: false,
},
},
};
if ( blockEditingMode !== 'default' ) {
return null;
}
return (
<>
<ColorEdit { ...passedProps } />
<BackgroundImagePanel { ...passedProps } />
<TypographyPanel { ...passedProps } />
<BorderPanel { ...passedProps } />
<DimensionsPanel { ...passedProps } />
</>
);
}
export default {
edit: BlockStyleControls,
hasSupport: hasStyleSupport,
addSaveProps,
attributeKeys: [ 'style' ],
useBlockProps,
};
// Defines which element types are supported, including their hover styles or
// any other elements that have been included under a single element type
// e.g. heading and h1-h6.
const elementTypes = [
{ elementType: 'button' },
{ elementType: 'link', pseudo: [ ':hover' ] },
{
elementType: 'heading',
elements: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ],
},
];
// Used for generating the instance ID
const STYLE_BLOCK_PROPS_REFERENCE = {};
function useBlockProps( { name, style } ) {
const blockElementsContainerIdentifier = useInstanceId(
STYLE_BLOCK_PROPS_REFERENCE,
'wp-elements'
);
const baseElementSelector = `.${ blockElementsContainerIdentifier }`;
const blockElementStyles = style?.elements;
const styles = useMemo( () => {
if ( ! blockElementStyles ) {
return;
}
const elementCSSRules = [];
elementTypes.forEach( ( { elementType, pseudo, elements } ) => {
const skipSerialization = shouldSkipSerialization(
name,
COLOR_SUPPORT_KEY,
elementType
);
if ( skipSerialization ) {
return;
}
const elementStyles = blockElementStyles?.[ elementType ];
// Process primary element type styles.
if ( elementStyles ) {
const selector = scopeSelector(
baseElementSelector,
ELEMENTS[ elementType ]
);
elementCSSRules.push(
compileCSS( elementStyles, { selector } )
);
// Process any interactive states for the element type.
if ( pseudo ) {
pseudo.forEach( ( pseudoSelector ) => {
if ( elementStyles[ pseudoSelector ] ) {
elementCSSRules.push(
compileCSS( elementStyles[ pseudoSelector ], {
selector: scopeSelector(
baseElementSelector,
`${ ELEMENTS[ elementType ] }${ pseudoSelector }`
),
} )
);
}
} );
}
}
// Process related elements e.g. h1-h6 for headings
if ( elements ) {
elements.forEach( ( element ) => {
if ( blockElementStyles[ element ] ) {
elementCSSRules.push(
compileCSS( blockElementStyles[ element ], {
selector: scopeSelector(
baseElementSelector,
ELEMENTS[ element ]
),
} )
);
}
} );
}
} );
return elementCSSRules.length > 0
? elementCSSRules.join( '' )
: undefined;
}, [ baseElementSelector, blockElementStyles, name ] );
useStyleOverride( { css: styles } );
return addSaveProps(
{ className: blockElementsContainerIdentifier },
name,
{ style },
skipSerializationPathsEdit
);
}
addFilter(
'blocks.registerBlockType',
'core/style/addAttribute',
addAttribute
);