@wordpress/block-editor
Version:
440 lines (392 loc) • 11.6 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
import { isObject } from 'lodash';
/**
* WordPress dependencies
*/
import { addFilter } from '@wordpress/hooks';
import { getBlockSupport } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { useRef, useEffect, Platform } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
/**
* Internal dependencies
*/
import {
getColorClassName,
getColorObjectByColorValue,
getColorObjectByAttributeValues,
} from '../components/colors';
import {
__experimentalGetGradientClass,
getGradientValueBySlug,
getGradientSlugByValue,
} from '../components/gradients';
import { cleanEmptyObject } from './utils';
import ColorPanel from './color-panel';
import useEditorFeature from '../components/use-editor-feature';
export const COLOR_SUPPORT_KEY = 'color';
const EMPTY_ARRAY = [];
const hasColorSupport = ( blockType ) => {
const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY );
return (
colorSupport &&
( colorSupport.link === true ||
colorSupport.gradient === true ||
colorSupport.background !== false ||
colorSupport.text !== false )
);
};
const shouldSkipSerialization = ( blockType ) => {
const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY );
return colorSupport?.__experimentalSkipSerialization;
};
const hasLinkColorSupport = ( blockType ) => {
if ( Platform.OS !== 'web' ) {
return false;
}
const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY );
return isObject( colorSupport ) && !! colorSupport.link;
};
const hasGradientSupport = ( blockType ) => {
if ( Platform.OS !== 'web' ) {
return false;
}
const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY );
return isObject( colorSupport ) && !! colorSupport.gradients;
};
const hasBackgroundColorSupport = ( blockType ) => {
const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY );
return colorSupport && colorSupport.background !== false;
};
const hasTextColorSupport = ( blockType ) => {
const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY );
return colorSupport && colorSupport.text !== false;
};
/**
* Filters registered block settings, extending attributes to include
* `backgroundColor` and `textColor` attribute.
*
* @param {Object} settings Original block settings
* @return {Object} Filtered block settings
*/
function addAttributes( settings ) {
if ( ! hasColorSupport( settings ) ) {
return settings;
}
// allow blocks to specify their own attribute definition with default values if needed.
if ( ! settings.attributes.backgroundColor ) {
Object.assign( settings.attributes, {
backgroundColor: {
type: 'string',
},
} );
}
if ( ! settings.attributes.textColor ) {
Object.assign( settings.attributes, {
textColor: {
type: 'string',
},
} );
}
if ( hasGradientSupport( settings ) && ! settings.attributes.gradient ) {
Object.assign( settings.attributes, {
gradient: {
type: 'string',
},
} );
}
return settings;
}
/**
* Override props assigned to save component to inject colors classnames.
*
* @param {Object} props Additional props applied to save element
* @param {Object} blockType Block type
* @param {Object} attributes Block attributes
* @return {Object} Filtered props applied to save element
*/
export function addSaveProps( props, blockType, attributes ) {
if (
! hasColorSupport( blockType ) ||
shouldSkipSerialization( blockType )
) {
return props;
}
const hasGradient = hasGradientSupport( blockType );
// I'd have prefered to avoid the "style" attribute usage here
const { backgroundColor, textColor, gradient, style } = attributes;
const backgroundClass = getColorClassName(
'background-color',
backgroundColor
);
const gradientClass = __experimentalGetGradientClass( gradient );
const textClass = getColorClassName( 'color', textColor );
const newClassName = classnames(
props.className,
textClass,
gradientClass,
{
// Don't apply the background class if there's a custom gradient
[ backgroundClass ]:
( ! hasGradient || ! style?.color?.gradient ) &&
!! backgroundClass,
'has-text-color': textColor || style?.color?.text,
'has-background':
backgroundColor ||
style?.color?.background ||
( hasGradient && ( gradient || style?.color?.gradient ) ),
'has-link-color': style?.color?.link,
}
);
props.className = newClassName ? newClassName : undefined;
return props;
}
/**
* Filters registered block settings to extand the block edit wrapper
* to apply the desired styles and classnames properly.
*
* @param {Object} settings Original block settings
* @return {Object} Filtered block settings
*/
export function addEditProps( settings ) {
if (
! hasColorSupport( settings ) ||
shouldSkipSerialization( settings )
) {
return settings;
}
const existingGetEditWrapperProps = settings.getEditWrapperProps;
settings.getEditWrapperProps = ( attributes ) => {
let props = {};
if ( existingGetEditWrapperProps ) {
props = existingGetEditWrapperProps( attributes );
}
return addSaveProps( props, settings, attributes );
};
return settings;
}
const getLinkColorFromAttributeValue = ( colors, value ) => {
const attributeParsed = /var:preset\|color\|(.+)/.exec( value );
if ( attributeParsed && attributeParsed[ 1 ] ) {
return getColorObjectByAttributeValues( colors, attributeParsed[ 1 ] )
.color;
}
return value;
};
/**
* Inspector control panel containing the color related configuration
*
* @param {Object} props
*
* @return {WPElement} Color edit element.
*/
export function ColorEdit( props ) {
const { name: blockName, attributes } = props;
const isLinkColorEnabled = useEditorFeature( 'color.link' );
const colors = useEditorFeature( 'color.palette' ) || EMPTY_ARRAY;
const gradients = useEditorFeature( 'color.gradients' ) || EMPTY_ARRAY;
// Shouldn't be needed but right now the ColorGradientsPanel
// can trigger both onChangeColor and onChangeBackground
// synchronously causing our two callbacks to override changes
// from each other.
const localAttributes = useRef( attributes );
useEffect( () => {
localAttributes.current = attributes;
}, [ attributes ] );
if ( ! hasColorSupport( blockName ) || Platform.OS !== 'web' ) {
return null;
}
const hasBackground = hasBackgroundColorSupport( blockName );
const hasGradient = hasGradientSupport( blockName );
const { style, textColor, backgroundColor, gradient } = attributes;
let gradientValue;
if ( hasGradient && gradient ) {
gradientValue = getGradientValueBySlug( gradients, gradient );
} else if ( hasGradient ) {
gradientValue = style?.color?.gradient;
}
const onChangeColor = ( name ) => ( value ) => {
const colorObject = getColorObjectByColorValue( colors, value );
const attributeName = name + 'Color';
const newStyle = {
...localAttributes.current.style,
color: {
...localAttributes.current?.style?.color,
[ name ]: colorObject?.slug ? undefined : value,
},
};
const newNamedColor = colorObject?.slug ? colorObject.slug : undefined;
const newAttributes = {
style: cleanEmptyObject( newStyle ),
[ attributeName ]: newNamedColor,
};
props.setAttributes( newAttributes );
localAttributes.current = {
...localAttributes.current,
...newAttributes,
};
};
const onChangeGradient = ( value ) => {
const slug = getGradientSlugByValue( gradients, value );
let newAttributes;
if ( slug ) {
const newStyle = {
...localAttributes.current?.style,
color: {
...localAttributes.current?.style?.color,
gradient: undefined,
},
};
newAttributes = {
style: cleanEmptyObject( newStyle ),
gradient: slug,
};
} else {
const newStyle = {
...localAttributes.current?.style,
color: {
...localAttributes.current?.style?.color,
gradient: value,
},
};
newAttributes = {
style: cleanEmptyObject( newStyle ),
gradient: undefined,
};
}
props.setAttributes( newAttributes );
localAttributes.current = {
...localAttributes.current,
...newAttributes,
};
};
const onChangeLinkColor = ( value ) => {
const colorObject = getColorObjectByColorValue( colors, value );
props.setAttributes( {
style: {
...props.attributes.style,
color: {
...props.attributes.style?.color,
link: colorObject?.slug
? `var:preset|color|${ colorObject.slug }`
: value,
},
},
} );
};
return (
<ColorPanel
enableContrastChecking={
// Turn on contrast checker for web only since it's not supported on mobile yet.
Platform.OS === 'web' && ! gradient && ! style?.color?.gradient
}
clientId={ props.clientId }
settings={ [
...( hasTextColorSupport( blockName )
? [
{
label: __( 'Text color' ),
onColorChange: onChangeColor( 'text' ),
colorValue: getColorObjectByAttributeValues(
colors,
textColor,
style?.color?.text
).color,
},
]
: [] ),
...( hasBackground || hasGradient
? [
{
label: __( 'Background color' ),
onColorChange: hasBackground
? onChangeColor( 'background' )
: undefined,
colorValue: getColorObjectByAttributeValues(
colors,
backgroundColor,
style?.color?.background
).color,
gradientValue,
onGradientChange: hasGradient
? onChangeGradient
: undefined,
},
]
: [] ),
...( isLinkColorEnabled && hasLinkColorSupport( blockName )
? [
{
label: __( 'Link Color' ),
onColorChange: onChangeLinkColor,
colorValue: getLinkColorFromAttributeValue(
colors,
style?.color?.link
),
clearable: !! props.attributes.style?.color
?.link,
},
]
: [] ),
] }
/>
);
}
/**
* This adds inline styles for color palette colors.
* Ideally, this is not needed and themes should load their palettes on the editor.
*
* @param {Function} BlockListBlock Original component
* @return {Function} Wrapped component
*/
export const withColorPaletteStyles = createHigherOrderComponent(
( BlockListBlock ) => ( props ) => {
const { name, attributes } = props;
const { backgroundColor, textColor } = attributes;
const colors = useEditorFeature( 'color.palette' ) || EMPTY_ARRAY;
if ( ! hasColorSupport( name ) || shouldSkipSerialization( name ) ) {
return <BlockListBlock { ...props } />;
}
const extraStyles = {
color: textColor
? getColorObjectByAttributeValues( colors, textColor )?.color
: undefined,
backgroundColor: backgroundColor
? getColorObjectByAttributeValues( colors, backgroundColor )
?.color
: undefined,
};
let wrapperProps = props.wrapperProps;
wrapperProps = {
...props.wrapperProps,
style: {
...extraStyles,
...props.wrapperProps?.style,
},
};
return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />;
}
);
addFilter(
'blocks.registerBlockType',
'core/color/addAttribute',
addAttributes
);
addFilter(
'blocks.getSaveContent.extraProps',
'core/color/addSaveProps',
addSaveProps
);
addFilter(
'blocks.registerBlockType',
'core/color/addEditProps',
addEditProps
);
addFilter(
'editor.BlockListBlock',
'core/color/with-color-palette-styles',
withColorPaletteStyles
);