From 1b03c7a75d575e1ae677041b9d45f18aec811a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Fri, 30 Apr 2021 11:06:11 +0200 Subject: [PATCH] Client: adapt to new shape --- .../components/use-editor-feature/index.js | 42 +-- packages/block-library/src/common.scss | 3 - packages/blocks/src/api/constants.js | 1 + .../editor/global-styles-provider.js | 182 +++++----- .../editor/global-styles-renderer.js | 282 +++++++++++---- .../editor/test/global-styles-renderer.js | 322 ++++++++++++++++++ .../edit-site/src/components/editor/utils.js | 79 +++-- .../components/sidebar/color-palette-panel.js | 13 +- .../sidebar/global-styles-sidebar.js | 54 +-- 9 files changed, 718 insertions(+), 260 deletions(-) create mode 100644 packages/edit-site/src/components/editor/test/global-styles-renderer.js diff --git a/packages/block-editor/src/components/use-editor-feature/index.js b/packages/block-editor/src/components/use-editor-feature/index.js index 81ab094ec7f6c5..1065859edc910b 100644 --- a/packages/block-editor/src/components/use-editor-feature/index.js +++ b/packages/block-editor/src/components/use-editor-feature/index.js @@ -1,12 +1,11 @@ /** * External dependencies */ -import { get, isObject } from 'lodash'; +import { get } from 'lodash'; /** * WordPress dependencies */ -import { store as blocksStore } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; /** @@ -50,15 +49,6 @@ const deprecatedFlags = { 'spacing.customPadding': ( settings ) => settings.enableCustomSpacing, }; -function blockAttributesMatch( blockAttributes, attributes ) { - for ( const attribute in attributes ) { - if ( attributes[ attribute ] !== blockAttributes[ attribute ] ) { - return false; - } - } - return true; -} - /** * Hook that retrieves the setting for the given editor feature. * It works with nested objects using by finding the value at path. @@ -73,36 +63,16 @@ function blockAttributesMatch( blockAttributes, attributes ) { * ``` */ export default function useEditorFeature( featurePath ) { - const { name: blockName, clientId } = useBlockEditContext(); + const { name: blockName } = useBlockEditContext(); const setting = useSelect( ( select ) => { - const { getBlockAttributes, getSettings } = select( - blockEditorStore - ); - const settings = getSettings(); - const blockType = select( blocksStore ).getBlockType( blockName ); - - let context = blockName; - const selectors = get( blockType, [ - 'supports', - '__experimentalSelector', - ] ); - if ( clientId && isObject( selectors ) ) { - const blockAttributes = getBlockAttributes( clientId ) || {}; - for ( const contextSelector in selectors ) { - const { attributes } = selectors[ contextSelector ]; - if ( blockAttributesMatch( blockAttributes, attributes ) ) { - context = contextSelector; - break; - } - } - } + const settings = select( blockEditorStore ).getSettings(); // 1 - Use __experimental features, if available. // We cascade to the all value if the block one is not available. - const defaultsPath = `__experimentalFeatures.defaults.${ featurePath }`; - const blockPath = `__experimentalFeatures.${ context }.${ featurePath }`; + const defaultsPath = `__experimentalFeatures.${ featurePath }`; + const blockPath = `__experimentalFeatures.blocks.${ blockName }.${ featurePath }`; const experimentalFeaturesResult = get( settings, blockPath ) ?? get( settings, defaultsPath ); if ( experimentalFeaturesResult !== undefined ) { @@ -123,7 +93,7 @@ export default function useEditorFeature( featurePath ) { // To remove when __experimentalFeatures are ported to core. return featurePath === 'typography.dropCap' ? true : undefined; }, - [ blockName, clientId, featurePath ] + [ blockName, featurePath ] ); return setting; diff --git a/packages/block-library/src/common.scss b/packages/block-library/src/common.scss index 4cd25f750d5223..6cf9854943376d 100644 --- a/packages/block-library/src/common.scss +++ b/packages/block-library/src/common.scss @@ -12,9 +12,6 @@ // Gradients @include gradient-colors(); - .has-link-color a:not(.wp-block-button__link) { - color: var(--wp--style--color--link, #00e); - } } // Font sizes. diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 4215022da66594..e276484dca72d9 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -14,6 +14,7 @@ export const DEPRECATED_ENTRY_KEYS = [ export const __EXPERIMENTAL_STYLE_PROPERTY = { '--wp--style--color--link': { + valueGlobal: [ 'elements', 'link', 'color', 'text' ], value: [ 'color', 'link' ], support: [ 'color', 'link' ], }, diff --git a/packages/edit-site/src/components/editor/global-styles-provider.js b/packages/edit-site/src/components/editor/global-styles-provider.js index 443f8a3da8490b..8da9dfc82fd279 100644 --- a/packages/edit-site/src/components/editor/global-styles-provider.js +++ b/packages/edit-site/src/components/editor/global-styles-provider.js @@ -24,18 +24,17 @@ import { useSelect, useDispatch } from '@wordpress/data'; * Internal dependencies */ import { - ALL_BLOCKS_NAME, - ALL_BLOCKS_SELECTOR, + ELEMENTS, ROOT_BLOCK_NAME, ROOT_BLOCK_SELECTOR, ROOT_BLOCK_SUPPORTS, getValueFromVariable, getPresetVariable, } from './utils'; -import getGlobalStyles from './global-styles-renderer'; +import { toCustomProperties, toStyles } from './global-styles-renderer'; import { store as editSiteStore } from '../../store'; -const EMPTY_CONTENT = { isGlobalStylesUserThemeJSON: true }; +const EMPTY_CONTENT = { isGlobalStylesUserThemeJSON: true, version: 1 }; const EMPTY_CONTENT_STRING = JSON.stringify( EMPTY_CONTENT ); const GlobalStylesContext = createContext( { @@ -82,47 +81,36 @@ const extractSupportKeys = ( supports ) => { return supportKeys; }; -const getContexts = ( blockTypes ) => { - const result = { - [ ROOT_BLOCK_NAME ]: { - selector: ROOT_BLOCK_SELECTOR, - supports: ROOT_BLOCK_SUPPORTS, - }, - [ ALL_BLOCKS_NAME ]: { - selector: ALL_BLOCKS_SELECTOR, - supports: [], // by being an empty array, the styles subtree will be ignored - }, - }; - - // Add contexts from block metadata. +const getBlockMetadata = ( blockTypes ) => { + const result = {}; + blockTypes.forEach( ( blockType ) => { - const blockName = blockType.name; - const blockSelector = blockType?.supports?.__experimentalSelector; + const name = blockType.name; const supports = extractSupportKeys( blockType?.supports ); const hasSupport = supports.length > 0; - if ( hasSupport && typeof blockSelector === 'string' ) { - result[ blockName ] = { - selector: blockSelector, - supports, - blockName, - }; - } else if ( hasSupport && typeof blockSelector === 'object' ) { - Object.keys( blockSelector ).forEach( ( key ) => { - result[ key ] = { - selector: blockSelector[ key ].selector, - supports, - blockName, - title: blockSelector[ key ].title, - attributes: blockSelector[ key ].attributes, - }; + if ( hasSupport ) { + const selector = + blockType?.supports?.__experimentalSelector ?? + '.wp-block-' + name.replace( 'core/', '' ).replace( '/', '-' ); + + const blockSelectors = selector.split( ',' ); + const elements = []; + Object.keys( ELEMENTS ).forEach( ( key ) => { + const elementSelector = []; + blockSelectors.forEach( ( blockSelector ) => { + elementSelector.push( + blockSelector + ' ' + ELEMENTS[ key ] + ); + } ); + elements[ key ] = elementSelector.join( ',' ); } ); - } else if ( hasSupport ) { - const suffix = blockName.replace( 'core/', '' ).replace( '/', '-' ); - result[ blockName ] = { - selector: '.wp-block-' + suffix, + + result[ name ] = { + name, + selector, supports, - blockName, + elements, }; } } ); @@ -140,13 +128,21 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { } ); const { updateSettings } = useDispatch( editSiteStore ); - const contexts = useMemo( () => getContexts( blockTypes ), [ blockTypes ] ); + const blocks = useMemo( () => getBlockMetadata( blockTypes ), [ + blockTypes, + ] ); const { __experimentalGlobalStylesBaseStyles: themeStyles } = settings; const { userStyles, mergedStyles } = useMemo( () => { let newUserStyles; try { newUserStyles = content ? JSON.parse( content ) : EMPTY_CONTENT; + + // At the moment, we ignore previous user config that + // is in a different version than the theme config. + if ( newUserStyles?.version !== baseStyles?.version ) { + newUserStyles = EMPTY_CONTENT; + } } catch ( e ) { /* eslint-disable no-console */ console.error( 'User data is not JSON' ); @@ -160,9 +156,9 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { if ( ! newUserStyles.isGlobalStylesUserThemeJSON ) { newUserStyles = EMPTY_CONTENT; } - // TODO: we probably want to check here that the shape is what we want - // This is, settings & styles are top-level keys, or perhaps a version. - // As to avoid merging trees that are different. + + // At this point, the version schema of the theme & user + // is the same, so we can merge them. const newMergedStyles = mergeWith( {}, baseStyles, @@ -178,33 +174,52 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { const nextValue = useMemo( () => ( { - contexts, - getSetting: ( context, path ) => - get( userStyles?.settings?.[ context ], path ), - setSetting: ( context, path, newValue ) => { + root: { + name: ROOT_BLOCK_NAME, + selector: ROOT_BLOCK_SELECTOR, + supports: ROOT_BLOCK_SUPPORTS, + elements: ELEMENTS, + }, + blocks, + getSetting: ( context, propertyPath ) => { + const path = + context === ROOT_BLOCK_NAME + ? propertyPath + : [ 'blocks', context, ...propertyPath ]; + get( userStyles?.settings, path ); + }, + setSetting: ( context, propertyPath, newValue ) => { const newContent = { ...userStyles }; - let contextSettings = newContent?.settings?.[ context ]; - if ( ! contextSettings ) { - contextSettings = {}; - set( newContent, [ 'settings', context ], contextSettings ); + const path = + context === ROOT_BLOCK_NAME + ? [ 'settings' ] + : [ 'settings', 'blocks', context ]; + + let newSettings = get( newContent, path ); + if ( ! newSettings ) { + newSettings = {}; + set( newContent, path, newSettings ); } - set( contextSettings, path, newValue ); + set( newSettings, propertyPath, newValue ); + setContent( JSON.stringify( newContent ) ); }, getStyle: ( context, propertyName, origin = 'merged' ) => { + const propertyPath = + STYLE_PROPERTY[ propertyName ].valueGlobal ?? + STYLE_PROPERTY[ propertyName ].value; + const path = + context === ROOT_BLOCK_NAME + ? propertyPath + : [ 'blocks', context, ...propertyPath ]; + if ( origin === 'theme' ) { - const value = get( - themeStyles?.styles?.[ context ], - STYLE_PROPERTY[ propertyName ].value - ); + const value = get( themeStyles?.styles, path ); return getValueFromVariable( themeStyles, context, value ); } if ( origin === 'user' ) { - const value = get( - userStyles?.styles?.[ context ], - STYLE_PROPERTY[ propertyName ].value - ); + const value = get( userStyles?.styles, path ); // We still need to use merged styles here because the // presets used to resolve user variable may be defined a @@ -212,22 +227,28 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { return getValueFromVariable( mergedStyles, context, value ); } - const value = get( - mergedStyles?.styles?.[ context ], - STYLE_PROPERTY[ propertyName ].value - ); + const value = get( mergedStyles?.styles, path ); return getValueFromVariable( mergedStyles, context, value ); }, setStyle: ( context, propertyName, newValue ) => { const newContent = { ...userStyles }; - let contextStyles = newContent?.styles?.[ context ]; - if ( ! contextStyles ) { - contextStyles = {}; - set( newContent, [ 'styles', context ], contextStyles ); + + const path = + ROOT_BLOCK_NAME === context + ? [ 'styles' ] + : [ 'styles', 'blocks', context ]; + const propertyPath = + STYLE_PROPERTY[ propertyName ].valueGlobal ?? + STYLE_PROPERTY[ propertyName ].value; + + let newStyles = get( newContent, path ); + if ( ! newStyles ) { + newStyles = {}; + set( newContent, path, newStyles ); } set( - contextStyles, - STYLE_PROPERTY[ propertyName ].value, + newStyles, + propertyPath, getPresetVariable( mergedStyles, context, @@ -235,6 +256,7 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { newValue ) ); + setContent( JSON.stringify( newContent ) ); }, } ), @@ -242,34 +264,28 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { ); useEffect( () => { - const newStyles = settings.styles.filter( + const nonGlobalStyles = settings.styles.filter( ( style ) => ! style.isGlobalStyles ); + const customProperties = toCustomProperties( mergedStyles, blocks ); + const globalStyles = toStyles( mergedStyles, blocks ); updateSettings( { ...settings, styles: [ - ...newStyles, + ...nonGlobalStyles, { - css: getGlobalStyles( - contexts, - mergedStyles, - 'cssVariables' - ), + css: customProperties, isGlobalStyles: true, __experimentalNoWrapper: true, }, { - css: getGlobalStyles( - contexts, - mergedStyles, - 'blockStyles' - ), + css: globalStyles, isGlobalStyles: true, }, ], __experimentalFeatures: mergedStyles.settings, } ); - }, [ contexts, mergedStyles ] ); + }, [ blocks, mergedStyles ] ); return ( diff --git a/packages/edit-site/src/components/editor/global-styles-renderer.js b/packages/edit-site/src/components/editor/global-styles-renderer.js index 2b2236e58e3696..054acf8468d6fa 100644 --- a/packages/edit-site/src/components/editor/global-styles-renderer.js +++ b/packages/edit-site/src/components/editor/global-styles-renderer.js @@ -1,7 +1,17 @@ /** * External dependencies */ -import { capitalize, get, kebabCase, reduce, startsWith } from 'lodash'; +import { + capitalize, + forEach, + get, + isEmpty, + kebabCase, + pickBy, + reduce, + set, + startsWith, +} from 'lodash'; /** * WordPress dependencies @@ -11,7 +21,7 @@ import { __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY } from '@wordpress/bloc /** * Internal dependencies */ -import { LINK_COLOR_DECLARATION, PRESET_METADATA } from './utils'; +import { PRESET_METADATA, ROOT_BLOCK_SELECTOR, ELEMENTS } from './utils'; function compileStyleValue( uncompiledValue ) { const VARIABLE_REFERENCE_PREFIX = 'var:'; @@ -34,7 +44,7 @@ function compileStyleValue( uncompiledValue ) { * * @return {Array} An array of style declarations. */ -function getBlockPresetsDeclarations( blockPresets = {} ) { +function getPresetsDeclarations( blockPresets = {} ) { return reduce( PRESET_METADATA, ( declarations, { path, valueKey, cssVarInfix } ) => { @@ -57,7 +67,7 @@ function getBlockPresetsDeclarations( blockPresets = {} ) { * @param {Object} blockPresets * @return {string} CSS declarations for the preset classes. */ -function getBlockPresetClasses( blockSelector, blockPresets = {} ) { +function getPresetsClasses( blockSelector, blockPresets = {} ) { return reduce( PRESET_METADATA, ( declarations, { path, valueKey, classes } ) => { @@ -103,13 +113,16 @@ function flattenTree( input = {}, prefix, token ) { * * @return {Array} An array of style declarations. */ -function getBlockStylesDeclarations( blockStyles = {} ) { +function getStylesDeclarations( blockStyles = {} ) { return reduce( STYLE_PROPERTY, - ( declarations, { value, properties }, key ) => { + ( declarations, { value, valueGlobal, properties }, key ) => { + const pathToValue = valueGlobal ?? value; if ( !! properties ) { properties.forEach( ( prop ) => { - if ( ! get( blockStyles, [ ...value, prop ], false ) ) { + if ( + ! get( blockStyles, [ ...pathToValue, prop ], false ) + ) { // Do not create a declaration // for sub-properties that don't have any value. return; @@ -119,17 +132,17 @@ function getBlockStylesDeclarations( blockStyles = {} ) { : kebabCase( `${ key }${ capitalize( prop ) }` ); declarations.push( `${ cssProperty }: ${ compileStyleValue( - get( blockStyles, [ ...value, prop ] ) + get( blockStyles, [ ...pathToValue, prop ] ) ) }` ); } ); - } else if ( get( blockStyles, value, false ) ) { + } else if ( get( blockStyles, pathToValue, false ) ) { const cssProperty = key.startsWith( '--' ) ? key : kebabCase( key ); declarations.push( `${ cssProperty }: ${ compileStyleValue( - get( blockStyles, value ) + get( blockStyles, pathToValue ) ) }` ); } @@ -140,57 +153,208 @@ function getBlockStylesDeclarations( blockStyles = {} ) { ); } -export default ( blockData, tree, type = 'all' ) => { - return reduce( - blockData, - ( styles, { selector }, context ) => { - if ( type === 'all' || type === 'cssVariables' ) { - const variableDeclarations = [ - ...getBlockPresetsDeclarations( - tree?.settings?.[ context ] - ), - ...flattenTree( - tree?.settings?.[ context ]?.custom, - '--wp--custom--', - '--' - ), - ]; - - if ( variableDeclarations.length > 0 ) { - styles.push( - `${ selector } { ${ variableDeclarations.join( - ';' - ) } }` - ); - } +export const getNodesWithStyles = ( tree, blockSelectors ) => { + const nodes = []; + + if ( ! tree?.styles ) { + return nodes; + } + + const pickStyleKeys = ( treeToPickFrom ) => + pickBy( treeToPickFrom, ( value, key ) => + [ 'border', 'color', 'spacing', 'typography' ].includes( key ) + ); + + // Top-level. + const styles = pickStyleKeys( tree.styles ); + if ( !! styles ) { + nodes.push( { + styles, + selector: ROOT_BLOCK_SELECTOR, + } ); + } + forEach( tree.styles?.elements, ( value, index ) => { + nodes.push( { + styles: value, + selector: ELEMENTS[ index ], + } ); + } ); + + // Iterate over blocks: they can have styles & elements. + forEach( tree.styles?.blocks, ( node, blockName ) => { + const blockStyles = pickStyleKeys( node ); + if ( !! blockStyles ) { + nodes.push( { + styles: blockStyles, + selector: blockSelectors[ blockName ].selector, + } ); + } + + forEach( node?.elements, ( value, elementName ) => { + nodes.push( { + styles: value, + selector: blockSelectors[ blockName ].elements[ elementName ], + } ); + } ); + } ); + + return nodes; +}; + +export const getNodesWithSettings = ( tree, blockSelectors ) => { + const nodes = []; + + if ( ! tree?.settings ) { + return nodes; + } + + const pickPresets = ( treeToPickFrom ) => { + const presets = {}; + PRESET_METADATA.forEach( ( { path } ) => { + const value = get( treeToPickFrom, path, false ); + if ( value !== false ) { + set( presets, path, value ); } - if ( type === 'all' || type === 'blockStyles' ) { - const blockStyleDeclarations = getBlockStylesDeclarations( - tree?.styles?.[ context ] - ); + } ); + return presets; + }; - if ( blockStyleDeclarations.length > 0 ) { - styles.push( - `${ selector } { ${ blockStyleDeclarations.join( - ';' - ) } }` - ); - } + // Top-level. + const presets = pickPresets( tree.settings ); + if ( ! isEmpty( presets ) ) { + nodes.push( { + presets, + custom: tree.settings?.custom, + selector: ROOT_BLOCK_SELECTOR, + } ); + } - const presetClasses = getBlockPresetClasses( - selector, - tree?.settings?.[ context ] - ); - if ( presetClasses ) { - styles.push( presetClasses ); - } + // Blocks. + forEach( tree.settings?.blocks, ( node, blockName ) => { + const blockPresets = pickPresets( node ); + if ( ! isEmpty( blockPresets ) ) { + nodes.push( { + presets: blockPresets, + custom: node.custom, + selector: blockSelectors[ blockName ].selector, + } ); + } + } ); + + return nodes; +}; + +export const toCustomProperties = ( tree, blockSelectors ) => { + const settings = getNodesWithSettings( tree, blockSelectors ); + + let ruleset = ''; + settings.forEach( ( { presets, custom, selector } ) => { + const declarations = getPresetsDeclarations( presets ); + const customProps = flattenTree( custom, '--wp--custom--', '--' ); + if ( customProps.length > 0 ) { + declarations.push( ...customProps ); + } + + if ( declarations.length > 0 ) { + ruleset = ruleset + `${ selector }{${ declarations.join( ';' ) };}`; + } + } ); + + return ruleset; +}; + +const containsLinkElement = ( selector ) => + selector.toLowerCase().includes( ELEMENTS.link ); +const withoutLinkSelector = ( selector ) => { + const newSelector = selector + .split( ',' ) + .map( ( individualSelector ) => + individualSelector.replace( ELEMENTS.link, '' ).trim() + ) + .join( ',' ); + + if ( '' === newSelector ) { + return ROOT_BLOCK_SELECTOR; + } + + return newSelector; +}; + +export const toStyles = ( tree, blockSelectors ) => { + const nodesWithStyles = getNodesWithStyles( tree, blockSelectors ); + const nodesWithSettings = getNodesWithSettings( tree, blockSelectors ); + + let ruleset = `${ ELEMENTS.link }{color: var(--wp--style--color--link);}`; + nodesWithStyles.forEach( ( { selector, styles } ) => { + const declarations = getStylesDeclarations( styles ); + + if ( declarations.length === 0 ) { + return; + } + + if ( ! containsLinkElement( selector ) ) { + ruleset = ruleset + `${ selector }{${ declarations.join( ';' ) };}`; + } else { + // To be removed when the user provided styles for link color + // no longer use the --wp--style--link-color variable. + // + // We need to: + // + // 1. For the color property, output: + // + // $selector_without_the_link_element_selector { + // --wp--style--color--link: value + // } + // + // 2. For the rest of the properties: + // + // $selector { + // other-prop: value; + // other-prop: value; + // } + // + // The reason for 1 is that user styles are attached to the block wrapper. + // If 1 targets the a element is going to have higher specificity + // and will overwrite the user preferences. + // + // Once the user styles are updated to output an `a` element instead + // this can be removed. + + const declarationsColor = declarations.filter( + ( declaration ) => declaration.split( ':' )[ 0 ] === 'color' + ); + const declarationsOther = declarations.filter( + ( declaration ) => declaration.split( ':' )[ 0 ] !== 'color' + ); + + if ( declarationsOther.length > 0 ) { + ruleset = + ruleset + + `${ selector }{${ declarationsOther.join( ';' ) };}`; } - return styles; - }, - // Can this be converted to a context, as the global context? - // See comment in the server. - type === 'all' || type === 'blockStyles' - ? [ LINK_COLOR_DECLARATION ] - : [] - ).join( '' ); + + if ( declarationsColor.length === 1 ) { + const value = declarationsColor[ 0 ].split( ':' )[ 1 ]; + ruleset = + ruleset + + `${ withoutLinkSelector( + selector + ) }{--wp--style--color--link:${ value };}`; + } + } + } ); + + nodesWithSettings.forEach( ( { selector, presets } ) => { + if ( ROOT_BLOCK_SELECTOR === selector ) { + // Do not add extra specificity for top-level classes. + selector = ''; + } + + const classes = getPresetsClasses( selector, presets ); + if ( ! isEmpty( classes ) ) { + ruleset = ruleset + classes; + } + } ); + + return ruleset; }; diff --git a/packages/edit-site/src/components/editor/test/global-styles-renderer.js b/packages/edit-site/src/components/editor/test/global-styles-renderer.js new file mode 100644 index 00000000000000..d6a8d2da02a48f --- /dev/null +++ b/packages/edit-site/src/components/editor/test/global-styles-renderer.js @@ -0,0 +1,322 @@ +/** + * Internal dependencies + */ +import { + getNodesWithSettings, + getNodesWithStyles, + toCustomProperties, + toStyles, +} from '../global-styles-renderer'; +import { ELEMENTS, ROOT_BLOCK_SELECTOR } from '../utils'; + +describe( 'global styles renderer', () => { + describe( 'getNodesWithStyles', () => { + it( 'should return the nodes with styles', () => { + const tree = { + styles: { + color: { + background: 'red', + text: 'red', + }, + blocks: { + 'core/heading': { + color: { + background: 'blue', + text: 'blue', + }, + elements: { + h1: { + typography: { + fontSize: '42px', + }, + }, + h2: { + typography: { + fontSize: '23px', + }, + }, + }, + }, + }, + elements: { + link: { + color: { + background: 'yellow', + text: 'yellow', + }, + }, + }, + }, + }; + const blockSelectors = { + 'core/heading': { + selector: 'h1,h2,h3,h4,h5,h6', + elements: { + link: 'h1 a,h2 a,h3 a,h4 a,h5 a,h6 a', + h1: 'h1', + h2: 'h2', + h3: 'h3', + h4: 'h4', + h5: 'h5', + h6: 'h6', + }, + }, + }; + expect( getNodesWithStyles( tree, blockSelectors ) ).toEqual( [ + { + styles: { + color: { + background: 'red', + text: 'red', + }, + }, + selector: ROOT_BLOCK_SELECTOR, + }, + { + styles: { + color: { + background: 'yellow', + text: 'yellow', + }, + }, + selector: ELEMENTS.link, + }, + { + styles: { + color: { + background: 'blue', + text: 'blue', + }, + }, + selector: 'h1,h2,h3,h4,h5,h6', + }, + { + styles: { + typography: { + fontSize: '42px', + }, + }, + selector: 'h1', + }, + { + styles: { + typography: { + fontSize: '23px', + }, + }, + selector: 'h2', + }, + ] ); + } ); + } ); + describe( 'getNodesWithSettings', () => { + it( 'should return nodes with settings', () => { + const tree = { + styles: { + color: { + background: 'red', + text: 'red', + }, + }, + settings: { + color: { + palette: [ + { name: 'White', slug: 'white', color: 'white' }, + { name: 'Black', slug: 'black', color: 'black' }, + ], + }, + blocks: { + 'core/paragraph': { + typography: { + fontSizes: [ + { + name: 'small', + slug: 'small', + size: '12px', + }, + { + name: 'medium', + slug: 'medium', + size: '23px', + }, + ], + }, + }, + }, + }, + }; + + const blockSelectors = { + 'core/paragraph': { + selector: 'p', + elements: { + link: 'p a', + h1: 'p h1', + h2: 'p h2', + h3: 'p h3', + h4: 'p h4', + h5: 'p h5', + h6: 'p h6', + }, + }, + }; + + expect( getNodesWithSettings( tree, blockSelectors ) ).toEqual( [ + { + presets: { + color: { + palette: [ + { + name: 'White', + slug: 'white', + color: 'white', + }, + { + name: 'Black', + slug: 'black', + color: 'black', + }, + ], + }, + }, + selector: ROOT_BLOCK_SELECTOR, + }, + { + presets: { + typography: { + fontSizes: [ + { + name: 'small', + slug: 'small', + size: '12px', + }, + { + name: 'medium', + slug: 'medium', + size: '23px', + }, + ], + }, + }, + selector: 'p', + }, + ] ); + } ); + } ); + + describe( 'toCustomProperties', () => { + it( 'should return a ruleset', () => { + const tree = { + settings: { + color: { + palette: [ + { name: 'White', slug: 'white', color: 'white' }, + { name: 'Black', slug: 'black', color: 'black' }, + ], + }, + custom: { + 'font-primary': 'value', + 'line-height': { + body: 1.7, + heading: 1.3, + }, + }, + blocks: { + 'core/heading': { + typography: { + fontSizes: [ + { + name: 'small', + slug: 'small', + size: '12px', + }, + { + name: 'medium', + slug: 'medium', + size: '23px', + }, + ], + }, + }, + }, + }, + }; + + const blockSelectors = { + 'core/heading': { + selector: 'h1,h2,h3,h4,h5,h6', + }, + }; + + expect( toCustomProperties( tree, blockSelectors ) ).toEqual( + 'body{--wp--preset--color--white: white;--wp--preset--color--black: black;--wp--custom--font-primary: value;--wp--custom--line-height--body: 1.7;--wp--custom--line-height--heading: 1.3;}h1,h2,h3,h4,h5,h6{--wp--preset--font-size--small: 12px;--wp--preset--font-size--medium: 23px;}' + ); + } ); + } ); + + describe( 'toStyles', () => { + it( 'should return a ruleset', () => { + const tree = { + settings: { + color: { + palette: [ + { name: 'White', slug: 'white', color: 'white' }, + { name: 'Black', slug: 'black', color: 'black' }, + ], + }, + }, + styles: { + color: { + background: 'red', + }, + elements: { + h1: { + typography: { + fontSize: '42px', + }, + }, + }, + blocks: { + 'core/heading': { + color: { + text: 'orange', + }, + elements: { + link: { + color: { + text: 'hotpink', + }, + }, + }, + }, + }, + }, + }; + + const blockSelectors = { + 'core/heading': { + selector: 'h1,h2,h3,h4,h5,h6', + elements: { + link: + 'h1 ' + + ELEMENTS.link + + ',h2 ' + + ELEMENTS.link + + ',h3 ' + + ELEMENTS.link + + ',h4 ' + + ELEMENTS.link + + ',h5 ' + + ELEMENTS.link + + ',h6 ' + + ELEMENTS.link, + }, + }, + }; + + expect( toStyles( tree, blockSelectors ) ).toEqual( + 'a:not(.wp-block-button_link){color: var(--wp--style--color--link);}body{background-color: red;}h1{font-size: 42px;}h1,h2,h3,h4,h5,h6{color: orange;}h1,h2,h3,h4,h5,h6{--wp--style--color--link: hotpink;}.has-white-color{color: white !important;}.has-white-background-color{background-color: white !important;}.has-white-border-color{border-color: white !important;}.has-black-color{color: black !important;}.has-black-background-color{background-color: black !important;}.has-black-border-color{border-color: black !important;}' + ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/components/editor/utils.js b/packages/edit-site/src/components/editor/utils.js index b695c102ef1c60..54aa19320397bb 100644 --- a/packages/edit-site/src/components/editor/utils.js +++ b/packages/edit-site/src/components/editor/utils.js @@ -12,8 +12,6 @@ import { useSelect } from '@wordpress/data'; import { store as editSiteStore } from '../../store'; /* Supporting data */ -export const ALL_BLOCKS_NAME = 'defaults'; -export const ALL_BLOCKS_SELECTOR = 'body'; export const ROOT_BLOCK_NAME = 'root'; export const ROOT_BLOCK_SELECTOR = 'body'; export const ROOT_BLOCK_SUPPORTS = [ @@ -29,6 +27,15 @@ export const ROOT_BLOCK_SUPPORTS = [ 'textDecoration', 'textTransform', ]; +export const ELEMENTS = { + link: 'a:not(.wp-block-button_link)', + h1: 'h1', + h2: 'h2', + h3: 'h3', + h4: 'h4', + h5: 'h5', + h6: 'h6', +}; export const PRESET_METADATA = [ { @@ -96,40 +103,43 @@ function getPresetMetadataFromStyleProperty( styleProperty ) { export const LINK_COLOR = '--wp--style--color--link'; export const LINK_COLOR_DECLARATION = `a { color: var(${ LINK_COLOR }, #00e); }`; -export function useEditorFeature( featurePath, blockName = ALL_BLOCKS_NAME ) { +export function useEditorFeature( featurePath, blockName = '' ) { const settings = useSelect( ( select ) => { return select( editSiteStore ).getSettings(); } ); - return ( - get( - settings, - `__experimentalFeatures.${ blockName }.${ featurePath }` - ) ?? - get( - settings, - `__experimentalFeatures.${ ALL_BLOCKS_NAME }.${ featurePath }` - ) - ); + const topLevelPath = `__experimentalFeatures.${ featurePath }`; + const blockPath = `__experimentalFeatures.blocks.${ blockName }.${ featurePath }`; + return get( settings, blockPath ) ?? get( settings, topLevelPath ); } -export function getPresetVariable( styles, blockName, propertyName, value ) { +export function getPresetVariable( styles, context, propertyName, value ) { if ( ! value ) { return value; } - const presetData = getPresetMetadataFromStyleProperty( propertyName ); - if ( ! presetData ) { + + const metadata = getPresetMetadataFromStyleProperty( propertyName ); + if ( ! metadata ) { + // The property doesn't have preset data + // so the value should be returned as it is. return value; } - const { valueKey, path, cssVarInfix } = presetData; - const presets = - get( styles, [ 'settings', blockName, ...path ] ) ?? - get( styles, [ 'settings', ALL_BLOCKS_NAME, ...path ] ); - const presetObject = find( presets, ( preset ) => { - return preset[ valueKey ] === value; - } ); + + const basePath = + ROOT_BLOCK_NAME === context + ? [ 'settings' ] + : [ 'settings', 'blocks', context ]; + const { valueKey, path: propertyPath, cssVarInfix } = metadata; + const presets = get( styles, [ ...basePath, ...propertyPath ] ); + const presetObject = find( + presets, + ( preset ) => preset[ valueKey ] === value + ); if ( ! presetObject ) { + // Value wasn't found in the presets, + // so it must be a custom value. return value; } + return `var:preset|${ cssVarInfix }|${ presetObject.slug }`; } @@ -140,31 +150,32 @@ function getValueFromPresetVariable( [ presetType, slug ] ) { presetType = camelCase( presetType ); - const presetData = getPresetMetadataFromStyleProperty( presetType ); - if ( ! presetData ) { + const metadata = getPresetMetadataFromStyleProperty( presetType ); + if ( ! metadata ) { return variable; } + const presets = - get( styles, [ 'settings', blockName, ...presetData.path ] ) ?? - get( styles, [ 'settings', ALL_BLOCKS_NAME, ...presetData.path ] ); + get( styles, [ 'settings', 'blocks', blockName, ...metadata.path ] ) ?? + get( styles, [ 'settings', ...metadata.path ] ); if ( ! presets ) { return variable; } - const presetObject = find( presets, ( preset ) => { - return preset.slug === slug; - } ); + + const presetObject = find( presets, ( preset ) => preset.slug === slug ); if ( presetObject ) { - const { valueKey } = presetData; + const { valueKey } = metadata; const result = presetObject[ valueKey ]; return getValueFromVariable( styles, blockName, result ); } + return variable; } function getValueFromCustomVariable( styles, blockName, variable, path ) { const result = - get( styles, [ 'settings', blockName, 'custom', ...path ] ) ?? - get( styles, [ 'settings', ALL_BLOCKS_NAME, 'custom', ...path ] ); + get( styles, [ 'settings', 'blocks', blockName, 'custom', ...path ] ) ?? + get( styles, [ 'settings', 'custom', ...path ] ); if ( ! result ) { return variable; } @@ -176,6 +187,7 @@ export function getValueFromVariable( styles, blockName, variable ) { if ( ! variable || ! isString( variable ) ) { return variable; } + let parsedVar; const INTERNAL_REFERENCE_PREFIX = 'var:'; const CSS_REFERENCE_PREFIX = 'var(--wp--'; @@ -192,6 +204,7 @@ export function getValueFromVariable( styles, blockName, variable ) { .slice( CSS_REFERENCE_PREFIX.length, -CSS_REFERENCE_SUFFIX.length ) .split( '--' ); } else { + // Value is raw. return variable; } diff --git a/packages/edit-site/src/components/sidebar/color-palette-panel.js b/packages/edit-site/src/components/sidebar/color-palette-panel.js index 3fce2f3d8d4c8f..8fae2214124d7e 100644 --- a/packages/edit-site/src/components/sidebar/color-palette-panel.js +++ b/packages/edit-site/src/components/sidebar/color-palette-panel.js @@ -13,7 +13,7 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { useEditorFeature, ALL_BLOCKS_NAME } from '../editor/utils'; +import { useEditorFeature } from '../editor/utils'; import { store as editSiteStore } from '../../store'; /** @@ -40,17 +40,12 @@ export default function ColorPalettePanel( { .__experimentalGlobalStylesBaseStyles; const basePalette = get( baseStyles, [ - contextName, - 'settings', - 'color', - 'palette', - ] ) ?? - get( baseStyles, [ - ALL_BLOCKS_NAME, 'settings', + 'blocks', + contextName, 'color', 'palette', - ] ); + ] ) ?? get( baseStyles, [ 'settings', 'color', 'palette' ] ); if ( ! basePalette ) { return EMPTY_ARRAY; } diff --git a/packages/edit-site/src/components/sidebar/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar/global-styles-sidebar.js index f0017eb703d05c..7aea104e9d8a46 100644 --- a/packages/edit-site/src/components/sidebar/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar/global-styles-sidebar.js @@ -24,7 +24,6 @@ import { useGlobalStylesReset, } from '../editor/global-styles-provider'; import DefaultSidebar from './default-sidebar'; -import { ROOT_BLOCK_NAME } from '../editor/utils'; import { default as TypographyPanel, useHasTypographyPanel, @@ -96,34 +95,20 @@ function GlobalStylesPanel( { ); } -function getPanelTitle( context ) { - /* - * We use the block's name as the panel title. - * - * Some blocks (eg: core/heading) can represent different - * contexts (eg: core/heading/h1, core/heading/h2). - * For those, we attach the selector (h1) after the block's name. - * - * The title can't be accessed in the server, - * as it's translatable and the block.json doesn't - * have it yet. - */ - const blockType = getBlockType( context.blockName ); +function getPanelTitle( blockName ) { + const blockType = getBlockType( blockName ); + // Protect against blocks that aren't registered // eg: widget-area if ( blockType === undefined ) { - return blockType; + return blockName; } - let panelTitle = blockType.title; - if ( context?.title ) { - panelTitle += ` (${ context.title })`; - } - return panelTitle; + return blockType.title; } function GlobalStylesBlockPanels( { - contexts, + blocks, getStyle, setStyle, getSetting, @@ -132,27 +117,24 @@ function GlobalStylesBlockPanels( { const panels = useMemo( () => sortBy( - map( contexts, ( context, name ) => { + map( blocks, ( block, name ) => { return { - context, + block, name, - wrapperPanelTitle: getPanelTitle( context ), + wrapperPanelTitle: getPanelTitle( name ), }; } ), ( { wrapperPanelTitle } ) => wrapperPanelTitle ), - [ contexts ] + [ blocks ] ); - return map( panels, ( { context, name, wrapperPanelTitle } ) => { - if ( name === ROOT_BLOCK_NAME ) { - return null; - } + return map( panels, ( { block, name, wrapperPanelTitle } ) => { return (