diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index ce3fdd2f327c40..a7aa72ae6693a6 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -13,7 +13,7 @@ import { __experimentalVStack as VStack, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; +import { useRegistry } from '@wordpress/data'; import { useContext, Fragment } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; @@ -24,10 +24,10 @@ import { canBindAttribute, getBindableAttributes, } from '../hooks/use-bindings-attributes'; -import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; +import { useBlockBindingsUtils } from '../utils/block-bindings'; const { DropdownMenuV2: DropdownMenu, @@ -51,17 +51,15 @@ const useToolsPanelDropdownMenuProps = () => { : {}; }; -function BlockBindingsPanelDropdown( { - fieldsList, - addConnection, - attribute, - binding, -} ) { +function BlockBindingsPanelDropdown( { fieldsList, attribute, binding } ) { + const { getBlockBindingsSources } = unlock( blocksPrivateApis ); + const registeredSources = getBlockBindingsSources(); + const { updateBlockBindings } = useBlockBindingsUtils(); const currentKey = binding?.args?.key; return ( <> - { Object.entries( fieldsList ).map( ( [ label, fields ], i ) => ( - + { Object.entries( fieldsList ).map( ( [ name, fields ], i ) => ( + { Object.keys( fieldsList ).length > 1 && ( - { label } + { registeredSources[ name ].label } ) } { Object.entries( fields ).map( ( [ key, value ] ) => ( - addConnection( key, attribute ) + updateBlockBindings( { + [ attribute ]: { + source: name, + args: { key }, + }, + } ) } name={ attribute + '-binding' } value={ key } @@ -141,9 +144,8 @@ function EditableBlockBindingsPanelItems( { attributes, bindings, fieldsList, - addConnection, - removeConnection, } ) { + const { updateBlockBindings } = useBlockBindingsUtils(); const isMobile = useViewportMatch( 'medium', '<' ); return ( <> @@ -155,7 +157,9 @@ function EditableBlockBindingsPanelItems( { hasValue={ () => !! binding } label={ attribute } onDeselect={ () => { - removeConnection( attribute ); + updateBlockBindings( { + [ attribute ]: undefined, + } ); } } > @@ -187,91 +190,33 @@ function EditableBlockBindingsPanelItems( { ); } -export const BlockBindingsPanel = ( { name, metadata } ) => { +export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const registry = useRegistry(); const blockContext = useContext( BlockContext ); const { bindings } = metadata || {}; - - const bindableAttributes = getBindableAttributes( name ); + const { removeAllBlockBindings } = useBlockBindingsUtils(); + const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const filteredBindings = { ...bindings }; Object.keys( filteredBindings ).forEach( ( key ) => { if ( - ! canBindAttribute( name, key ) || + ! canBindAttribute( blockName, key ) || filteredBindings[ key ].source === 'core/pattern-overrides' ) { delete filteredBindings[ key ]; } } ); - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - - const { _id } = useSelect( ( select ) => { - const { getSelectedBlockClientId } = select( blockEditorStore ); - - return { - _id: getSelectedBlockClientId(), - }; - }, [] ); - if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; } - const removeAllConnections = () => { - const newMetadata = { ...metadata }; - delete newMetadata.bindings; - updateBlockAttributes( _id, { - metadata: - Object.keys( newMetadata ).length === 0 - ? undefined - : newMetadata, - } ); - }; - - const addConnection = ( value, attribute ) => { - // Assuming the block expects a flat structure for its metadata attribute - const newMetadata = { - ...metadata, - // Adjust this according to the actual structure expected by your block - bindings: { - ...metadata?.bindings, - [ attribute ]: { - source: 'core/post-meta', - args: { key: value }, - }, - }, - }; - // Update the block's attributes with the new metadata - updateBlockAttributes( _id, { - metadata: newMetadata, - } ); - }; - - const removeConnection = ( key ) => { - const newMetadata = { ...metadata }; - if ( ! newMetadata.bindings ) { - return; - } - - delete newMetadata.bindings[ key ]; - if ( Object.keys( newMetadata.bindings ).length === 0 ) { - delete newMetadata.bindings; - } - updateBlockAttributes( _id, { - metadata: - Object.keys( newMetadata ).length === 0 - ? undefined - : newMetadata, - } ); - }; - const fieldsList = {}; const { getBlockBindingsSources } = unlock( blocksPrivateApis ); const registeredSources = getBlockBindingsSources(); - Object.values( registeredSources ).forEach( - ( { getFieldsList, label, usesContext } ) => { + Object.entries( registeredSources ).forEach( + ( [ sourceName, { getFieldsList, usesContext } ] ) => { if ( getFieldsList ) { // Populate context. const context = {}; @@ -286,7 +231,7 @@ export const BlockBindingsPanel = ( { name, metadata } ) => { } ); // Only add source if the list is not empty. if ( sourceList ) { - fieldsList[ label ] = { ...sourceList }; + fieldsList[ sourceName ] = { ...sourceList }; } } } @@ -312,7 +257,7 @@ export const BlockBindingsPanel = ( { name, metadata } ) => { { - removeAllConnections(); + removeAllBlockBindings(); } } dropdownMenuProps={ dropdownMenuProps } className="block-editor-bindings__panel" @@ -327,8 +272,6 @@ export const BlockBindingsPanel = ( { name, metadata } ) => { attributes={ bindableAttributes } bindings={ filteredBindings } fieldsList={ fieldsList } - addConnection={ addConnection } - removeConnection={ removeConnection } /> ) } diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index cf464cedf04175..788bb2bef56efa 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -47,6 +47,7 @@ import { PrivatePublishDateTimePicker } from './components/publish-date-time-pic import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes'; import useBlockDisplayTitle from './components/block-title/use-block-display-title'; import TabbedSidebar from './components/tabbed-sidebar'; +import { useBlockBindingsUtils } from './utils/block-bindings'; /** * Private @wordpress/block-editor APIs. @@ -92,4 +93,5 @@ lock( privateApis, { useBlockDisplayTitle, __unstableBlockStyleVariationOverridesWithConfig, setBackgroundStyleDefaults, + useBlockBindingsUtils, } ); diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js new file mode 100644 index 00000000000000..4ba6e1a362958c --- /dev/null +++ b/packages/block-editor/src/utils/block-bindings.js @@ -0,0 +1,98 @@ +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { useBlockEditContext } from '../components/block-edit'; + +export function useBlockBindingsUtils() { + const { clientId } = useBlockEditContext(); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + const { getBlockAttributes } = useSelect( blockEditorStore ); + + /** + * Updates the value of the bindings connected to block attributes. + * It removes the binding when the new value is `undefined`. + * + * @param {Object} bindings Bindings including the attributes to update and the new object. + * @param {string} bindings.source The source name to connect to. + * @param {Object} [bindings.args] Object containing the arguments needed by the source. + * + * @example + * ```js + * import { useBlockBindingsUtils } from '@wordpress/block-editor' + * + * const { updateBlockBindings } = useBlockBindingsUtils(); + * updateBlockBindings( { + * url: { + * source: 'core/post-meta', + * args: { + * key: 'url_custom_field', + * }, + * }, + * alt: { + * source: 'core/post-meta', + * args: { + * key: 'text_custom_field', + * }, + * } + * } ); + * ``` + */ + const updateBlockBindings = ( bindings ) => { + const { metadata } = getBlockAttributes( clientId ); + const newBindings = { ...metadata?.bindings }; + Object.entries( bindings ).forEach( ( [ attribute, binding ] ) => { + if ( ! binding && newBindings[ attribute ] ) { + delete newBindings[ attribute ]; + return; + } + newBindings[ attribute ] = binding; + } ); + + const newMetadata = { + ...metadata, + bindings: newBindings, + }; + + if ( Object.keys( newMetadata.bindings ).length === 0 ) { + delete newMetadata.bindings; + } + + updateBlockAttributes( clientId, { + metadata: + Object.keys( newMetadata ).length === 0 + ? undefined + : newMetadata, + } ); + }; + + /** + * Removes the bindings property of the `metadata` attribute. + * + * @example + * ```js + * import { useBlockBindingsUtils } from '@wordpress/block-editor' + * + * const { removeAllBlockBindings } = useBlockBindingsUtils(); + * removeAllBlockBindings(); + * ``` + */ + const removeAllBlockBindings = () => { + const { metadata } = getBlockAttributes( clientId ); + const newMetadata = { ...metadata }; + delete newMetadata.bindings; + updateBlockAttributes( clientId, { + metadata: + Object.keys( newMetadata ).length === 0 + ? undefined + : newMetadata, + } ); + }; + + return { updateBlockBindings, removeAllBlockBindings }; +} diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js index 9bec88a93471eb..28dd8788a390c6 100644 --- a/packages/patterns/src/components/pattern-overrides-controls.js +++ b/packages/patterns/src/components/pattern-overrides-controls.js @@ -2,7 +2,10 @@ * WordPress dependencies */ import { useState, useId } from '@wordpress/element'; -import { InspectorControls } from '@wordpress/block-editor'; +import { + InspectorControls, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; import { BaseControl, Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -14,22 +17,9 @@ import { AllowOverridesModal, DisallowOverridesModal, } from './allow-overrides-modal'; +import { unlock } from '../lock-unlock'; -function removeBindings( bindings ) { - let updatedBindings = { ...bindings }; - delete updatedBindings.__default; - if ( ! Object.keys( updatedBindings ).length ) { - updatedBindings = undefined; - } - return updatedBindings; -} - -function addBindings( bindings ) { - return { - ...bindings, - __default: { source: PATTERN_OVERRIDES_BINDING_SOURCE }, - }; -} +const { useBlockBindingsUtils } = unlock( blockEditorPrivateApis ); function PatternOverridesControls( { attributes, @@ -49,24 +39,22 @@ function PatternOverridesControls( { const isConnectedToOtherSources = defaultBindings?.source && defaultBindings.source !== PATTERN_OVERRIDES_BINDING_SOURCE; + const { updateBlockBindings } = useBlockBindingsUtils(); function updateBindings( isChecked, customName ) { - const prevBindings = attributes?.metadata?.bindings; - const updatedBindings = isChecked - ? addBindings( prevBindings ) - : removeBindings( prevBindings ); - - const updatedMetadata = { - ...attributes.metadata, - bindings: updatedBindings, - }; - if ( customName ) { - updatedMetadata.name = customName; + setAttributes( { + metadata: { + ...attributes.metadata, + name: customName, + }, + } ); } - setAttributes( { - metadata: updatedMetadata, + updateBlockBindings( { + __default: isChecked + ? { source: PATTERN_OVERRIDES_BINDING_SOURCE } + : undefined, } ); }