UNPKG

@wordpress/editor

Version:
256 lines (222 loc) 7.56 kB
import { capitalCase } from 'change-case'; // Em dash shown when a value isn't set. export const EMPTY_VALUE_LABEL = '\u2014'; // Preset values come in two forms: the user form `var:preset|color|vivid-red` // and the CSS custom property form `var(--wp--preset--color--vivid-red)`. In // both the slug (e.g. `vivid-red`) is the last segment. const PRESET_USER_PREFIX = 'var:preset|'; const PRESET_CSS_VAR_REGEX = /^var\(\s*--wp--preset--[a-z0-9-]+?\s*(?:,[^)]*)?\)$/i; // Returns a preset's slug from either form, or `undefined` when the value isn't // a preset. function getPresetSlug( value ) { if ( value.startsWith( PRESET_USER_PREFIX ) ) { return value.split( '|' ).pop(); } if ( PRESET_CSS_VAR_REGEX.test( value ) ) { return value .replace( /^var\(\s*/, '' ) .replace( /\s*(?:,[^)]*)?\)$/, '' ) .split( '--' ) .pop(); } return undefined; } // True when a value is actually set. const isSet = ( value ) => value !== undefined && value !== null && value !== ''; const BORDER_SIDES = [ 'top', 'right', 'bottom', 'left' ]; // Global Styles stores border style, width and color per side (e.g. // `border.top.style`). Collapse a per-side border object to flat // `{ width, style, color }`, keeping a value only when every set side agrees. function flattenBorder( border ) { if ( isSet( border.width ) || isSet( border.style ) || isSet( border.color ) ) { return border; } const setSides = BORDER_SIDES.filter( ( side ) => border[ side ] ); if ( ! setSides.length ) { return border; } const collapse = ( property ) => { const values = setSides.map( ( side ) => border[ side ]?.[ property ] ); return values.every( ( value ) => value === values[ 0 ] ) ? values[ 0 ] : undefined; }; return { width: collapse( 'width' ), style: collapse( 'style' ), color: collapse( 'color' ), }; } /** * Turns a raw style value into readable text for the modal. * * - Empty, `null` or `undefined`: an em dash. * - Preset values (`var:preset|type|slug` or `var(--wp--preset--type--slug)`): * the readable slug. * - Strings and numbers: used as-is. * - Objects and arrays (like a border side): turned into text, or an em dash * when there's nothing to show. * * @param {*} value The raw style value. * * @return {string} Readable text for the value. */ export function formatStyleValue( value ) { if ( ! isSet( value ) ) { return EMPTY_VALUE_LABEL; } if ( typeof value === 'string' ) { const presetSlug = getPresetSlug( value ); if ( presetSlug ) { return capitalCase( presetSlug ); } return value; } if ( typeof value === 'number' ) { return String( value ); } if ( typeof value === 'object' ) { try { const stringified = JSON.stringify( value ); return stringified && stringified !== '{}' && stringified !== '[]' ? stringified : EMPTY_VALUE_LABEL; } catch { return EMPTY_VALUE_LABEL; } } return String( value ); } /** * Turns a border object into a single CSS `border` value, e.g. * `2px dashed #000fff`. * * The parts follow the CSS order of `width style color`, anything that isn't * set is left out, and preset colors are shown by name. A per-side object (as * Global Styles stores borders) is collapsed to its shared values first. * Returns an em dash when the border has nothing set. * * @param {*} border A border object, e.g. `{ color, width, style }`, or a * per-side object, e.g. `{ top: { style } }`. * * @return {string} The combined border value, or an em dash. */ export function formatBorderShorthand( border ) { if ( ! border || typeof border !== 'object' ) { return formatStyleValue( border ); } const { width, style, color } = flattenBorder( border ); const parts = [ width, style, color ] .filter( isSet ) .map( ( part ) => formatStyleValue( part ) ); return parts.length ? parts.join( ' ' ) : EMPTY_VALUE_LABEL; } // Corner order used by the CSS `border-radius` shorthand. const RADIUS_CORNERS = [ 'topLeft', 'topRight', 'bottomRight', 'bottomLeft' ]; /** * Turns a border radius into readable text. * * A plain string is used as-is. An object with a value per corner is joined in * CSS corner order, e.g. `1px 20px 1px 15px`. Returns an em dash when there's * nothing to show. * * @param {*} radius A radius string or an object with a value per corner. * * @return {string} The readable radius, or an em dash. */ export function formatBorderRadius( radius ) { if ( radius && typeof radius === 'object' ) { const parts = RADIUS_CORNERS.map( ( corner ) => radius[ corner ] ).filter( isSet ); return parts.length ? parts.join( ' ' ) : EMPTY_VALUE_LABEL; } return formatStyleValue( radius ); } /** * Turns a spacing value (padding or margin) into a single CSS value. * * A plain string is used as-is. An object with a value per side is shortened * to the smallest CSS form: one value when every side matches, a * `vertical horizontal` pair when top/bottom and left/right match, otherwise * all four sides in `top right bottom left` order. * * The optional `resolve` callback swaps a raw value for its real one first, * e.g. turning `var:preset|spacing|40` into its actual size, which reads * better than the preset name. * * @param {*} spacing A spacing string or an object with a value per side. * @param {Function} resolve Optional callback to resolve each raw value. * * @return {string} The combined spacing, or an em dash. */ export function formatSpacingShorthand( spacing, resolve = ( value ) => value ) { const format = ( value ) => formatStyleValue( resolve( value ) ); if ( ! spacing || typeof spacing !== 'object' ) { return format( spacing ); } const { top, right, bottom, left } = spacing; if ( isSet( top ) && isSet( right ) && isSet( bottom ) && isSet( left ) ) { if ( top === right && right === bottom && bottom === left ) { return format( top ); } if ( top === bottom && left === right ) { return `${ format( top ) } ${ format( left ) }`; } return [ top, right, bottom, left ].map( format ).join( ' ' ); } if ( ! isSet( top ) && ! isSet( right ) && ! isSet( bottom ) && ! isSet( left ) ) { return EMPTY_VALUE_LABEL; } // Keep all four slots so an unset side still shows its position. return [ top, right, bottom, left ] .map( ( value ) => isSet( value ) ? format( value ) : EMPTY_VALUE_LABEL ) .join( ' ' ); } /** * Turns a block gap value into readable text. * * A plain string is used as-is. An axial object with a `top` (row) and `left` * (column) gap reads as one value when both match, or `row - column` when they * differ. * * The optional `resolve` callback swaps a raw value for its real one first, * e.g. turning `var:preset|spacing|40` into its actual size. * * @param {*} gap A gap string or an axial `{ top, left }` object. * @param {Function} resolve Optional callback to resolve each raw value. * * @return {string} The readable gap, or an em dash. */ export function formatBlockGap( gap, resolve = ( value ) => value ) { const format = ( value ) => formatStyleValue( resolve( value ) ); if ( ! gap || typeof gap !== 'object' ) { return format( gap ); } const { top: row, left: column } = gap; if ( ! isSet( row ) && ! isSet( column ) ) { return EMPTY_VALUE_LABEL; } if ( row === column ) { return format( row ); } const rowLabel = isSet( row ) ? format( row ) : EMPTY_VALUE_LABEL; const columnLabel = isSet( column ) ? format( column ) : EMPTY_VALUE_LABEL; return `${ rowLabel } - ${ columnLabel }`; }