diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index fafeba00e3b3f6..febb404394a4f9 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -121,3 +121,107 @@ function gutenberg_register_metadata_attribute( $args ) { return $args; } add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); + + +$gutenberg_experiments = get_option( 'gutenberg-experiments' ); +if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { + /** + * Renders the block meta attributes. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param WP_Block $block_instance The block instance. + */ + function gutenberg_render_block_connections( $block_content, $block, $block_instance ) { + $connection_sources = require __DIR__ . '/connection-sources/index.php'; + $block_type = $block_instance->block_type; + + // Allowlist of blocks that support block connections. + // Currently, we only allow the following blocks and attributes: + // - Paragraph: content. + // - Image: url. + $blocks_attributes_allowlist = array( + 'core/paragraph' => array( 'content' ), + 'core/image' => array( 'url' ), + ); + + // Whitelist of the block types that support block connections. + // Currently, we only allow the Paragraph and Image blocks to use block connections. + if ( ! in_array( $block['blockName'], array_keys( $blocks_attributes_allowlist ), true ) ) { + return $block_content; + } + + // If for some reason, the block type is not found, skip it. + if ( null === $block_type ) { + return $block_content; + } + + // If the block does not have support for block connections, skip it. + if ( ! block_has_support( $block_type, array( '__experimentalConnections' ), false ) ) { + return $block_content; + } + + // Get all the attributes that have a connection. + $connected_attributes = _wp_array_get( $block['attrs'], array( 'connections', 'attributes' ), false ); + if ( ! $connected_attributes ) { + return $block_content; + } + + foreach ( $connected_attributes as $attribute_name => $attribute_value ) { + + // If the attribute is not in the allowlist, skip it. + if ( ! in_array( $attribute_name, $blocks_attributes_allowlist[ $block['blockName'] ], true ) ) { + 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'] ) { + continue; + } + + // If the attribute does not have a source, skip it. + if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { + continue; + } + + // If the attribute does not specify the name of the custom field, skip it. + if ( ! isset( $attribute_value['value'] ) ) { + continue; + } + + // Get the content from the connection source. + $custom_value = $connection_sources[ $attribute_value['source'] ]( + $block_instance, + $attribute_value['value'] + ); + + $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'], + ) + ); + if ( ! $found ) { + return $block_content; + }; + $tag_name = $tags->get_tag(); + $markup = "<$tag_name>$custom_value"; + $updated_tags = new WP_HTML_Tag_Processor( $markup ); + $updated_tags->next_tag(); + + // Get all the attributes from the original block and add them to the new markup. + $names = $tags->get_attribute_names_with_prefix( '' ); + foreach ( $names as $name ) { + $updated_tags->set_attribute( $name, $tags->get_attribute( $name ) ); + } + + return $updated_tags->get_updated_html(); + } + + return $block_content; + } + add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 ); +} diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php new file mode 100644 index 00000000000000..b63abcad96f628 --- /dev/null +++ b/lib/experimental/connection-sources/index.php @@ -0,0 +1,15 @@ + '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 ); + }, +); diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index cec1eafcf94fa0..3233624557a5cb 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -90,6 +90,10 @@ function gutenberg_enable_experiments() { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalConnections = true', 'before' ); + } + if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' ); } diff --git a/lib/experiments-page.php b/lib/experiments-page.php index d10be06feef19a..74b2ad01672f71 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -115,6 +115,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-custom-fields', + __( 'Connections', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test Connections', 'gutenberg' ), + 'id' => 'gutenberg-connections', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js new file mode 100644 index 00000000000000..dbc8c3ec2c089f --- /dev/null +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -0,0 +1,139 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { PanelBody, TextControl } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { hasBlockSupport } from '@wordpress/blocks'; +import { createHigherOrderComponent } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { InspectorControls } from '../components'; +import { useBlockEditingMode } from '../components/block-editing-mode'; + +/** + * Filters registered block settings, extending attributes to include `connections`. + * + * @param {Object} settings Original block settings. + * + * @return {Object} Filtered block settings. + */ +function addAttribute( settings ) { + if ( hasBlockSupport( settings, '__experimentalConnections', true ) ) { + // Gracefully handle if settings.attributes.connections is undefined. + settings.attributes = { + ...settings.attributes, + connections: { + type: 'object', + }, + }; + } + + return settings; +} + +/** + * Override the default edit UI to include a new block inspector control for + * assigning a connection to blocks that has support for connections. + * Currently, only the `core/paragraph` block is supported and there is only a relation + * between paragraph content and a custom field. + * + * @param {WPComponent} BlockEdit Original component. + * + * @return {WPComponent} Wrapped component. + */ +const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { + return ( props ) => { + const blockEditingMode = useBlockEditingMode(); + const hasCustomFieldsSupport = hasBlockSupport( + props.name, + '__experimentalConnections', + false + ); + + // 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 ) ) { + return ; + } + + // If the block is a paragraph or image block, we need to know which + // attribute to use for the connection. Only the `content` attribute + // of the paragraph block and the `url` attribute of the image block are supported. + let attributeName; + if ( props.name === 'core/paragraph' ) attributeName = 'content'; + if ( props.name === 'core/image' ) attributeName = 'url'; + + if ( hasCustomFieldsSupport && props.isSelected ) { + return ( + <> + + { blockEditingMode === 'default' && ( + + + { + 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 + ), + } ); + } + } } + /> + + + ) } + + ); + } + + return ; + }; +}, 'withInspectorControl' ); + +if ( window.__experimentalConnections ) { + addFilter( + 'blocks.registerBlockType', + 'core/connections/attribute', + addAttribute + ); + addFilter( + 'editor.BlockEdit', + 'core/connections/with-inspector-control', + withInspectorControl + ); +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index a66aa0a73ed411..6834d859d25453 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -21,6 +21,7 @@ import './content-lock-ui'; import './metadata'; import './metadata-name'; import './behaviors'; +import './custom-fields'; export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 7e13b13dc4feb9..85f56f4a838f50 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -7,6 +7,7 @@ "description": "Start with the basic building block of all narrative.", "keywords": [ "text" ], "textdomain": "default", + "usesContext": [ "postId" ], "attributes": { "align": { "type": "string" @@ -41,6 +42,7 @@ "text": true } }, + "__experimentalConnections": true, "spacing": { "margin": true, "padding": true,