@wordpress/block-editor
Version:
372 lines (337 loc) • 12.3 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { createElement, Fragment } from "@wordpress/element";
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useContext, useMemo, createPortal } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
import { getBlockSupport, hasBlockSupport, __EXPERIMENTAL_ELEMENTS as ELEMENTS } from '@wordpress/blocks';
import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose';
import { getCSSRules, compileCSS } from '@wordpress/style-engine';
/**
* 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 { DIMENSIONS_SUPPORT_KEY, SPACING_SUPPORT_KEY, DimensionsPanel } from './dimensions';
import useDisplayBlockControls from '../components/use-display-block-controls';
import { shouldSkipSerialization } from './utils';
import { useBlockEditingMode } from '../components/block-editing-mode';
const styleSupportKeys = [...TYPOGRAPHY_SUPPORT_KEYS, BORDER_SUPPORT_KEY, COLOR_SUPPORT_KEY, DIMENSIONS_SUPPORT_KEY, SPACING_SUPPORT_KEY];
const hasStyleSupport = blockType => styleSupportKeys.some(key => hasBlockSupport(blockType, 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]
};
/**
* 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']
};
/**
* 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} 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;
Object.entries(skipPaths).forEach(([indicator, path]) => {
const skipSerialization = getBlockSupport(blockType, 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;
}
/**
* 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();
const blockEditingMode = useBlockEditingMode();
return createElement(Fragment, null, shouldDisplayControls && blockEditingMode === 'default' && createElement(Fragment, null, createElement(ColorEdit, props), createElement(TypographyPanel, props), createElement(BorderPanel, props), createElement(DimensionsPanel, props)), createElement(BlockEdit, props));
}, 'withToolbarControls');
/**
* Override the default block element to include elements styles.
*
* @param {Function} BlockListBlock Original component
* @return {Function} Wrapped component
*/
const withElementsStyles = createHigherOrderComponent(BlockListBlock => props => {
const blockElementsContainerIdentifier = `wp-elements-${useInstanceId(BlockListBlock)}`;
const skipLinkColorSerialization = shouldSkipSerialization(props.name, COLOR_SUPPORT_KEY, 'link');
const styles = useMemo(() => {
// The .editor-styles-wrapper selector is required on elements styles. As it is
// added to all other editor styles, not providing it causes reset and global
// styles to override element styles because of higher specificity.
const elements = [{
styles: !skipLinkColorSerialization ? props.attributes.style?.elements?.link : undefined,
selector: `.editor-styles-wrapper .${blockElementsContainerIdentifier} ${ELEMENTS.link}`
}, {
styles: !skipLinkColorSerialization ? props.attributes.style?.elements?.link?.[':hover'] : undefined,
selector: `.editor-styles-wrapper .${blockElementsContainerIdentifier} ${ELEMENTS.link}:hover`
}];
const elementCssRules = [];
for (const {
styles: elementStyles,
selector
} of elements) {
if (elementStyles) {
const cssRule = compileCSS(elementStyles, {
selector
});
elementCssRules.push(cssRule);
}
}
return elementCssRules.length > 0 ? elementCssRules.join('') : undefined;
}, [props.attributes.style?.elements, blockElementsContainerIdentifier, skipLinkColorSerialization]);
const element = useContext(BlockList.__unstableElementContext);
return createElement(Fragment, null, styles && element && createPortal(createElement("style", {
dangerouslySetInnerHTML: {
__html: styles
}
}), element), createElement(BlockListBlock, _extends({}, props, {
className: props.attributes.style?.elements ? classnames(props.className, blockElementsContainerIdentifier) : props.className
})));
}, 'withElementsStyles');
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);
//# sourceMappingURL=style.js.map