@gechiui/block-editor
Version:
358 lines (322 loc) • 8.94 kB
JavaScript
/**
* External dependencies
*/
import {
first,
forEach,
get,
has,
isEmpty,
isString,
kebabCase,
map,
omit,
startsWith,
} from 'lodash';
import classnames from 'classnames';
/**
* GeChiUI dependencies
*/
import { useContext, createPortal } from '@gechiui/element';
import { addFilter } from '@gechiui/hooks';
import {
getBlockSupport,
hasBlockSupport,
__EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY,
__EXPERIMENTAL_ELEMENTS as ELEMENTS,
} from '@gechiui/blocks';
import { createHigherOrderComponent, useInstanceId } from '@gechiui/compose';
/**
* Internal dependencies
*/
import BlockList from '../components/block-list';
import { BORDER_SUPPORT_KEY, BorderPanel } from './border';
import { COLOR_SUPPORT_KEY, ColorEdit } from './color';
import {
TypographyPanel,
TYPOGRAPHY_SUPPORT_KEY,
TYPOGRAPHY_SUPPORT_KEYS,
} from './typography';
import { SPACING_SUPPORT_KEY, DimensionsPanel } from './dimensions';
import useDisplayBlockControls from '../components/use-display-block-controls';
const styleSupportKeys = [
...TYPOGRAPHY_SUPPORT_KEYS,
BORDER_SUPPORT_KEY,
COLOR_SUPPORT_KEY,
SPACING_SUPPORT_KEY,
];
const hasStyleSupport = ( blockType ) =>
styleSupportKeys.some( ( key ) => hasBlockSupport( blockType, key ) );
const VARIABLE_REFERENCE_PREFIX = 'var:';
const VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE = '|';
const VARIABLE_PATH_SEPARATOR_TOKEN_STYLE = '--';
function compileStyleValue( uncompiledValue ) {
if ( startsWith( uncompiledValue, VARIABLE_REFERENCE_PREFIX ) ) {
const variable = uncompiledValue
.slice( VARIABLE_REFERENCE_PREFIX.length )
.split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE )
.join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE );
return `var(--gc--${ variable })`;
}
return uncompiledValue;
}
/**
* 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 ignoredStyles = [ 'spacing.blockGap' ];
const output = {};
Object.keys( STYLE_PROPERTY ).forEach( ( propKey ) => {
const path = STYLE_PROPERTY[ propKey ].value;
const subPaths = STYLE_PROPERTY[ propKey ].properties;
// Ignore styles on elements because they are handled on the server.
if ( has( styles, path ) && 'elements' !== first( path ) ) {
// Checking if style value is a string allows for shorthand css
// option and backwards compatibility for border radius support.
const styleValue = get( styles, path );
if ( !! subPaths && ! isString( styleValue ) ) {
Object.entries( subPaths ).forEach( ( entry ) => {
const [ name, subPath ] = entry;
const value = get( styleValue, [ subPath ] );
if ( value ) {
output[ name ] = compileStyleValue( value );
}
} );
} else if ( ! ignoredStyles.includes( path.join( '.' ) ) ) {
output[ propKey ] = compileStyleValue( get( styles, path ) );
}
}
} );
return output;
}
function compileElementsStyles( selector, elements = {} ) {
return map( elements, ( styles, element ) => {
const elementStyles = getInlineStyles( styles );
if ( ! isEmpty( elementStyles ) ) {
return [
`.${ selector } ${ ELEMENTS[ element ] }{`,
...map(
elementStyles,
( value, property ) =>
`\t${ kebabCase( property ) }: ${ value };`
),
'}',
].join( '\n' );
}
return '';
} ).join( '\n' );
}
/**
* 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,
],
[ `${ SPACING_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
'spacing',
],
};
/**
* 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,
[ `${ SPACING_SUPPORT_KEY }` ]: [ 'spacing.blockGap' ],
};
/**
* Override props assigned to save component to inject the CSS variables definition.
*
* @param {Object} props Additional props applied to save element.
* @param {Object} blockType 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,
blockType,
attributes,
skipPaths = skipSerializationPathsSave
) {
if ( ! hasStyleSupport( blockType ) ) {
return props;
}
let { style } = attributes;
forEach( skipPaths, ( path, indicator ) => {
if ( getBlockSupport( blockType, indicator ) ) {
style = omit( style, path );
}
} );
props.style = {
...getInlineStyles( style ),
...props.style,
};
return props;
}
/**
* Filters registered block settings to extend 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 ( ! hasStyleSupport( settings ) ) {
return settings;
}
const existingGetEditWrapperProps = settings.getEditWrapperProps;
settings.getEditWrapperProps = ( attributes ) => {
let props = {};
if ( existingGetEditWrapperProps ) {
props = existingGetEditWrapperProps( attributes );
}
return addSaveProps(
props,
settings,
attributes,
skipSerializationPathsEdit
);
};
return settings;
}
/**
* Override the default edit UI to include new inspector controls for
* all the custom styles configs.
*
* @param {Function} BlockEdit Original component.
*
* @return {Function} Wrapped component.
*/
export const withBlockControls = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const shouldDisplayControls = useDisplayBlockControls();
return (
<>
{ shouldDisplayControls && (
<>
<ColorEdit { ...props } />
<TypographyPanel { ...props } />
<BorderPanel { ...props } />
<DimensionsPanel { ...props } />
</>
) }
<BlockEdit { ...props } />
</>
);
},
'withToolbarControls'
);
/**
* Override the default block element to include duotone styles.
*
* @param {Function} BlockListBlock Original component
* @return {Function} Wrapped component
*/
const withElementsStyles = createHigherOrderComponent(
( BlockListBlock ) => ( props ) => {
const elements = props.attributes.style?.elements;
const blockElementsContainerIdentifier = `gc-elements-${ useInstanceId(
BlockListBlock
) }`;
const styles = compileElementsStyles(
blockElementsContainerIdentifier,
props.attributes.style?.elements
);
const element = useContext( BlockList.__unstableElementContext );
return (
<>
{ elements &&
element &&
createPortal(
<style
dangerouslySetInnerHTML={ {
__html: styles,
} }
/>,
element
) }
<BlockListBlock
{ ...props }
className={
elements
? classnames(
props.className,
blockElementsContainerIdentifier
)
: props.className
}
/>
</>
);
}
);
addFilter(
'blocks.registerBlockType',
'core/style/addAttribute',
addAttribute
);
addFilter(
'blocks.getSaveContent.extraProps',
'core/style/addSaveProps',
addSaveProps
);
addFilter(
'blocks.registerBlockType',
'core/style/addEditProps',
addEditProps
);
addFilter(
'editor.BlockEdit',
'core/style/with-block-controls',
withBlockControls
);
addFilter(
'editor.BlockListBlock',
'core/editor/with-elements-styles',
withElementsStyles
);