Skip to content
36 changes: 36 additions & 0 deletions lib/block-supports/pattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Pattern block support flag.
*
* @package gutenberg
*/

$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) {
/**
* Registers the dynamicContent context for block types that support it.
*
* @param WP_Block_Type $block_type Block Type.
*/
function gutenberg_register_pattern_support( $block_type ) {
$pattern_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( '__experimentalConnections' ), false ) : false;

if ( $pattern_support ) {
if ( ! $block_type->uses_context ) {
$block_type->uses_context = array();
}

if ( ! in_array( 'dynamicContent', $block_type->uses_context, true ) ) {
$block_type->uses_context[] = 'dynamicContent';
}
}
}

// Register the block support.
WP_Block_Supports::get_instance()->register(
'pattern',
array(
'register_attribute' => 'gutenberg_register_pattern_support',
)
);
}
21 changes: 16 additions & 5 deletions lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst
$blocks_attributes_allowlist = array(
'core/paragraph' => array( 'content' ),
'core/image' => array( 'url' ),
'core/list' => array( 'innerBlocks' ),
);

// Whitelist of the block types that support block connections.
Expand Down Expand Up @@ -132,14 +133,13 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst
continue;
}

// If the source value is not "meta_fields", skip it because the only supported
// connection source is meta (custom fields) for now.
if ( 'meta_fields' !== $attribute_value['source'] ) {
// Skip if the source value is not "meta_fields" or "pattern_attributes".
if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) {
continue;
}

// If the attribute does not have a source, skip it.
if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
if ( 'innerBlocks' !== $attribute_name && ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) {
continue;
}

Expand All @@ -154,12 +154,22 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst
$attribute_value['value']
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following will need some work. Currently, there is no common attribute that identifies what the outer wrapper tag is across blocks using innerBlocks. This currently just gets the first tag in the content, which works for lists, but may not for other blocks, so we may need to add an additional attribute to the blocks to clearly identify the innerBlocks wrapper.

$block_tag = 'innerBlocks' === $attribute_name ? null : $block_type->attributes[ $attribute_name ]['selector'];

if ( 'innerBlocks' === $attribute_name ) {
$custom_value = do_blocks( $custom_value );
}

if ( false === $custom_value ) {
continue;
}

$tags = new WP_HTML_Tag_Processor( $block_content );
$found = $tags->next_tag(
array(
// TODO: In the future, when blocks other than Paragraph and Image are
// supported, we should build the full query from CSS selector.
'tag_name' => $block_type->attributes[ $attribute_name ]['selector'],
'tag_name' => $block_tag,
)
);
if ( ! $found ) {
Expand All @@ -181,5 +191,6 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst

return $block_content;
}

add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 );
}
7 changes: 5 additions & 2 deletions lib/experimental/connection-sources/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
*/

