/** * External dependencies */ import deepmerge from 'deepmerge'; import { isPlainObject } from 'is-plain-object'; /** * WordPress dependencies */ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; import { useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; const { GlobalStylesContext, cleanEmptyObject } = unlock( blockEditorPrivateApis ); export function mergeBaseAndUserConfigs( base, user ) { return deepmerge( base, user, { /* * We only pass as arrays the presets, * in which case we want the new array of values * to override the old array (no merging). */ isMergeableObject: isPlainObject, /* * Exceptions to the above rule. * Background images should be replaced, not merged, * as they themselves are specific object definitions for the style. */ customMerge: ( key ) => { if ( key === 'backgroundImage' ) { return ( baseConfig, userConfig ) => userConfig; } return undefined; }, } ); } function useGlobalStylesUserConfig() { const { globalStylesId, isReady, settings, styles, _links } = useSelect( ( select ) => { const { getEntityRecord, getEditedEntityRecord, hasFinishedResolution, canUser, } = select( coreStore ); const _globalStylesId = select( coreStore ).__experimentalGetCurrentGlobalStylesId(); let record; const userCanEditGlobalStyles = canUser( 'update', { kind: 'root', name: 'globalStyles', id: _globalStylesId, } ); if ( _globalStylesId ) { if ( userCanEditGlobalStyles ) { record = getEditedEntityRecord( 'root', 'globalStyles', _globalStylesId ); } else { record = getEntityRecord( 'root', 'globalStyles', _globalStylesId, { context: 'view' } ); } } let hasResolved = false; if ( hasFinishedResolution( '__experimentalGetCurrentGlobalStylesId' ) ) { if ( _globalStylesId ) { hasResolved = userCanEditGlobalStyles ? hasFinishedResolution( 'getEditedEntityRecord', [ 'root', 'globalStyles', _globalStylesId, ] ) : hasFinishedResolution( 'getEntityRecord', [ 'root', 'globalStyles', _globalStylesId, { context: 'view' }, ] ); } else { hasResolved = true; } } return { globalStylesId: _globalStylesId, isReady: hasResolved, settings: record?.settings, styles: record?.styles, _links: record?._links, }; }, [] ); const { getEditedEntityRecord } = useSelect( coreStore ); const { editEntityRecord } = useDispatch( coreStore ); const config = useMemo( () => { return { settings: settings ?? {}, styles: styles ?? {}, _links: _links ?? {}, }; }, [ settings, styles, _links ] ); const setConfig = useCallback( /** * Set the global styles config. * @param {Function|Object} callbackOrObject If the callbackOrObject is a function, pass the current config to the callback so the consumer can merge values. * Otherwise, overwrite the current config with the incoming object. * @param {Object} options Options for editEntityRecord Core selector. */ ( callbackOrObject, options = {} ) => { const record = getEditedEntityRecord( 'root', 'globalStyles', globalStylesId ); const currentConfig = { styles: record?.styles ?? {}, settings: record?.settings ?? {}, _links: record?._links ?? {}, }; const updatedConfig = typeof callbackOrObject === 'function' ? callbackOrObject( currentConfig ) : callbackOrObject; editEntityRecord( 'root', 'globalStyles', globalStylesId, { styles: cleanEmptyObject( updatedConfig.styles ) || {}, settings: cleanEmptyObject( updatedConfig.settings ) || {}, _links: cleanEmptyObject( updatedConfig._links ) || {}, }, options ); }, [ globalStylesId, editEntityRecord, getEditedEntityRecord ] ); return [ isReady, config, setConfig ]; } function useGlobalStylesBaseConfig() { const baseConfig = useSelect( ( select ) => select( coreStore ).__experimentalGetCurrentThemeBaseGlobalStyles(), [] ); return [ !! baseConfig, baseConfig ]; } export function useGlobalStylesContext() { const [ isUserConfigReady, userConfig, setUserConfig ] = useGlobalStylesUserConfig(); const [ isBaseConfigReady, baseConfig ] = useGlobalStylesBaseConfig(); const mergedConfig = useMemo( () => { if ( ! baseConfig || ! userConfig ) { return {}; } return mergeBaseAndUserConfigs( baseConfig, userConfig ); }, [ userConfig, baseConfig ] ); const context = useMemo( () => { return { isReady: isUserConfigReady && isBaseConfigReady, user: userConfig, base: baseConfig, merged: mergedConfig, setUserConfig, }; }, [ mergedConfig, userConfig, baseConfig, setUserConfig, isUserConfigReady, isBaseConfigReady, ] ); return context; } export function GlobalStylesProvider( { children } ) { const context = useGlobalStylesContext(); if ( ! context.isReady ) { return null; } return ( { children } ); }