Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ function gutenberg_enable_experiments() {
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-content-only-inspector-fields', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalContentOnlyInspectorFields = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-fields-bindings', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockFieldsBindings = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-customizable-navigation-overlays', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalNavigationOverlays = true', 'before' );
}
Expand Down
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-block-fields-bindings',
__( 'Block fields: Integrate Block Bindings with Block Fields', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Enables Block Bindings integration directly in Block Fields UI, allowing you to connect block attributes to data sources alongside field controls.', 'gutenberg' ),
'id' => 'gutenberg-block-fields-bindings',
)
);

add_settings_field(
'gutenberg-workflow-palette',
__( 'Workflow Palette', 'gutenberg' ),
Expand Down
69 changes: 44 additions & 25 deletions packages/block-editor/src/hooks/block-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { store as blocksStore } from '@wordpress/blocks';
import {
store as blocksStore,
privateApis as blocksPrivateApis,
} from '@wordpress/blocks';
import {
__experimentalItemGroup as ItemGroup,
__experimentalText as Text,
Expand Down Expand Up @@ -42,31 +45,47 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => {
const { removeAllBlockBindings } = useBlockBindingsUtils();
const dropdownMenuProps = useToolsPanelDropdownMenuProps();

const { bindableAttributes, hasCompatibleFields } = useSelect(
( select ) => {
const { __experimentalBlockBindingsSupportedAttributes } =
select( blockEditorStore ).getSettings();
const {
getAllBlockBindingsSources,
getBlockBindingsSourceFieldsList,
} = unlock( select( blocksStore ) );
const { bindableAttributes, hasCompatibleFields, hasBlockFields } =
useSelect(
( select ) => {
const { __experimentalBlockBindingsSupportedAttributes } =
select( blockEditorStore ).getSettings();
const {
getAllBlockBindingsSources,
getBlockBindingsSourceFieldsList,
} = unlock( select( blocksStore ) );

return {
bindableAttributes:
__experimentalBlockBindingsSupportedAttributes?.[
blockName
],
hasCompatibleFields: Object.values(
getAllBlockBindingsSources()
).some(
( source ) =>
getBlockBindingsSourceFieldsList( source, blockContext )
?.length > 0
),
};
},
[ blockName, blockContext ]
);
const blockType =
select( blocksStore ).getBlockType( blockName );
const { fieldsKey } = unlock( blocksPrivateApis );

return {
bindableAttributes:
__experimentalBlockBindingsSupportedAttributes?.[
blockName
],
hasCompatibleFields: Object.values(
getAllBlockBindingsSources()
).some(
( source ) =>
getBlockBindingsSourceFieldsList(
source,
blockContext
)?.length > 0
),
hasBlockFields: !! (
window?.__experimentalContentOnlyInspectorFields &&
blockType?.[ fieldsKey ]
),
};
},
[ blockName, blockContext ]
);

// Hide if Block Fields handle bindings
if ( hasBlockFields && window?.__experimentalBlockFieldsBindings ) {
return null;
}

// Return early if there are no bindable attributes.
if ( ! bindableAttributes || bindableAttributes.length === 0 ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* WordPress dependencies
*/
import { privateApis as componentsPrivateApis } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { link, lockSmall, error, linkOff } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { unlock } from '../../../lock-unlock';
import useBindingState from './use-binding-state';
import BindingMenu from './binding-menu';

const { Menu } = unlock( componentsPrivateApis );

/**
* Badge component that shows binding status and provides access to binding menu.
*
* @param {Object} props Component props.
* @param {string} props.fieldId The field/attribute identifier.
* @param {string} props.blockName The block type name.
* @param {string} props.clientId The block client ID.
* @param {Object} props.blockContext The block context.
* @return {Element} The binding field badge component.
*/
export default function BindingFieldBadge( {
fieldId,
blockName,
clientId,
blockContext,
} ) {
const {
isBound,
binding,
isEditable,
isValid,
sourceLabel,
fieldLabel,
isBindable,
} = useBindingState( {
fieldId,
blockName,
clientId,
blockContext,
} );

// Don't render if field is not bindable
if ( ! isBindable ) {
return null;
}

// Determine icon and label based on state
let icon;
let label;
let className = 'binding-field-badge';

if ( isBound ) {
if ( ! isValid ) {
icon = error;
label = __( 'Source not registered' );
className += ' is-invalid';
} else if ( isEditable ) {
icon = link;
label = fieldLabel || sourceLabel || __( 'Connected to source' );
className += ' is-connected';
} else {
icon = lockSmall;
label = fieldLabel || sourceLabel || __( 'Connected (read-only)' );
className += ' is-connected is-read-only';
}
} else {
icon = linkOff;
label = __( 'Connect to source' );
className += ' binding-field-badge__connect';
}

return (
<div className={ className }>
<Menu placement="left-start">
<Menu.TriggerButton
icon={ icon }
label={ label }
size="compact"
variant="tertiary"
/>
<BindingMenu
fieldId={ fieldId }
blockName={ blockName }
clientId={ clientId }
blockContext={ blockContext }
binding={ binding }
placement="left-start"
/>
</Menu>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* WordPress dependencies
*/
import { privateApis as componentsPrivateApis } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { unlock } from '../../../lock-unlock';
import BlockBindingsSourceFieldsList from '../../../components/block-bindings/source-fields-list';
import useBlockBindingsUtils from '../../../components/block-bindings/use-block-bindings-utils';
import { getCompatibleFields } from './get-compatible-fields';

const { Menu } = unlock( componentsPrivateApis );

/**
* Menu content for managing bindings.
*
* @param {Object} props Component props.
* @param {string} props.fieldId The field/attribute identifier.
* @param {string} props.blockName The block type name.
* @param {string} props.clientId The block client ID.
* @param {Object} props.blockContext The block context.
* @param {Object} props.binding Current binding (if any).
* @param {string} props.placement Popover placement.
* @return {Element} The binding menu component.
*/
export default function BindingMenu( {
fieldId,
blockName,
clientId,
blockContext,
binding,
placement = 'left-start',
} ) {
const compatibleFields = useSelect(
( select ) =>
getCompatibleFields( fieldId, blockName, blockContext, select ),
[ fieldId, blockName, blockContext ]
);

const { updateBlockBindings } = useBlockBindingsUtils( clientId );

const hasCompatibleFields = Object.keys( compatibleFields ).length > 0;
const isBound = !! binding;

const handleDisconnect = () => {
updateBlockBindings( {
[ fieldId ]: undefined,
} );
};

return (
<Menu.Popover placement={ placement } gutter={ 8 }>
<Menu>
<Menu.Group>
{ ! hasCompatibleFields && ! isBound && (
<Menu.Item disabled>
<Menu.ItemLabel>
{ __( 'No sources available' ) }
</Menu.ItemLabel>
</Menu.Item>
) }

{ hasCompatibleFields &&
Object.entries( compatibleFields ).map(
( [ sourceKey, fields ] ) => (
<BlockBindingsSourceFieldsList
key={ sourceKey }
sourceKey={ sourceKey }
fields={ fields }
attribute={ fieldId }
args={ binding?.args }
/>
)
) }
</Menu.Group>

{ isBound && (
<>
<Menu.Separator />
<Menu.Group>
<Menu.Item onClick={ handleDisconnect }>
<Menu.ItemLabel>
{ __( 'Disconnect' ) }
</Menu.ItemLabel>
</Menu.Item>
</Menu.Group>
</>
) }
</Menu>
</Menu.Popover>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* WordPress dependencies
*/
import { store as blocksStore } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import { unlock } from '../../../lock-unlock';

/**
* Get compatible binding sources/fields for a specific Block Field.
*
* @param {string} fieldId The field/attribute identifier.
* @param {string} blockName The block type name.
* @param {Object} blockContext The block context.
* @param {Object} select The select function from useSelect.
* @return {Object} Object with source names as keys and compatible fields arrays as values.
*/
export function getCompatibleFields(
fieldId,
blockName,
blockContext,
select
) {
// Unlock selectors from the blocks store
const { getAllBlockBindingsSources, getBlockBindingsSourceFieldsList } =
unlock( select( blocksStore ) );

const blockType = select( blocksStore ).getBlockType( blockName );
const attributeType = blockType?.attributes?.[ fieldId ]?.type;

if ( ! attributeType ) {
return {};
}

// Map Block Fields types to binding types
const bindingType =
attributeType === 'rich-text' ? 'string' : attributeType;

const allSources = getAllBlockBindingsSources();
const compatibleFields = {};

Object.entries( allSources ).forEach( ( [ sourceName, source ] ) => {
// Filter out pattern-overrides
if ( sourceName === 'core/pattern-overrides' ) {
return;
}

const fieldsList = getBlockBindingsSourceFieldsList(
source,
blockContext,
select
);

if ( ! fieldsList?.length ) {
return;
}

const compatible = fieldsList.filter(
( field ) => field.type === bindingType
);

if ( compatible.length ) {
compatibleFields[ sourceName ] = compatible;
}
} );

return compatibleFields;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Internal dependencies
*/
export { default as useBindingState } from './use-binding-state';
export { default as BindingFieldBadge } from './binding-field-badge';
export { default as BindingMenu } from './binding-menu';
export { withBindingBadge } from './with-binding-badge';
export { getCompatibleFields } from './get-compatible-fields';
Loading
Loading