return array(
'name' => 'meta',
'meta_fields' => function ( $block_instance, $meta_field ) {
'name' => 'meta',
'meta_fields' => function ( $block_instance, $meta_field ) {
// We should probably also check if the meta field exists but for now it's okay because
// if it doesn't, `get_post_meta()` will just return an empty string.
return get_post_meta( $block_instance->context['postId'], $meta_field, true );
},
'pattern_attributes' => function ( $block_instance, $meta_field ) {
return _wp_array_get( $block_instance->context, array( 'dynamicContent', $meta_field ), false );
},
);
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ function () {
require __DIR__ . '/block-supports/shadow.php';
require __DIR__ . '/block-supports/background.php';
require __DIR__ . '/block-supports/behaviors.php';
require __DIR__ . '/block-supports/pattern.php';

// Data views.
require_once __DIR__ . '/experimental/data-views.php';
103 changes: 70 additions & 33 deletions packages/block-editor/src/hooks/custom-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { addFilter } from '@wordpress/hooks';
import { PanelBody, TextControl } from '@wordpress/components';
import { PanelBody, TextControl, SelectControl } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { hasBlockSupport } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
Expand Down Expand Up @@ -55,7 +55,11 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => {

// Check if the current block is a paragraph or image block.
// Currently, only these two blocks are supported.
if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) {
if (
! [ 'core/paragraph', 'core/image', 'core/list' ].includes(
props.name
)
) {
return <BlockEdit { ...props } />;
}

Expand All @@ -65,6 +69,41 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => {
let attributeName;
if ( props.name === 'core/paragraph' ) attributeName = 'content';
if ( props.name === 'core/image' ) attributeName = 'url';
if ( props.name === 'core/list' ) attributeName = 'innerBlocks';

const connectionSource =
props.attributes?.connections?.attributes?.[ attributeName ]
?.source || '';
const connectionValue =
props.attributes?.connections?.attributes?.[ attributeName ]
?.value || '';

function updateConnections( source, value ) {
if ( value === '' ) {
props.setAttributes( {
connections: undefined,
placeholder: undefined,
} );
} else {
props.setAttributes( {
connections: {
attributes: {
// The attributeName will be either `content` or `url`.
[ attributeName ]: {
// Source will be variable, could be post_meta, user_meta, term_meta, etc.
// Could even be a custom source like a social media attribute.
source,
value,
},
},
},
placeholder: sprintf(
'This content will be replaced on the frontend by the value of "%s" custom field.',
value
),
} );
}
}

if ( hasCustomFieldsSupport && props.isSelected ) {
return (
Expand All @@ -76,42 +115,40 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => {
title={ __( 'Connections' ) }
initialOpen={ true }
>
<SelectControl
label={ __( 'Source' ) }
value={ connectionSource }
options={ [
{
label: __( 'None' ),
value: '',
},
{
label: __( 'Meta fields' ),
value: 'meta_fields',
},
{
label: __( 'Pattern attributes' ),
value: 'pattern_attributes',
},
] }
onChange={ ( nextSource ) => {
updateConnections(
nextSource,
connectionValue
);
} }
/>
<TextControl
__nextHasNoMarginBottom
autoComplete="off"
label={ __( 'Custom field meta_key' ) }
value={
props.attributes?.connections
?.attributes?.[ attributeName ]
?.value || ''
}
value={ connectionValue }
onChange={ ( nextValue ) => {
if ( nextValue === '' ) {
props.setAttributes( {
connections: undefined,
[ attributeName ]: undefined,
placeholder: undefined,
} );
} else {
props.setAttributes( {
connections: {
attributes: {
// The attributeName will be either `content` or `url`.
[ attributeName ]: {
// Source will be variable, could be post_meta, user_meta, term_meta, etc.
// Could even be a custom source like a social media attribute.
source: 'meta_fields',
value: nextValue,
},
},
},
[ attributeName ]: undefined,
placeholder: sprintf(
'This content will be replaced on the frontend by the value of "%s" custom field.',
nextValue
),
} );
}
updateConnections(
connectionSource,
nextValue
);
} }
/>
</PanelBody>
Expand Down
8 changes: 8 additions & 0 deletions packages/block-editor/src/store/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,11 @@ export function deleteStyleOverride( id ) {
id,
};
}

export function syncDerivedBlockAttributes( clientId, attributes ) {
return {
type: 'SYNC_DERIVED_BLOCK_ATTRIBUTES',
clientIds: [ clientId ],
attributes,
};
}
8 changes: 8 additions & 0 deletions packages/block-editor/src/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ const withBlockTree =
false
);
break;
case 'SYNC_DERIVED_BLOCK_ATTRIBUTES':
case 'UPDATE_BLOCK_ATTRIBUTES': {
newState.tree = new Map( newState.tree );
action.clientIds.forEach( ( clientId ) => {
Expand Down Expand Up @@ -456,6 +457,12 @@ function withPersistentBlockChange( reducer ) {
return ( state, action ) => {
let nextState = reducer( state, action );

if ( action.type === 'SYNC_DERIVED_BLOCK_ATTRIBUTES' ) {
return nextState.isPersistentChange
? { ...nextState, isPersistentChange: false }
: nextState;
}

const isExplicitPersistentChange =
action.type === 'MARK_LAST_CHANGE_AS_PERSISTENT' ||
markNextChangeAsNotPersistent;
Expand Down Expand Up @@ -860,6 +867,7 @@ export const blocks = pipe(
return newState;
}

case 'SYNC_DERIVED_BLOCK_ATTRIBUTES':
case 'UPDATE_BLOCK_ATTRIBUTES': {
// Avoid a state change if none of the block IDs are known.
if ( action.clientIds.every( ( id ) => ! state.get( id ) ) ) {
Expand Down
Loading