@wordpress/block-editor
Version:
183 lines (158 loc) • 5.01 kB
JavaScript
/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { useInstanceId } from '@wordpress/compose';
import { getBlockType, hasBlockSupport } from '@wordpress/blocks';
import { __, sprintf } from '@wordpress/i18n';
import { processCSSNesting } from '@wordpress/global-styles-engine';
import { useBlockEditingMode } from '../components/block-editing-mode';
/**
* Internal dependencies
*/
import InspectorControls from '../components/inspector-controls';
import AdvancedPanel, {
validateCSS,
} from '../components/global-styles/advanced-panel';
import { cleanEmptyObject, useStyleOverride } from './utils';
import { store as blockEditorStore } from '../store';
// Stable reference for useInstanceId.
const CUSTOM_CSS_INSTANCE_REFERENCE = {};
// Stable empty object reference for useSelect.
const EMPTY_STYLE = {};
/**
* Inspector control for custom CSS.
*
* @param {Object} props Component props.
* @param {string} props.blockName Block name.
* @param {Function} props.setAttributes Function to set block attributes.
* @param {Object} props.style Block style attribute.
*/
function CustomCSSControl( { blockName, setAttributes, style } ) {
const blockEditingMode = useBlockEditingMode();
if ( blockEditingMode !== 'default' ) {
return null;
}
const blockType = getBlockType( blockName );
function onChange( newStyle ) {
// Normalize whitespace-only CSS to undefined so it gets cleaned up.
const css = newStyle?.css?.trim() ? newStyle.css : undefined;
setAttributes( {
style: cleanEmptyObject( { ...newStyle, css } ),
} );
}
const cssHelpText = sprintf(
// translators: %s: is the name of a block e.g., 'Image' or 'Quote'.
__(
'Add your own CSS to customize the appearance of the %s block. You do not need to include a CSS selector, just add the property and value, e.g. color: red;.'
),
blockType?.title
);
return (
<InspectorControls group="advanced">
<AdvancedPanel
value={ style }
onChange={ onChange }
inheritedValue={ style }
help={ cssHelpText }
/>
</InspectorControls>
);
}
function CustomCSSEdit( { clientId, name, setAttributes } ) {
const { style, canEditCSS } = useSelect(
( select ) => {
const { getBlockAttributes, getSettings } =
select( blockEditorStore );
return {
style: getBlockAttributes( clientId )?.style || EMPTY_STYLE,
canEditCSS: getSettings().canEditCSS,
};
},
[ clientId ]
);
// Don't render the panel if user lacks edit_css capability.
if ( ! canEditCSS ) {
return null;
}
return (
<CustomCSSControl
blockName={ name }
setAttributes={ setAttributes }
style={ style }
/>
);
}
/**
* Hook to handle custom CSS for a block in the editor.
* Generates a unique class and applies scoped CSS via style override.
*
* @param {Object} props Block props.
* @param {Object} props.style Block style attribute.
* @return {Object} Block props including className for custom CSS scoping.
*/
function useBlockProps( { style } ) {
const customCSS = style?.css;
// Validate CSS is non-empty and passes validation checks.
const isValidCSS =
typeof customCSS === 'string' &&
customCSS.trim().length > 0 &&
validateCSS( customCSS );
const customCSSIdentifier = useInstanceId(
CUSTOM_CSS_INSTANCE_REFERENCE,
'wp-custom-css'
);
const customCSSSelector = `.${ customCSSIdentifier }`;
// Transform the custom CSS using the same logic as global styles.
// Only process if CSS is valid (doesn't contain HTML markup).
const transformedCSS = useMemo( () => {
if ( ! isValidCSS ) {
return undefined;
}
return processCSSNesting( customCSS, customCSSSelector );
}, [ customCSS, customCSSSelector, isValidCSS ] );
// Inject the CSS via style override.
useStyleOverride( { css: transformedCSS } );
// Only add the class if there's valid custom CSS.
if ( ! isValidCSS ) {
return {};
}
return {
className: `has-custom-css ${ customCSSIdentifier }`,
};
}
/**
* Adds a marker class to blocks with custom CSS for server-side rendering.
*
* @param {Object} props Additional props applied to save element.
* @param {Object} blockType Block type definition.
* @param {Object} attributes Block's attributes.
* @return {Object} Filtered props applied to save element.
*/
function addSaveProps( props, blockType, attributes ) {
if ( ! hasBlockSupport( blockType, 'customCSS', true ) ) {
return props;
}
if ( ! attributes?.style?.css?.trim() ) {
return props;
}
// Add a class to indicate this block has custom CSS.
// The actual CSS is rendered server-side using the render_block filter.
const className = props.className
? `${ props.className } has-custom-css`
: 'has-custom-css';
return {
...props,
className,
};
}
export default {
edit: CustomCSSEdit,
useBlockProps,
addSaveProps,
attributeKeys: [ 'style' ],
hasSupport( name ) {
return hasBlockSupport( name, 'customCSS', true );
},
};