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
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