diff --git a/lib/compat/wordpress-7.1/block-bindings.php b/lib/compat/wordpress-7.1/block-bindings.php
index e0f4aad4075962..42e3696ec57ed5 100644
--- a/lib/compat/wordpress-7.1/block-bindings.php
+++ b/lib/compat/wordpress-7.1/block-bindings.php
@@ -7,7 +7,7 @@
* @subpackage Block Bindings
*/
-// The following filter can be removed once the minimum required WordPress version is 7.1 or newer.
+// The following filters can be removed once the minimum required WordPress version is 7.1 or newer.
add_filter(
'block_bindings_supported_attributes',
function ( $attributes, $block_type ) {
@@ -20,6 +20,16 @@ function ( $attributes, $block_type ) {
2
);
+add_filter(
+ 'block_bindings_supported_attributes_core/media-text',
+ function ( $supported_attributes ) {
+ $supported_attributes[] = 'mediaUrl';
+ $supported_attributes[] = 'mediaAlt';
+ $supported_attributes[] = 'mediaId';
+ return $supported_attributes;
+ }
+);
+
/*
* On WordPress versions before 7.1, `WP_Block::replace_html()` lacks the
* inner-blocks fix (wordpress-develop#12113), so binding a List Item's content
diff --git a/packages/block-library/src/media-text/index.php b/packages/block-library/src/media-text/index.php
index fef37cdd8db565..48e689cb1d58d0 100644
--- a/packages/block-library/src/media-text/index.php
+++ b/packages/block-library/src/media-text/index.php
@@ -17,7 +17,97 @@
*/
function render_block_core_media_text( $attributes, $content ) {
if ( false === $attributes['useFeaturedImage'] ) {
- return $content;
+ $media_bindings = $attributes['metadata']['bindings'] ?? array();
+ if ( empty( $media_bindings['mediaUrl'] ) ) {
+ return $content;
+ }
+
+ /*
+ * The `mediaUrl` and `mediaAlt` attributes are bound to a block bindings
+ * source. `WP_Block::replace_html()` cannot match the block's descendant
+ * selectors (e.g. `figure img`), so update the media element markup here
+ * with the resolved values.
+ */
+ $media_url = $attributes['mediaUrl'] ?? null;
+ if ( '' === $media_url ) {
+ return $content;
+ }
+
+ $media_type = $attributes['mediaType'] ?? 'image';
+ $is_video = 'video' === $media_type;
+ $media_alt = $attributes['mediaAlt'] ?? '';
+ $has_media_on_right = 'right' === ( $attributes['mediaPosition'] ?? null );
+ $figure_query = array(
+ 'tag_name' => 'figure',
+ 'class_name' => 'wp-block-media-text__media',
+ );
+
+ /*
+ * Locate this block's media figure, accounting for `media-position: right`
+ * where the media column follows the content column. Returns a processor
+ * positioned on the figure, or null if no media figure is found.
+ */
+ $find_media_figure = static function ( $block_content ) use ( $has_media_on_right, $figure_query ) {
+ $processor = new WP_HTML_Tag_Processor( $block_content );
+ $found = false;
+ while ( $processor->next_tag( $figure_query ) ) {
+ $found = true;
+ $processor->set_bookmark( 'media_figure' );
+ if ( ! $has_media_on_right ) {
+ break;
+ }
+ }
+ if ( ! $found || ! $processor->seek( 'media_figure' ) ) {
+ return null;
+ }
+ return $processor;
+ };
+
+ $media_processor = $find_media_figure( $content );
+ if ( null === $media_processor ) {
+ return $content;
+ }
+
+ if ( $media_processor->next_tag( array( 'tag_name' => $is_video ? 'video' : 'img' ) ) ) {
+ // Update an existing media element in the saved markup.
+ $media_processor->set_attribute( 'src', $media_url );
+ if ( ! $is_video && isset( $media_bindings['mediaAlt'] ) ) {
+ $media_processor->set_attribute( 'alt', $media_alt );
+ }
+
+ return $media_processor->get_updated_html();
+ }
+
+ // The media figure is empty (e.g. the pattern was saved without an image),
+ // so build the media element from the bound attributes and insert it.
+ $media_processor->seek( 'media_figure' );
+ $unique_id = 'wp-block-media-text__media-' . wp_unique_id();
+ $media_processor->set_attribute( 'id', $unique_id );
+
+ $media_tag = $is_video
+ ? ''
+ : '';
+
+ $content = $media_processor->get_updated_html();
+ // Insert the media element right after the figure opening tag, then remove
+ // the temporary id used to target it.
+ $content = preg_replace(
+ '/()/',
+ '$1' . $media_tag,
+ $content
+ );
+
+ $cleanup_processor = new WP_HTML_Tag_Processor( $content );
+ if ( $cleanup_processor->next_tag(
+ array(
+ 'tag_name' => 'figure',
+ 'id' => $unique_id,
+ )
+ ) ) {
+ $cleanup_processor->remove_attribute( 'id' );
+ }
+
+ return $cleanup_processor->get_updated_html();
}
if ( in_the_loop() ) {
diff --git a/packages/block-library/src/media-text/media-container.js b/packages/block-library/src/media-text/media-container.js
index af0d3ec59a2e16..d78e0d820cf015 100644
--- a/packages/block-library/src/media-text/media-container.js
+++ b/packages/block-library/src/media-text/media-container.js
@@ -134,6 +134,11 @@ function MediaContainer( props, ref ) {
const { toggleSelection } = useDispatch( blockEditorStore );
if ( mediaUrl || featuredImageURL || useFeaturedImage ) {
+ // `mediaType` may be unset when the media comes from a block binding
+ // (e.g. a pattern override) rather than a media library selection.
+ // Default to rendering an image so the bound media is visible in the editor.
+ const mediaTypeToRender = mediaType || 'image';
+
const onResizeStart = () => {
toggleSelection( false );
};
@@ -207,7 +212,7 @@ function MediaContainer( props, ref ) {
toggleUseFeaturedImage={ toggleUseFeaturedImage }
useFeaturedImage={ useFeaturedImage }
/>
- { ( mediaTypeRenderers[ mediaType ] || noop )() }
+ { ( mediaTypeRenderers[ mediaTypeToRender ] || noop )() }
{ isTemporaryMedia && }
{ ! useFeaturedImage && }
{ ! featuredImageURL && useFeaturedImage && (
diff --git a/phpunit/block-bindings-test.php b/phpunit/block-bindings-test.php
index 99f5b901ac8813..2704aadc2c51a1 100644
--- a/phpunit/block-bindings-test.php
+++ b/phpunit/block-bindings-test.php
@@ -316,6 +316,133 @@ public function test_update_block_with_value_from_source_image_placeholder() {
);
}
+ /**
+ * Tests if the block content is updated with the value returned by the source
+ * for the Media & Text block, whose supported attributes are added via the
+ * `block_bindings_supported_attributes_core/media-text` filter.
+ *
+ * @covers ::register_block_bindings_source
+ */
+ public function test_update_block_with_value_from_source_media_text() {
+ $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) {
+ if ( 'mediaUrl' === $attribute_name ) {
+ return 'https://example.com/updated-image.jpg';
+ }
+ if ( 'mediaAlt' === $attribute_name ) {
+ return 'Updated alt text';
+ }
+ };
+
+ register_block_bindings_source(
+ self::SOURCE_NAME,
+ array(
+ 'label' => self::SOURCE_LABEL,
+ 'get_value_callback' => $get_value_callback,
+ )
+ );
+
+ $this->assertContains(
+ 'mediaUrl',
+ get_block_bindings_supported_attributes( 'core/media-text' ),
+ 'The mediaUrl attribute should be supported for block bindings.'
+ );
+ $this->assertContains(
+ 'mediaAlt',
+ get_block_bindings_supported_attributes( 'core/media-text' ),
+ 'The mediaAlt attribute should be supported for block bindings.'
+ );
+ $this->assertContains(
+ 'mediaId',
+ get_block_bindings_supported_attributes( 'core/media-text' ),
+ 'The mediaId attribute should be supported for block bindings.'
+ );
+
+ $block_content = <<
+
+
+HTML;
+ $parsed_blocks = parse_blocks( $block_content );
+ $block = new WP_Block( $parsed_blocks[0] );
+ $result = $block->render();
+
+ $this->assertSame(
+ 'https://example.com/updated-image.jpg',
+ $block->attributes['mediaUrl'],
+ "The 'mediaUrl' attribute should be updated with the value returned by the source."
+ );
+ $this->assertSame(
+ 'Updated alt text',
+ $block->attributes['mediaAlt'],
+ "The 'mediaAlt' attribute should be updated with the value returned by the source."
+ );
+ $this->assertStringContainsString(
+ '',
+ $result,
+ 'The block content should be updated with the value returned by the source.'
+ );
+ }
+
+ public function data_update_block_with_value_from_source_media_text_empty_figure() {
+ return array(
+ 'media on the left' => array(
+ 'left',
+ '
',
+ ),
+ 'media on the right' => array(
+ 'right',
+ '
',
+ ),
+ );
+ }
+
+ /**
+ * Tests that a Media & Text block saved without media renders the image
+ * bound via block bindings, for both media positions.
+ *
+ * @covers ::register_block_bindings_source
+ *
+ * @dataProvider data_update_block_with_value_from_source_media_text_empty_figure
+ */
+ public function test_update_block_with_value_from_source_media_text_empty_figure( $media_position, $block_markup ) {
+ $get_value_callback = function ( $source_args, $block_instance, $attribute_name ) {
+ if ( 'mediaUrl' === $attribute_name ) {
+ return 'https://example.com/updated-image.jpg';
+ }
+ if ( 'mediaAlt' === $attribute_name ) {
+ return 'Updated alt text';
+ }
+ };
+
+ register_block_bindings_source(
+ self::SOURCE_NAME,
+ array(
+ 'label' => self::SOURCE_LABEL,
+ 'get_value_callback' => $get_value_callback,
+ )
+ );
+
+ $block_content = <<
+{$block_markup}
+
+HTML;
+ $parsed_blocks = parse_blocks( $block_content );
+ $block = new WP_Block( $parsed_blocks[0] );
+ $result = $block->render();
+
+ $this->assertStringContainsString(
+ '',
+ $result,
+ 'The block should render the bound image in an empty media figure.'
+ );
+ $this->assertStringNotContainsString(
+ 'id="wp-block-media-text__media-',
+ $result,
+ 'The temporary figure id used to insert the media should be removed.'
+ );
+ }
+
/**
* Tests if the `__default` attribute is replaced with real attributes for
* pattern overrides.