UNPKG

@wordpress/block-editor

Version:
301 lines (272 loc) 7.54 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { getBlockDefaultClassName, getBlockType, hasBlockSupport, store as blocksStore, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { useRegistry, useSelect } from '@wordpress/data'; import { useCallback, useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import BlockContext from '../block-context'; import isURLLike from '../link-control/is-url-like'; import { canBindAttribute, hasPatternOverridesDefaultBinding, replacePatternOverridesDefaultBinding, } from '../../utils/block-bindings'; import { unlock } from '../../lock-unlock'; /** * Default value used for blocks which do not define their own context needs, * used to guarantee that a block's `context` prop will always be an object. It * is assigned as a constant since it is always expected to be an empty object, * and in order to avoid unnecessary React reconciliations of a changing object. * * @type {{}} */ const DEFAULT_BLOCK_CONTEXT = {}; const Edit = ( props ) => { const { name } = props; const blockType = getBlockType( name ); if ( ! blockType ) { return null; } // `edit` and `save` are functions or components describing the markup // with which a block is displayed. If `blockType` is valid, assign // them preferentially as the render value for the block. const Component = blockType.edit || blockType.save; return <Component { ...props } />; }; const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); const EditWithGeneratedProps = ( props ) => { const { name, clientId, attributes, setAttributes } = props; const registry = useRegistry(); const blockType = getBlockType( name ); const blockContext = useContext( BlockContext ); const registeredSources = useSelect( ( select ) => unlock( select( blocksStore ) ).getAllBlockBindingsSources(), [] ); const { blockBindings, context, hasPatternOverrides } = useMemo( () => { // Assign context values using the block type's declared context needs. const computedContext = blockType?.usesContext ? Object.fromEntries( Object.entries( blockContext ).filter( ( [ key ] ) => blockType.usesContext.includes( key ) ) ) : DEFAULT_BLOCK_CONTEXT; // Add context requested by Block Bindings sources. if ( attributes?.metadata?.bindings ) { Object.values( attributes?.metadata?.bindings || {} ).forEach( ( binding ) => { registeredSources[ binding?.source ]?.usesContext?.forEach( ( key ) => { computedContext[ key ] = blockContext[ key ]; } ); } ); } return { blockBindings: replacePatternOverridesDefaultBinding( name, attributes?.metadata?.bindings ), context: computedContext, hasPatternOverrides: hasPatternOverridesDefaultBinding( attributes?.metadata?.bindings ), }; }, [ name, blockType?.usesContext, blockContext, attributes?.metadata?.bindings, registeredSources, ] ); const computedAttributes = useSelect( ( select ) => { if ( ! blockBindings ) { return attributes; } const attributesFromSources = {}; const blockBindingsBySource = new Map(); for ( const [ attributeName, binding ] of Object.entries( blockBindings ) ) { const { source: sourceName, args: sourceArgs } = binding; const source = registeredSources[ sourceName ]; if ( ! source || ! canBindAttribute( name, attributeName ) ) { continue; } blockBindingsBySource.set( source, { ...blockBindingsBySource.get( source ), [ attributeName ]: { args: sourceArgs, }, } ); } if ( blockBindingsBySource.size ) { for ( const [ source, bindings ] of blockBindingsBySource ) { // Get values in batch if the source supports it. let values = {}; if ( ! source.getValues ) { Object.keys( bindings ).forEach( ( attr ) => { // Default to the the source label when `getValues` doesn't exist. values[ attr ] = source.label; } ); } else { values = source.getValues( { select, context, clientId, bindings, } ); } for ( const [ attributeName, value ] of Object.entries( values ) ) { if ( attributeName === 'url' && ( ! value || ! isURLLike( value ) ) ) { // Return null if value is not a valid URL. attributesFromSources[ attributeName ] = null; } else { attributesFromSources[ attributeName ] = value; } } } } return { ...attributes, ...attributesFromSources, }; }, [ attributes, blockBindings, clientId, context, name, registeredSources, ] ); const setBoundAttributes = useCallback( ( nextAttributes ) => { if ( ! blockBindings ) { setAttributes( nextAttributes ); return; } registry.batch( () => { const keptAttributes = { ...nextAttributes }; const blockBindingsBySource = new Map(); // Loop only over the updated attributes to avoid modifying the bound ones that haven't changed. for ( const [ attributeName, newValue ] of Object.entries( keptAttributes ) ) { if ( ! blockBindings[ attributeName ] || ! canBindAttribute( name, attributeName ) ) { continue; } const binding = blockBindings[ attributeName ]; const source = registeredSources[ binding?.source ]; if ( ! source?.setValues ) { continue; } blockBindingsBySource.set( source, { ...blockBindingsBySource.get( source ), [ attributeName ]: { args: binding.args, newValue, }, } ); delete keptAttributes[ attributeName ]; } if ( blockBindingsBySource.size ) { for ( const [ source, bindings, ] of blockBindingsBySource ) { source.setValues( { select: registry.select, dispatch: registry.dispatch, context, clientId, bindings, } ); } } const hasParentPattern = !! context[ 'pattern/overrides' ]; if ( // Don't update non-connected attributes if the block is using pattern overrides // and the editing is happening while overriding the pattern (not editing the original). ! ( hasPatternOverrides && hasParentPattern ) && Object.keys( keptAttributes ).length ) { // Don't update caption and href until they are supported. if ( hasPatternOverrides ) { delete keptAttributes.caption; delete keptAttributes.href; } setAttributes( keptAttributes ); } } ); }, [ blockBindings, clientId, context, hasPatternOverrides, setAttributes, registeredSources, name, registry, ] ); if ( ! blockType ) { return null; } if ( blockType.apiVersion > 1 ) { return ( <EditWithFilters { ...props } attributes={ computedAttributes } context={ context } setAttributes={ setBoundAttributes } /> ); } // Generate a class name for the block's editable form. const generatedClassName = hasBlockSupport( blockType, 'className', true ) ? getBlockDefaultClassName( name ) : null; const className = clsx( generatedClassName, attributes?.className, props.className ); return ( <EditWithFilters { ...props } attributes={ computedAttributes } className={ className } context={ context } setAttributes={ setBoundAttributes } /> ); }; export default EditWithGeneratedProps;