UNPKG

@wordpress/block-editor

Version:
251 lines (229 loc) 7.49 kB
/** * External dependencies */ import memoize from 'memize'; /** * WordPress dependencies */ import { __, _n, sprintf } from '@wordpress/i18n'; import { getBlockTypes } from '@wordpress/blocks'; const globalStylesChangesCache = new Map(); const EMPTY_ARRAY = []; const translationMap = { caption: __( 'Caption' ), link: __( 'Link' ), button: __( 'Button' ), heading: __( 'Heading' ), h1: __( 'H1' ), h2: __( 'H2' ), h3: __( 'H3' ), h4: __( 'H4' ), h5: __( 'H5' ), h6: __( 'H6' ), 'settings.color': __( 'Color' ), 'settings.typography': __( 'Typography' ), 'styles.color': __( 'Colors' ), 'styles.spacing': __( 'Spacing' ), 'styles.background': __( 'Background' ), 'styles.typography': __( 'Typography' ), }; const getBlockNames = memoize( () => getBlockTypes().reduce( ( accumulator, { name, title } ) => { accumulator[ name ] = title; return accumulator; }, {} ) ); const isObject = ( obj ) => obj !== null && typeof obj === 'object'; /** * Get the translation for a given global styles key. * @param {string} key A key representing a path to a global style property or setting. * @return {string|undefined} A translated key or undefined if no translation exists. */ function getTranslation( key ) { if ( translationMap[ key ] ) { return translationMap[ key ]; } const keyArray = key.split( '.' ); if ( keyArray?.[ 0 ] === 'blocks' ) { const blockName = getBlockNames()?.[ keyArray[ 1 ] ]; return blockName || keyArray[ 1 ]; } if ( keyArray?.[ 0 ] === 'elements' ) { return translationMap[ keyArray[ 1 ] ] || keyArray[ 1 ]; } return undefined; } /** * A deep comparison of two objects, optimized for comparing global styles. * @param {Object} changedObject The changed object to compare. * @param {Object} originalObject The original object to compare against. * @param {string} parentPath A key/value pair object of block names and their rendered titles. * @return {string[]} An array of paths whose values have changed. */ function deepCompare( changedObject, originalObject, parentPath = '' ) { // We have two non-object values to compare. if ( ! isObject( changedObject ) && ! isObject( originalObject ) ) { /* * Only return a path if the value has changed. * And then only the path name up to 2 levels deep. */ return changedObject !== originalObject ? parentPath.split( '.' ).slice( 0, 2 ).join( '.' ) : undefined; } // Enable comparison when an object doesn't have a corresponding property to compare. changedObject = isObject( changedObject ) ? changedObject : {}; originalObject = isObject( originalObject ) ? originalObject : {}; const allKeys = new Set( [ ...Object.keys( changedObject ), ...Object.keys( originalObject ), ] ); let diffs = []; for ( const key of allKeys ) { const path = parentPath ? parentPath + '.' + key : key; const changedPath = deepCompare( changedObject[ key ], originalObject[ key ], path ); if ( changedPath ) { diffs = diffs.concat( changedPath ); } } return diffs; } /** * Returns an array of translated summarized global styles changes. * Results are cached using a Map() key of `JSON.stringify( { next, previous } )`. * * @param {Object} next The changed object to compare. * @param {Object} previous The original object to compare against. * @return {Array[]} A 2-dimensional array of tuples: [ "group", "translated change" ]. */ export function getGlobalStylesChangelist( next, previous ) { const cacheKey = JSON.stringify( { next, previous } ); if ( globalStylesChangesCache.has( cacheKey ) ) { return globalStylesChangesCache.get( cacheKey ); } /* * Compare the two changesets with normalized keys. * The order of these keys determines the order in which * they'll appear in the results. */ const changedValueTree = deepCompare( { styles: { background: next?.styles?.background, color: next?.styles?.color, typography: next?.styles?.typography, spacing: next?.styles?.spacing, }, blocks: next?.styles?.blocks, elements: next?.styles?.elements, settings: next?.settings, }, { styles: { background: previous?.styles?.background, color: previous?.styles?.color, typography: previous?.styles?.typography, spacing: previous?.styles?.spacing, }, blocks: previous?.styles?.blocks, elements: previous?.styles?.elements, settings: previous?.settings, } ); if ( ! changedValueTree.length ) { globalStylesChangesCache.set( cacheKey, EMPTY_ARRAY ); return EMPTY_ARRAY; } // Remove duplicate results. const result = [ ...new Set( changedValueTree ) ] /* * Translate the keys. * Remove empty translations. */ .reduce( ( acc, curr ) => { const translation = getTranslation( curr ); if ( translation ) { acc.push( [ curr.split( '.' )[ 0 ], translation ] ); } return acc; }, [] ); globalStylesChangesCache.set( cacheKey, result ); return result; } /** * From a getGlobalStylesChangelist() result, returns an array of translated global styles changes, grouped by type. * The types are 'blocks', 'elements', 'settings', and 'styles'. * * @param {Object} next The changed object to compare. * @param {Object} previous The original object to compare against. * @param {{maxResults:number}} options Options. maxResults: results to return before truncating. * @return {string[]} An array of translated changes. */ export default function getGlobalStylesChanges( next, previous, options = {} ) { let changeList = getGlobalStylesChangelist( next, previous ); const changesLength = changeList.length; const { maxResults } = options; if ( changesLength ) { // Truncate to `n` results if necessary. if ( !! maxResults && changesLength > maxResults ) { changeList = changeList.slice( 0, maxResults ); } return Object.entries( changeList.reduce( ( acc, curr ) => { const group = acc[ curr[ 0 ] ] || []; if ( ! group.includes( curr[ 1 ] ) ) { acc[ curr[ 0 ] ] = [ ...group, curr[ 1 ] ]; } return acc; }, {} ) ).map( ( [ key, changeValues ] ) => { const changeValuesLength = changeValues.length; const joinedChangesValue = changeValues.join( /* translators: Used between list items, there is a space after the comma. */ __( ', ' ) // eslint-disable-line @wordpress/i18n-no-flanking-whitespace ); switch ( key ) { case 'blocks': { return sprintf( // translators: %s: a list of block names separated by a comma. _n( '%s block.', '%s blocks.', changeValuesLength ), joinedChangesValue ); } case 'elements': { return sprintf( // translators: %s: a list of element names separated by a comma. _n( '%s element.', '%s elements.', changeValuesLength ), joinedChangesValue ); } case 'settings': { return sprintf( // translators: %s: a list of theme.json setting labels separated by a comma. __( '%s settings.' ), joinedChangesValue ); } case 'styles': { return sprintf( // translators: %s: a list of theme.json top-level styles labels separated by a comma. __( '%s styles.' ), joinedChangesValue ); } default: { return sprintf( // translators: %s: a list of global styles changes separated by a comma. __( '%s.' ), joinedChangesValue ); } } } ); } return EMPTY_ARRAY; }