UNPKG

@wordpress/block-editor

Version:
593 lines (520 loc) 15.8 kB
/** * External dependencies */ import { camelCase } from 'change-case'; import { Dimensions } from 'react-native'; import { colord } from 'colord'; /** * WordPress dependencies */ import { usePreferredColorSchemeStyle } from '@wordpress/compose'; import { createContext, useContext } from '@wordpress/element'; import { getPxFromCssUnit } from '@wordpress/components'; /** * Internal dependencies */ import useMultipleOriginColorsAndGradients from '../colors-gradients/use-multiple-origin-colors-and-gradients'; import { useSettings } from '../use-settings'; import { SETTINGS_DEFAULTS } from '../../store/defaults'; const BLOCK_STYLE_ATTRIBUTES = [ 'textColor', 'backgroundColor', 'style', 'color', 'fontSize', ]; export const GlobalStylesContext = createContext( { style: {} } ); GlobalStylesContext.BLOCK_STYLE_ATTRIBUTES = BLOCK_STYLE_ATTRIBUTES; // Mapping style properties name to native. const BLOCK_STYLE_ATTRIBUTES_MAPPING = { textColor: 'color', text: 'color', background: 'backgroundColor', link: 'linkColor', placeholder: 'placeholderColor', }; const PADDING = 12; // $solid-border-space const UNKNOWN_VALUE = 'undefined'; const DEFAULT_FONT_SIZE = 16; export function getBlockPaddings( mergedStyle, wrapperPropsStyle, blockStyleAttributes, blockColors ) { const blockPaddings = {}; if ( ! mergedStyle.padding && ( wrapperPropsStyle?.backgroundColor || blockStyleAttributes?.backgroundColor || blockColors?.backgroundColor ) ) { blockPaddings.padding = PADDING; return blockPaddings; } // Prevent adding extra paddings to inner blocks without background colors. if ( mergedStyle?.padding && ! wrapperPropsStyle?.backgroundColor && ! blockStyleAttributes?.backgroundColor && ! blockColors?.backgroundColor ) { blockPaddings.padding = undefined; } return blockPaddings; } export function getBlockColors( blockStyleAttributes, defaultColors, blockName, baseGlobalStyles ) { const blockStyles = {}; const customBlockStyles = blockStyleAttributes?.style?.color || {}; const blockGlobalStyles = baseGlobalStyles?.blocks?.[ blockName ]; // Global styles colors. if ( blockGlobalStyles?.color ) { Object.entries( blockGlobalStyles.color ).forEach( ( [ key, value ] ) => { const styleKey = BLOCK_STYLE_ATTRIBUTES_MAPPING[ key ]; if ( styleKey && value !== UNKNOWN_VALUE ) { const color = customBlockStyles[ key ] ?? value; blockStyles[ styleKey ] = color; } } ); } else if ( baseGlobalStyles?.styles?.color?.text ) { blockStyles[ BLOCK_STYLE_ATTRIBUTES_MAPPING.text ] = baseGlobalStyles?.styles?.color?.text; } // Global styles elements. if ( blockGlobalStyles?.elements ) { const linkColor = blockGlobalStyles.elements?.link?.color?.text; const styleKey = BLOCK_STYLE_ATTRIBUTES_MAPPING.link; if ( styleKey && linkColor && linkColor !== UNKNOWN_VALUE ) { blockStyles[ styleKey ] = linkColor; } } // Custom colors. Object.entries( blockStyleAttributes ).forEach( ( [ key, value ] ) => { const isCustomColor = value?.startsWith?.( '#' ); let styleKey = key; if ( BLOCK_STYLE_ATTRIBUTES_MAPPING[ styleKey ] ) { styleKey = BLOCK_STYLE_ATTRIBUTES_MAPPING[ styleKey ]; } if ( ! isCustomColor ) { const mappedColor = Object.values( defaultColors ?? {} ).find( ( { slug } ) => slug === value ); if ( mappedColor ) { blockStyles[ styleKey ] = mappedColor.color; } } else { blockStyles[ styleKey ] = value; } } ); // Color placeholder. if ( blockStyles?.color ) { blockStyles[ BLOCK_STYLE_ATTRIBUTES_MAPPING.placeholder ] = blockStyles.color; } return blockStyles; } function getBlockTypography( blockStyleAttributes, fontSizes, blockName, baseGlobalStyles ) { const typographyStyles = {}; const customBlockStyles = blockStyleAttributes?.style?.typography || {}; const blockGlobalStyles = baseGlobalStyles?.blocks?.[ blockName ]; const parsedFontSizes = Object.values( fontSizes ?? {} ); // Global styles. if ( blockGlobalStyles?.typography ) { const fontSize = blockGlobalStyles?.typography?.fontSize; const lineHeight = blockGlobalStyles?.typography?.lineHeight; if ( fontSize ) { if ( parseInt( fontSize, 10 ) ) { typographyStyles.fontSize = fontSize; } else { const mappedFontSize = parsedFontSizes.find( ( { slug } ) => slug === fontSize ); if ( mappedFontSize ) { typographyStyles.fontSize = mappedFontSize?.size; } } } if ( lineHeight ) { typographyStyles.lineHeight = lineHeight; } } if ( blockStyleAttributes?.fontSize && baseGlobalStyles ) { const mappedFontSize = parsedFontSizes.find( ( { slug } ) => slug === blockStyleAttributes?.fontSize ); if ( mappedFontSize ) { typographyStyles.fontSize = mappedFontSize?.size; } } // Custom styles. if ( customBlockStyles?.fontSize ) { typographyStyles.fontSize = customBlockStyles?.fontSize; } if ( customBlockStyles?.lineHeight ) { typographyStyles.lineHeight = customBlockStyles?.lineHeight; } return typographyStyles; } /** * Return a value from a certain path of the object. * Path is specified as an array of properties, like: [ 'parent', 'child' ]. * * @param {Object} object Input object. * @param {Array} path Path to the object property. * @return {*} Value of the object property at the specified path. */ const getValueFromObjectPath = ( object, path ) => { let value = object; path.forEach( ( fieldName ) => { value = value?.[ fieldName ]; } ); return value; }; export function parseStylesVariables( styles, mappedValues, customValues ) { let stylesBase = styles; const variables = [ 'preset', 'custom', 'var', 'fontSize' ]; if ( ! stylesBase ) { return styles; } variables.forEach( ( variable ) => { // Examples // var(--wp--preset--color--gray) // var(--wp--custom--body--typography--font-family) // var:preset|color|custom-color-2 const regex = new RegExp( `var\\(--wp--${ variable }--(.*?)\\)`, 'g' ); const varRegex = /\"var:preset\|color\|(.*?)\"/gm; const fontSizeRegex = /"fontSize":"(.*?)"/gm; if ( variable === 'preset' ) { stylesBase = stylesBase.replace( regex, ( _$1, $2 ) => { const path = $2.split( '--' ); const mappedPresetValue = mappedValues[ path[ 0 ] ]; if ( mappedPresetValue && mappedPresetValue.slug ) { const matchedValue = Object.values( mappedPresetValue.values ?? {} ).find( ( { slug } ) => slug === path[ 1 ] ); return matchedValue?.[ mappedPresetValue.slug ]; } return UNKNOWN_VALUE; } ); } if ( variable === 'custom' ) { const customValuesData = customValues ?? JSON.parse( stylesBase ); stylesBase = stylesBase.replace( regex, ( _$1, $2 ) => { const path = $2.split( '--' ); // Supports cases for variables like var(--wp--custom--color--background) if ( path[ 0 ] === 'color' ) { const colorKey = path[ path.length - 1 ]; if ( mappedValues?.color ) { const matchedValue = mappedValues.color?.values?.find( ( { slug } ) => slug === colorKey ); if ( matchedValue ) { return `${ matchedValue?.color }`; } } } if ( path.reduce( ( prev, curr ) => prev && prev[ curr ], customValuesData ) ) { return getValueFromObjectPath( customValuesData, path ); } // Check for camelcase properties. return getValueFromObjectPath( customValuesData, [ ...path.slice( 0, path.length - 1 ), camelCase( path[ path.length - 1 ] ), ] ); } ); } if ( variable === 'var' ) { stylesBase = stylesBase.replace( varRegex, ( _$1, $2 ) => { if ( mappedValues?.color ) { const matchedValue = mappedValues.color?.values?.find( ( { slug } ) => slug === $2 ); return `"${ matchedValue?.color }"`; } return UNKNOWN_VALUE; } ); } if ( variable === 'fontSize' ) { const { width, height } = Dimensions.get( 'window' ); stylesBase = stylesBase.replace( fontSizeRegex, ( _$1, $2 ) => { const parsedFontSize = getPxFromCssUnit( $2, { width, height, fontSize: DEFAULT_FONT_SIZE, } ) || `${ DEFAULT_FONT_SIZE }px`; return `"fontSize":"${ parsedFontSize }"`; } ); } } ); return JSON.parse( stylesBase ); } function getMappedValues( features, palette ) { const typography = features?.typography; const colors = [ ...( palette?.theme || [] ), ...( palette?.custom || [] ), ...( palette?.default || [] ), ]; const fontSizes = { ...typography?.fontSizes?.theme, ...typography?.fontSizes?.custom, }; const mappedValues = { color: { values: colors, slug: 'color', }, 'font-size': { values: fontSizes, slug: 'size', }, }; return mappedValues; } /** * Returns the normalized fontSizes to include the sizePx value for each of the different sizes. * * @param {Object} fontSizes found in global styles. * @return {Object} normalized sizes. */ function normalizeFontSizes( fontSizes ) { if ( ! fontSizes ) { return fontSizes; } const dimensions = Dimensions.get( 'window' ); const normalizedFontSizes = {}; const keysToProcess = []; // Check if 'theme' or 'custom' keys exist and add them to keysToProcess array if ( fontSizes?.theme ) { keysToProcess.push( 'theme' ); } if ( fontSizes?.custom ) { keysToProcess.push( 'custom' ); } // If neither 'theme' nor 'custom' exist, add 'default' if it exists if ( keysToProcess.length === 0 && fontSizes?.default ) { keysToProcess.push( 'default' ); } keysToProcess.forEach( ( key ) => { normalizedFontSizes[ key ] = fontSizes[ key ].map( ( fontSizeObject ) => { fontSizeObject.sizePx = getPxFromCssUnit( fontSizeObject.size, { width: dimensions.width, height: dimensions.height, fontSize: DEFAULT_FONT_SIZE, } ); return fontSizeObject; } ); } ); return normalizedFontSizes; } export function useMobileGlobalStylesColors( type = 'colors' ) { const colorGradientSettings = useMultipleOriginColorsAndGradients(); const availableThemeColors = colorGradientSettings?.[ type ]?.reduce( ( colors, origin ) => colors.concat( origin?.[ type ] ), [] ); // Default editor colors/gradients if it's not a block-based theme. const defaultPaletteSetting = type === 'colors' ? 'color.palette' : 'color.gradients'; const [ defaultPaletteValue ] = useSettings( defaultPaletteSetting ); // In edge cases, the default palette might be undefined. To avoid // exceptions across the editor in that case, we explicitly return // the default editor colors. const defaultPalette = defaultPaletteValue ?? SETTINGS_DEFAULTS.colors; return availableThemeColors.length >= 1 ? availableThemeColors : defaultPalette; } export function getColorsAndGradients( defaultEditorColors = [], defaultEditorGradients = [], rawFeatures ) { const features = rawFeatures ? JSON.parse( rawFeatures ) : {}; return { __experimentalGlobalStylesBaseStyles: null, __experimentalFeatures: { // Set an empty object to avoid errors from shared web components relying // upon block settings. E.g., the Gallery block. blocks: {}, color: { ...( ! features?.color ? { text: true, background: true, palette: { default: defaultEditorColors, }, gradients: { default: defaultEditorGradients, }, } : features?.color ), defaultPalette: defaultEditorColors?.length > 0, defaultGradients: defaultEditorGradients?.length > 0, }, }, }; } export function getGlobalStyles( rawStyles, rawFeatures ) { const features = rawFeatures ? JSON.parse( rawFeatures ) : {}; const mappedValues = getMappedValues( features, features?.color?.palette ); const colors = parseStylesVariables( JSON.stringify( features?.color ), mappedValues ); const gradients = parseStylesVariables( JSON.stringify( features?.color?.gradients ), mappedValues ); const customValues = parseStylesVariables( JSON.stringify( features?.custom ), mappedValues ); const globalStyles = parseStylesVariables( rawStyles, mappedValues, customValues ); const fontSizes = normalizeFontSizes( features?.typography?.fontSizes ); return { __experimentalFeatures: { // Set an empty object to avoid errors from shared web components relying // upon block settings. E.g., the Gallery block. blocks: {}, color: { palette: colors?.palette, gradients, text: features?.color?.text ?? true, background: features?.color?.background ?? true, defaultPalette: features?.color?.defaultPalette ?? true, defaultGradients: features?.color?.defaultGradients ?? true, }, typography: { fontSizes, customLineHeight: features?.custom?.[ 'line-height' ], }, spacing: features?.spacing, }, __experimentalGlobalStylesBaseStyles: globalStyles, }; } export const getMergedGlobalStyles = ( baseGlobalStyles, globalStyle, wrapperPropsStyle, blockAttributes, defaultColors, blockName, fontSizes ) => { // Current support for general styles and blocks. const baseGlobalColors = { baseColors: { color: baseGlobalStyles?.color, typography: baseGlobalStyles?.typography, elements: { link: baseGlobalStyles?.elements?.link, }, blocks: { 'core/button': baseGlobalStyles?.blocks?.[ 'core/button' ], }, }, }; const blockStyleAttributes = Object.fromEntries( Object.entries( blockAttributes ?? {} ).filter( ( [ key ] ) => BLOCK_STYLE_ATTRIBUTES.includes( key ) ) ); // This prevents certain wrapper styles from being applied to blocks that // don't support them yet. const wrapperPropsStyleFiltered = Object.fromEntries( Object.entries( wrapperPropsStyle ?? {} ).filter( ( [ key ] ) => BLOCK_STYLE_ATTRIBUTES.includes( key ) ) ); const mergedStyle = { ...baseGlobalColors, ...globalStyle, ...wrapperPropsStyleFiltered, }; const blockColors = getBlockColors( blockStyleAttributes, defaultColors, blockName, baseGlobalStyles ); const blockPaddings = getBlockPaddings( mergedStyle, wrapperPropsStyle, blockStyleAttributes, blockColors ); const blockTypography = getBlockTypography( blockStyleAttributes, fontSizes, blockName, baseGlobalStyles ); return { ...mergedStyle, ...blockPaddings, ...blockColors, ...blockTypography, }; }; export const useGlobalStyles = () => { const globalStyles = useContext( GlobalStylesContext ); return globalStyles; }; /** * Determine and apply appropriate color scheme based on global styles or device's light/dark mode. * * The function first attempts to retrieve the editor's background color from global styles. * If the detected background color is light, light styles are applied, and dark styles otherwise. * If no custom background color is defined, styles are applied using the device's dark/light setting. * * @param {Object} baseStyle - An object representing the base (light theme) styles for the editor. * @param {Object} darkStyle - An object representing the additional styles to apply when the editor is in dark mode. * * @return {Object} - The combined style object that should be applied to the editor. */ export const useEditorColorScheme = ( baseStyle, darkStyle ) => { const globalStyles = useGlobalStyles(); const deviceColorScheme = usePreferredColorSchemeStyle( baseStyle, darkStyle ); const editorColors = globalStyles?.baseColors?.color; const editorBackgroundColor = editorColors?.background; const isBackgroundColorDefined = typeof editorBackgroundColor !== 'undefined' && editorBackgroundColor !== 'undefined'; if ( isBackgroundColorDefined ) { const isEditorBackgroundDark = colord( editorBackgroundColor ).isDark(); return isEditorBackgroundDark ? { ...baseStyle, ...darkStyle } : baseStyle; } return deviceColorScheme; };