ProductGallery.php
<?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Internal\ProductGallery\ProductMediaGallery;
/**
* ProductGallery class.
*/
class ProductGallery extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery';
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'postId' ];
}
/**
* Return the dialog content.
*
* @param array $media_items An array of all media items of the product.
* @return string
*/
protected function render_dialog( $media_items ) {
ob_start();
?>
<dialog
data-wp-bind--open="context.isDialogOpen"
data-wp-bind--inert="!context.isDialogOpen"
data-wp-on--close="actions.closeDialog"
data-wp-on--keydown="actions.onDialogKeyDown"
data-wp-watch="callbacks.dialogStateChange"
class="wc-block-product-gallery-dialog"
role="dialog"
aria-modal="true"
aria-label="Product Gallery">
<div class="wc-block-product-gallery-dialog__header">
<button class="wc-block-product-gallery-dialog__close-button" data-wp-on--click="actions.closeDialog" aria-label="<?php echo esc_attr__( 'Close dialog', 'woocommerce' ); ?>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
<path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path>
</svg>
</button>
</div>
<div class="wc-block-product-gallery-dialog__content">
<?php foreach ( $media_items as $index => $media ) : ?>
<?php if ( 'video' === ( $media['media_type'] ?? '' ) ) : ?>
<?php echo $this->get_dialog_video_html( $media ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php else : ?>
<img
data-image-id="<?php echo esc_attr( $media['id'] ); ?>"
data-wp-watch="callbacks.toggleImageVisibility"
src="<?php echo esc_url( $media['src'] ); ?>"
srcset="<?php echo esc_attr( $media['srcset'] ); ?>"
sizes="<?php echo esc_attr( $media['sizes'] ); ?>"
decoding="async"
alt="<?php echo esc_attr( $media['alt'] ); ?>" />
<?php endif; ?>
<?php endforeach; ?>
</div>
</dialog>
<?php
return ob_get_clean();
}
/**
* Get video HTML for the product gallery dialog.
*
* @param array $media Video media data.
* @return string
*/
private function get_dialog_video_html( $media ) {
return ProductGalleryUtils::get_video_html(
ProductGalleryUtils::get_video_attributes( $media, 'dialog' )
);
}
/**
* Inject dialog into the product gallery HTML.
*
* @param string $gallery_html The gallery HTML.
* @param string $dialog_html The dialog HTML.
*
* @return string
*/
protected function inject_dialog( $gallery_html, $dialog_html ) {
// Find the position of the last </div>.
$pos = strrpos( $gallery_html, '</div>' );
if ( false !== $pos ) {
// Inject the dialog_html at the correct position.
$html = substr_replace( $gallery_html, $dialog_html, $pos, 0 );
return $html;
}
return $gallery_html;
}
/**
* Find the Product Image block attributes used by the gallery.
*
* @param iterable<\WP_Block> $inner_blocks Inner blocks to search.
* @return array|null
*/
private function get_product_image_attributes( $inner_blocks ) {
foreach ( $inner_blocks as $inner_block ) {
if ( 'woocommerce/product-image' === $inner_block->name ) {
return $inner_block->parsed_block['attrs'] ?? array();
}
$found_attributes = $this->get_product_image_attributes( $inner_block->inner_blocks );
if ( null !== $found_attributes ) {
return $found_attributes;
}
}
return null;
}
/**
* Get the store thumbnail aspect ratio from WooCommerce Customizer settings.
*
* @return string|null CSS aspect ratio value, or null when uncropped.
*/
private function get_store_thumbnail_aspect_ratio() {
$cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' );
if ( 'uncropped' === $cropping ) {
return null;
}
if ( 'custom' === $cropping ) {
$width = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) );
$height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) );
return $width . '/' . $height;
}
return str_replace( ':', '/', $cropping );
}
/**
* Resolve the gallery aspect ratio from the nested Product Image block.
*
* @param \WP_Block $block The Product Gallery block.
* @return string|null CSS aspect ratio value, or null when no ratio should be applied.
*/
private function resolve_product_image_aspect_ratio( $block ) {
$attributes = $this->get_product_image_attributes( $block->inner_blocks );
if ( empty( $attributes ) ) {
return null;
}
if ( ! empty( $attributes['style']['dimensions']['aspectRatio'] ) ) {
return $attributes['style']['dimensions']['aspectRatio'];
}
if ( ! empty( $attributes['aspectRatio'] ) ) {
return $attributes['aspectRatio'];
}
$image_sizing = $attributes['imageSizing'] ?? 'single';
if ( 'thumbnail' === $image_sizing || 'cropped' === $image_sizing ) {
return $this->get_store_thumbnail_aspect_ratio();
}
return null;
}
/**
* Parse an aspect ratio into CSS variable values.
*
* @param string|null $aspect_ratio CSS aspect ratio value.
* @return array
*/
private function parse_aspect_ratio( $aspect_ratio ) {
$default_aspect_ratio = array(
'width' => '1',
'height' => '1',
);
if ( empty( $aspect_ratio ) || ! is_string( $aspect_ratio ) ) {
return $default_aspect_ratio;
}
$parts = array_map( 'trim', explode( '/', $aspect_ratio ) );
if ( count( $parts ) > 2 ) {
return $default_aspect_ratio;
}
$width = (float) $parts[0];
$height = isset( $parts[1] ) ? (float) $parts[1] : $width;
if ( $width <= 0 || $height <= 0 ) {
return $default_aspect_ratio;
}
return array(
'width' => (string) $width,
'height' => (string) $height,
);
}
/**
* Get the gallery aspect ratio CSS variables.
*
* @param \WP_Block $block The Product Gallery block.
* @return string
*/
private function get_aspect_ratio_style( $block ) {
$aspect_ratio = $this->parse_aspect_ratio(
$this->resolve_product_image_aspect_ratio( $block )
);
return sprintf(
'--wc-block-product-gallery-large-image-ratio-width:%1$s;' .
'--wc-block-product-gallery-large-image-ratio-height:%2$s;',
$aspect_ratio['width'],
$aspect_ratio['height']
);
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param \WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
return '';
}
$media_items = ProductGalleryUtils::get_all_media_items( $product );
$default_media_ids = ProductGalleryUtils::get_media_ids(
ProductMediaGallery::get_product_media_gallery_items_for_display( $product )
);
$number_of_media = count( $default_media_ids );
$classname = StyleAttributesUtils::get_classes_by_attributes( $attributes, array( 'extra_classes' ) );
$initial_media_id = $number_of_media > 0 ? $default_media_ids[0] : -1;
$classname_single_image = $number_of_media < 2 ? 'is-single-product-gallery-image' : '';
$product_id = strval( $product->get_id() );
$fullsize_media_data = ProductGalleryUtils::get_media_src_data( $media_items, 'full', $product->get_title() );
$gallery_with_dialog = $this->inject_dialog( $content, $this->render_dialog( $fullsize_media_data ) );
$p = new \WP_HTML_Tag_Processor( $gallery_with_dialog );
$html = $gallery_with_dialog;
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wp-interactive', $this->get_full_block_name() );
$p->set_attribute(
'data-wp-context',
wp_json_encode(
array(
'imageData' => $default_media_ids,
'isDialogOpen' => false,
'isDragging' => false,
'touchStartX' => 0,
'touchCurrentX' => 0,
'productId' => $product_id,
'selectedImageId' => $initial_media_id,
'thumbnailsOverflow' => [
'top' => false,
'bottom' => false,
'left' => false,
'right' => false,
],
// Next/Previous Buttons block context.
'hideNextPreviousButtons' => $number_of_media <= 1,
'isDisabledPrevious' => true,
'isDisabledNext' => $number_of_media <= 1,
'ariaLabelPrevious' => __( 'Previous image', 'woocommerce' ),
'ariaLabelNext' => __( 'Next image', 'woocommerce' ),
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
if ( $product->is_type( ProductType::VARIABLE ) ) {
$formatted_variations_data = ProductGalleryUtils::get_product_variation_gallery_data( $product );
if ( ! empty( $formatted_variations_data ) ) {
wp_interactivity_config(
'woocommerce',
array(
'products' => array(
$product->get_id() => array(
'image_id' => $initial_media_id,
'image_ids' => $default_media_ids,
'variations' => $formatted_variations_data,
),
),
)
);
// Support legacy Add to Cart with Options block.
$p->set_attribute( 'data-wp-init--watch-changes-on-add-to-cart-form', 'callbacks.watchForChangesOnAddToCartForm' );
// Support blockified Add to Cart + Options block.
$p->set_attribute( 'data-wp-watch', 'callbacks.listenToProductDataChanges' );
}
}
$p->add_class( $classname );
$p->add_class( $classname_single_image );
$style = $p->get_attribute( 'style' );
$style = is_string( $style ) ? trim( $style ) : '';
if ( '' !== $style && ';' !== substr( $style, -1 ) ) {
$style .= ';';
}
$p->set_attribute( 'style', $style . $this->get_aspect_ratio_style( $block ) );
$html = $p->get_updated_html();
}
return $html;
}
}