Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Bindings API: Add components for the editor UI and create basic UI for the existing sources #57258

Closed
Closed
2 changes: 1 addition & 1 deletion lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function wp_enqueue_block_view_script( $block_name, $args ) {
) ) {

require_once __DIR__ . '/block-bindings/index.php';
// Allowed blocks that support block bindings.
// Allowed blocks that support block bindings.
// TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes?
global $block_bindings_allowed_blocks;
$block_bindings_allowed_blocks = array(
Expand Down
12 changes: 12 additions & 0 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,18 @@ _Properties_

Ensures that the text selection keeps the same vertical distance from the viewport during keyboard events within this component. The vertical distance can vary. It is the last clicked or scrolled to position.

### updateBlockBindingsAttribute

Helper to update the bindings attribute used by the Block Bindings API.

_Parameters_

- _blockAttributes_ `Object`: - The original block attributes.
- _setAttributes_ `Function`: - setAttributes function to modify the bindings property.
- _attributeName_ `string`: - The attribute in the bindings object to update.
- _sourceName_ `string`: - The source name added to the bindings property.
- _sourceAttributes_ `string`: - The source attributes added to the bindings property.

### URLInput

_Related_
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as transformStyles } from './transform-styles';
export * from './block-variation-transforms';
export { default as getPxFromCssUnit } from './get-px-from-css-unit';
export * from './update-block-bindings';
68 changes: 68 additions & 0 deletions packages/block-editor/src/utils/update-block-bindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Helper to update the bindings attribute used by the Block Bindings API.
*
* @param {Object} blockAttributes - The original block attributes.
* @param {Function} setAttributes - setAttributes function to modify the bindings property.
* @param {string} attributeName - The attribute in the bindings object to update.
* @param {string} sourceName - The source name added to the bindings property.
* @param {string} sourceAttributes - The source attributes added to the bindings property.
*/
export const updateBlockBindingsAttribute = (
blockAttributes,
setAttributes,
attributeName,
sourceName,
sourceAttributes
) => {
// TODO: Review if we can create a React Hook for this.

// Assuming the following format for the bindings property of the "metadata" attribute:
//
// "bindings": {
// "title": {
// "source": {
// "name": "metadata",
// "attributes": { "value": "text_custom_field" }
// }
// },
// "url": {
// "source": {
// "name": "metadata",
// "attributes": { "value": "text_custom_field" }
// }
// }
// },
// .

let updatedBindings = {};
// // If no sourceName is provided, remove the attribute from the bindings.
if ( sourceName === null ) {
if ( ! blockAttributes?.metadata.bindings ) {
return blockAttributes?.metadata;
}

updatedBindings = {
...blockAttributes?.metadata?.bindings,
[ attributeName ]: undefined,
};
if ( Object.keys( updatedBindings ).length === 1 ) {
updatedBindings = undefined;
}
} else {
updatedBindings = {
...blockAttributes?.metadata?.bindings,
[ attributeName ]: {
source: { name: sourceName, attributes: sourceAttributes },
},
};
}

setAttributes( {
metadata: {
...blockAttributes.metadata,
bindings: updatedBindings,
},
} );

return blockAttributes.metadata;
};
2 changes: 1 addition & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"sideEffects": [
"build-style/**",
"src/**/*.scss",
"{src,build,build-module}/{index.js,store/index.js,hooks/**}"
"{src,build,build-module}/{index.js,store/index.js,hooks/**,hooks/block-bindings-sources/**}"
],
"dependencies": {
"@babel/runtime": "^7.16.0",
Expand Down
279 changes: 279 additions & 0 deletions packages/editor/src/components/block-bindings/bindings-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/**
* WordPress dependencies
*/
import { useState, cloneElement, Fragment } from '@wordpress/element';
import {
BlockControls,
updateBlockBindingsAttribute,
} from '@wordpress/block-editor';
import {
Button,
createSlotFill,
MenuItem,
MenuGroup,
Popover,
} from '@wordpress/components';
import {
plugins as pluginsIcon,
chevronDown,
chevronUp,
} from '@wordpress/icons';
import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import { BLOCK_BINDINGS_ALLOWED_BLOCKS } from '../../store/constants';

const { Slot, Fill } = createSlotFill( 'BlockBindingsUI' );

const BlockBindingsFill = ( { children, source, label } ) => {
return (
<Fill>
{ ( props ) => {
return (
<>
{ cloneElement( children, {
source,
label,
...props,
} ) }
</>
);
} }
</Fill>
);
};

export default BlockBindingsFill;

const BlockBindingsUI = ( props ) => {
const [ addingBinding, setAddingBinding ] = useState( false );
const [ popoverAnchor, setPopoverAnchor ] = useState();
return (
<>
<BlockControls group="other">
<Button
onClick={ () => {
setAddingBinding( ! addingBinding );
} }
aria-expanded={ true }
icon={ pluginsIcon }
ref={ setPopoverAnchor }
></Button>
{ addingBinding && (
<Popover
popoverAnchor={ popoverAnchor }
onClose={ () => {
setAddingBinding( false );
} }
onFocusOutside={ () => {
setAddingBinding( false );
} }
placement="bottom"
shift
className="block-bindings-ui-popover"
{ ...props }
>
<AttributesLayer
{ ...props }
setAddingBinding={ setAddingBinding }
/>
</Popover>
) }
</BlockControls>
</>
);
};

function AttributesLayer( props ) {
const [ activeAttribute, setIsActiveAttribute ] = useState( false );
const [ activeSource, setIsActiveSource ] = useState( false );
return (
<MenuGroup>
{ BLOCK_BINDINGS_ALLOWED_BLOCKS[ props.name ].map(
( attribute ) => (
<div
key={ attribute }
className="block-bindings-attribute-picker-container"
>
<MenuItem
icon={
activeAttribute === attribute
? chevronUp
: chevronDown
}
isSelected={ activeAttribute === attribute }
onClick={ () =>
setIsActiveAttribute(
activeAttribute === attribute
? false
: attribute
)
}
className="block-bindings-attribute-picker-button"
>
{ attribute }
</MenuItem>
{ activeAttribute === attribute && (
<>
<MenuGroup>
{ /* Sources can fill this slot */ }
<Slot
fillProps={ {
...props,
currentAttribute: attribute,
setIsActiveAttribute,
} }
>
{ ( fills ) => {
if ( ! fills.length ) {
return null;
}

return (
<>
{ fills.map(
( fill, index ) => {
// TODO: Check better way to get the source and label.
const source =
fill[ 0 ].props
.children
.props
.source;
const sourceLabel =
fill[ 0 ].props
.children
.props
.label;
const isSourceSelected =
activeSource ===
source;

return (
<Fragment
key={
index
}
>
<MenuItem
icon={
isSourceSelected
? chevronUp
: chevronDown
}
isSelected={
isSourceSelected
}
onClick={ () =>
setIsActiveSource(
isSourceSelected
? false
: source
)
}
className="block-bindings-source-picker-button"
>
{
sourceLabel
}
</MenuItem>
{ isSourceSelected &&
fill }
</Fragment>
);
}
) }
</>
);
} }
</Slot>
</MenuGroup>
<RemoveBindingButton
{ ...props }
currentAttribute={ attribute }
setIsActiveAttribute={
setIsActiveAttribute
}
/>
</>
) }
</div>
)
) }
</MenuGroup>
);
}

function RemoveBindingButton( props ) {
return (
<Button
className="block-bindings-remove-button"
onClick={ () => {
if ( ! props.attributes?.metadata.bindings ) {
return;
}

const {
currentAttribute,
attributes,
setAttributes,
setAddingBinding,
} = props;
// Modify the attribute we are binding.
const newAttributes = {};
newAttributes[ currentAttribute ] = '';
props.setAttributes( newAttributes );

updateBlockBindingsAttribute(
attributes,
setAttributes,
currentAttribute,
null
);

setAddingBinding( false );
} }
>
Remove binding
</Button>
);
}

if ( window.__experimentalBlockBindings ) {
addFilter(
'blocks.registerBlockType',
'core/block-bindings-ui',
( settings, name ) => {
if ( ! ( name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) {
return settings;
}

// TODO: Review the implications of this and the code.
// Add the necessary context to the block.
const contextItems = [ 'postId', 'postType', 'queryId' ];
const usesContextArray = settings.usesContext;
const oldUsesContextArray = new Set( usesContextArray );
contextItems.forEach( ( item ) => {
if ( ! oldUsesContextArray.has( item ) ) {
usesContextArray.push( item );
}
} );
settings.usesContext = usesContextArray;

// Add bindings button to the block toolbar.
const OriginalComponent = settings.edit;
settings.edit = ( props ) => {
return (
<>
<OriginalComponent { ...props } />
<BlockBindingsUI { ...props } />
</>
);
};

return settings;
}
);
}

// TODO: Add also some components to the sidebar.
Loading
Loading