@wordpress/block-editor
Version:
374 lines (343 loc) • 10.7 kB
JavaScript
/**
* WordPress dependencies
*/
import { getBlockTypes, store as blocksStore } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import { useContext, useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import {
GlobalStylesContext,
toStyles,
getBlockSelectors,
} from '../components/global-styles';
import { usePrivateStyleOverride } from './utils';
import { getValueFromObjectPath } from '../utils/object';
import { store as blockEditorStore } from '../store';
import { globalStylesDataKey } from '../store/private-keys';
import { unlock } from '../lock-unlock';
const VARIATION_PREFIX = 'is-style-';
function getVariationMatches( className ) {
if ( ! className ) {
return [];
}
return className.split( /\s+/ ).reduce( ( matches, name ) => {
if ( name.startsWith( VARIATION_PREFIX ) ) {
const match = name.slice( VARIATION_PREFIX.length );
if ( match !== 'default' ) {
matches.push( match );
}
}
return matches;
}, [] );
}
/**
* Get the first block style variation that has been registered from the class string.
*
* @param {string} className CSS class string for a block.
* @param {Array} registeredStyles Currently registered block styles.
*
* @return {string|null} The name of the first registered variation.
*/
function getVariationNameFromClass( className, registeredStyles = [] ) {
// The global flag affects how capturing groups work in JS. So the regex
// below will only return full CSS classes not just the variation name.
const matches = getVariationMatches( className );
if ( ! matches ) {
return null;
}
for ( const variation of matches ) {
if ( registeredStyles.some( ( style ) => style.name === variation ) ) {
return variation;
}
}
return null;
}
// A helper component to apply a style override using the useStyleOverride hook.
function OverrideStyles( { override } ) {
usePrivateStyleOverride( override );
}
/**
* This component is used to generate new block style variation overrides
* based on an incoming theme config. If a matching style is found in the config,
* a new override is created and returned. The overrides can be used in conjunction with
* useStyleOverride to apply the new styles to the editor. Its use is
* subject to change.
*
* @param {Object} props Props.
* @param {Object} props.config A global styles object, containing settings and styles.
* @return {JSX.Element|undefined} An array of new block variation overrides.
*/
export function __unstableBlockStyleVariationOverridesWithConfig( { config } ) {
const { getBlockStyles, overrides } = useSelect(
( select ) => ( {
getBlockStyles: select( blocksStore ).getBlockStyles,
overrides: unlock( select( blockEditorStore ) ).getStyleOverrides(),
} ),
[]
);
const { getBlockName } = useSelect( blockEditorStore );
const overridesWithConfig = useMemo( () => {
if ( ! overrides?.length ) {
return;
}
const newOverrides = [];
const overriddenClientIds = [];
for ( const [ , override ] of overrides ) {
if (
override?.variation &&
override?.clientId &&
/*
* Because this component overwrites existing style overrides,
* filter out any overrides that are already present in the store.
*/
! overriddenClientIds.includes( override.clientId )
) {
const blockName = getBlockName( override.clientId );
const configStyles =
config?.styles?.blocks?.[ blockName ]?.variations?.[
override.variation
];
if ( configStyles ) {
const variationConfig = {
settings: config?.settings,
// The variation style data is all that is needed to generate
// the styles for the current application to a block. The variation
// name is updated to match the instance specific class name.
styles: {
blocks: {
[ blockName ]: {
variations: {
[ `${ override.variation }-${ override.clientId }` ]:
configStyles,
},
},
},
},
};
const blockSelectors = getBlockSelectors(
getBlockTypes(),
getBlockStyles,
override.clientId
);
const hasBlockGapSupport = false;
const hasFallbackGapSupport = true;
const disableLayoutStyles = true;
const disableRootPadding = true;
const variationStyles = toStyles(
variationConfig,
blockSelectors,
hasBlockGapSupport,
hasFallbackGapSupport,
disableLayoutStyles,
disableRootPadding,
{
blockGap: false,
blockStyles: true,
layoutStyles: false,
marginReset: false,
presets: false,
rootPadding: false,
variationStyles: true,
}
);
newOverrides.push( {
id: `${ override.variation }-${ override.clientId }`,
css: variationStyles,
__unstableType: 'variation',
variation: override.variation,
// The clientId will be stored with the override and used to ensure
// the order of overrides matches the order of blocks so that the
// correct CSS cascade is maintained.
clientId: override.clientId,
} );
overriddenClientIds.push( override.clientId );
}
}
}
return newOverrides;
}, [ config, overrides, getBlockStyles, getBlockName ] );
if ( ! overridesWithConfig || ! overridesWithConfig.length ) {
return;
}
return (
<>
{ overridesWithConfig.map( ( override ) => (
<OverrideStyles key={ override.id } override={ override } />
) ) }
</>
);
}
/**
* Retrieves any variation styles data and resolves any referenced values.
*
* @param {Object} globalStyles A complete global styles object, containing settings and styles.
* @param {string} name The name of the desired block type.
* @param {variation} variation The of the block style variation to retrieve data for.
*
* @return {Object|undefined} The global styles data for the specified variation.
*/
export function getVariationStylesWithRefValues(
globalStyles,
name,
variation
) {
if ( ! globalStyles?.styles?.blocks?.[ name ]?.variations?.[ variation ] ) {
return;
}
// Helper to recursively look for `ref` values to resolve.
const replaceRefs = ( variationStyles ) => {
Object.keys( variationStyles ).forEach( ( key ) => {
const value = variationStyles[ key ];
// Only process objects.
if ( typeof value === 'object' && value !== null ) {
// Process `ref` value if present.
if ( value.ref !== undefined ) {
if (
typeof value.ref !== 'string' ||
value.ref.trim() === ''
) {
// Remove invalid ref.
delete variationStyles[ key ];
} else {
// Resolve `ref` value.
const refValue = getValueFromObjectPath(
globalStyles,
value.ref
);
if ( refValue ) {
variationStyles[ key ] = refValue;
} else {
delete variationStyles[ key ];
}
}
} else {
// Recursively resolve `ref` values in nested objects.
replaceRefs( value );
// After recursion, if value is empty due to explicitly
// `undefined` ref value, remove it.
if ( Object.keys( value ).length === 0 ) {
delete variationStyles[ key ];
}
}
}
} );
};
// Deep clone variation node to avoid mutating it within global styles and losing refs.
const styles = JSON.parse(
JSON.stringify(
globalStyles.styles.blocks[ name ].variations[ variation ]
)
);
replaceRefs( styles );
return styles;
}
function useBlockStyleVariation( name, variation, clientId ) {
// Prefer global styles data in GlobalStylesContext, which are available
// if in the site editor. Otherwise fall back to whatever is in the
// editor settings and available in the post editor.
const { merged: mergedConfig } = useContext( GlobalStylesContext );
const { globalSettings, globalStyles } = useSelect( ( select ) => {
const settings = select( blockEditorStore ).getSettings();
return {
globalSettings: settings.__experimentalFeatures,
globalStyles: settings[ globalStylesDataKey ],
};
}, [] );
return useMemo( () => {
const variationStyles = getVariationStylesWithRefValues(
{
settings: mergedConfig?.settings ?? globalSettings,
styles: mergedConfig?.styles ?? globalStyles,
},
name,
variation
);
return {
settings: mergedConfig?.settings ?? globalSettings,
// The variation style data is all that is needed to generate
// the styles for the current application to a block. The variation
// name is updated to match the instance specific class name.
styles: {
blocks: {
[ name ]: {
variations: {
[ `${ variation }-${ clientId }` ]: variationStyles,
},
},
},
},
};
}, [
mergedConfig,
globalSettings,
globalStyles,
variation,
clientId,
name,
] );
}
// Rather than leveraging `useInstanceId` here, the `clientId` is used.
// This is so that the variation style override's ID is predictable
// when the order of applied style variations changes.
function useBlockProps( { name, className, clientId } ) {
const { getBlockStyles } = useSelect( blocksStore );
const registeredStyles = getBlockStyles( name );
const variation = getVariationNameFromClass( className, registeredStyles );
const variationClass = `${ VARIATION_PREFIX }${ variation }-${ clientId }`;
const { settings, styles } = useBlockStyleVariation(
name,
variation,
clientId
);
const variationStyles = useMemo( () => {
if ( ! variation ) {
return;
}
const variationConfig = { settings, styles };
const blockSelectors = getBlockSelectors(
getBlockTypes(),
getBlockStyles,
clientId
);
const hasBlockGapSupport = false;
const hasFallbackGapSupport = true;
const disableLayoutStyles = true;
const disableRootPadding = true;
return toStyles(
variationConfig,
blockSelectors,
hasBlockGapSupport,
hasFallbackGapSupport,
disableLayoutStyles,
disableRootPadding,
{
blockGap: false,
blockStyles: true,
layoutStyles: false,
marginReset: false,
presets: false,
rootPadding: false,
variationStyles: true,
}
);
}, [ variation, settings, styles, getBlockStyles, clientId ] );
usePrivateStyleOverride( {
id: `variation-${ clientId }`,
css: variationStyles,
__unstableType: 'variation',
variation,
// The clientId will be stored with the override and used to ensure
// the order of overrides matches the order of blocks so that the
// correct CSS cascade is maintained.
clientId,
} );
return variation ? { className: variationClass } : {};
}
export default {
hasSupport: () => true,
attributeKeys: [ 'className' ],
isMatch: ( { className } ) => getVariationMatches( className ).length > 0,
useBlockProps,
};