WooCommerce Code Reference

ProductGalleryLargeImage.php

Source code

<?php
declare(strict_types=1);

namespace Automattic\WooCommerce\Blocks\BlockTypes;

use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use WP_Block;

/**
 * ProductGalleryLargeImage class.
 */
class ProductGalleryLargeImage extends AbstractBlock {

	use EnableBlockJsonAssetsTrait;

	/**
	 * Block name. Block has been initially created as Large Image but has been renamed
	 * to more generic name.
	 *
	 * @var string
	 */
	protected $block_name = 'product-gallery-large-image';

	/**
	 *  Register the context
	 *
	 * @return string[]
	 */
	protected function get_block_type_uses_context() {
		return [ 'postId', 'hoverZoom', 'fullScreenOnClick' ];
	}

	/**
	 * Initialize this block type.
	 *
	 * - Hook into WP lifecycle.
	 * - Register the block with WordPress.
	 * - Hook into pre_render_block to update the query.
	 */
	protected function initialize() {
		add_filter( 'block_type_metadata_settings', array( $this, 'add_block_type_metadata_settings' ), 10, 2 );
		parent::initialize();
	}

	/**
	 * Enqueue frontend assets for this block, just in time for rendering.
	 *
	 * @param array    $attributes  Any attributes that currently are available from the block.
	 * @param string   $content    The block content.
	 * @param WP_Block $block    The block object.
	 */
	protected function enqueue_assets( array $attributes, $content, $block ) {
		if ( ! empty( $block->context['hoverZoom'] ) || ! empty( $block->context['fullScreenOnClick'] ) ) {
			parent::enqueue_assets( $attributes, $content, $block );
		}
	}

	/**
	 * 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'];

		if ( ! isset( $post_id ) ) {
			return '';
		}

		global $product;

		$previous_product = $product;
		$product          = wc_get_product( $post_id );
		if ( ! $product instanceof \WC_Product ) {
			$product = $previous_product;

			return '';
		}

		$images_html       = '';
		$inner_blocks_html = '';

		foreach ( $block->inner_blocks as $inner_block ) {
			if ( 'woocommerce/product-image' === $inner_block->name ) {
				// Product Image requires special handling because we need to render it once for each media item.
				$images_html .= $this->get_main_media_html( $block->context, $product, $inner_block );
			} else {
				// For Next/Previous Buttons block, check if we have more than one media item, otherwise don't render it.
				if ( 'woocommerce/product-gallery-large-image-next-previous' === $inner_block->name ) {
					$product_gallery_media_count = ProductGalleryUtils::get_product_gallery_media_count( $product );
					if ( $product_gallery_media_count <= 1 ) {
						continue;
					}
				}

				// Render all the inner blocks once each.
				$inner_block_html = (
					new WP_Block(
						$inner_block->parsed_block,
						array_merge(
							(array) $block->context,
							array( 'iapi/provider' => 'woocommerce/product-gallery' )
						),
					)
				)->render( array( 'dynamic' => true ) );

				$inner_blocks_html .= $inner_block_html;
			}
		}

		ob_start();
		?>
			<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image">
				<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				<?php echo $images_html; ?>
				<div class="wc-block-product-gallery-large-image__inner-blocks">
					<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					<?php echo $inner_blocks_html; ?>
				</div>
			</div>
		<?php
		$html = ob_get_clean();

		return $html;
	}

	/**
	 * Update the single image html.
	 *
	 * @param string $image_html The image html.
	 * @param array  $context The block context.
	 * @param int    $index The index of the image.
	 * @return string
	 */
	private function update_single_image( $image_html, $context, $index ) {
		$p = $this->get_product_image_processor( $image_html );

		// Bail out early if we don't find any image.
		if ( ! $p->next_tag( array( 'tag_name' => 'img' ) ) ) {
			return $image_html;
		}

		$p->set_attribute( 'tabindex', '-1' );
		$p->set_attribute( 'draggable', 'false' );
		$p->set_attribute( 'data-wp-watch', 'callbacks.toggleImageVisibility' );
		$p->set_attribute( 'data-wp-on--click', 'actions.onViewerClick' );
		$p->set_attribute( 'data-wp-on--touchstart', 'actions.onTouchStart' );
		$p->set_attribute( 'data-wp-on--touchmove', 'actions.onTouchMove' );
		$p->set_attribute( 'data-wp-on--touchend', 'actions.onTouchEnd' );

		if ( 0 === $index ) {
			$p->set_attribute( 'fetchpriority', 'high' );
			$p->set_attribute( 'loading', 'eager' );
		} else {
			$p->set_attribute( 'fetchpriority', 'low' );
			$p->set_attribute( 'loading', 'lazy' );
		}

		$img_classes = 'wc-block-woocommerce-product-gallery-large-image__image';

		if ( ! empty( $context['fullScreenOnClick'] ) ) {
			$img_classes .= ' wc-block-woocommerce-product-gallery-large-image__image--full-screen-on-click';

			$p->set_attribute( 'data-wp-on--click', 'actions.openDialog' );
		}
		if ( ! empty( $context['hoverZoom'] ) ) {
			$img_classes .= ' wc-block-woocommerce-product-gallery-large-image__image--hoverZoom';

			$p->set_attribute( 'data-wp-on--mousemove', 'actions.startZoom' );
			$p->set_attribute( 'data-wp-on--mouseleave', 'actions.resetZoom' );
		}

		$p->add_class( $img_classes );

		return $p->get_updated_html();
	}

	/**
	 * Get the main media HTML.
	 *
	 * @param array       $context The block context.
	 * @param \WC_Product $product The product object.
	 * @param WP_Block    $inner_block The inner block object.
	 * @return string
	 */
	private function get_main_media_html( $context, $product, $inner_block ) {
		$media_data = ProductGalleryUtils::get_product_gallery_media_data( $product, 'woocommerce_single' );

		ob_start();
		?>
			<ul
				class="wc-block-product-gallery-large-image__container"
				data-wp-interactive="woocommerce/product-gallery"
				data-wp-on--keydown="actions.onViewerImageKeyDown"
				aria-label="<?php esc_attr_e( 'Product gallery', 'woocommerce' ); ?>"
				tabindex="0"
				aria-roledescription="carousel"
			>
				<?php foreach ( $media_data as $index => $media ) : ?>
					<li
						class="wc-block-product-gallery-large-image__wrapper"
					>
						<?php
						if ( 'video' === ( $media['media_type'] ?? '' ) ) {
							echo $this->get_video_html( $media, $context, $inner_block ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
							continue;
						}

						$image_html = (
							new WP_Block(
								$inner_block->parsed_block,
								array_merge( $context, array( 'imageId' => $media['id'] ) )
							)
						)->render( array( 'dynamic' => true ) );

						// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						echo $this->update_single_image( $image_html, $context, $index );
						?>
					</li>
				<?php endforeach; ?>
			</ul>
		<?php
		$template = ob_get_clean();

		return wp_interactivity_process_directives( $template );
	}

	/**
	 * Get a Product Image processor prepared for gallery updates.
	 *
	 * @param string $image_html The image html.
	 * @return \WP_HTML_Tag_Processor
	 */
	private function get_product_image_processor( $image_html ) {
		$p = new \WP_HTML_Tag_Processor( $image_html );

		if ( $p->next_tag( array( 'tag_name' => 'a' ) ) ) {
			$p->remove_attribute( 'onclick' );
			$p->remove_attribute( 'style' );
			$p->set_attribute( 'tabindex', '-1' );
		} else {
			$p = new \WP_HTML_Tag_Processor( $image_html );
		}

		return $p;
	}

	/**
	 * Get video HTML for the product gallery large image area.
	 *
	 * @param array    $media       Video media data.
	 * @param array    $context     The block context.
	 * @param WP_Block $inner_block The inner block object.
	 * @return string
	 */
	private function get_video_html( $media, $context, $inner_block ) {
		$image_html = (
			new WP_Block(
				$inner_block->parsed_block,
				$context
			)
		)->render( array( 'dynamic' => true ) );

		return $this->replace_product_image_with_video( $image_html, $media, $context );
	}

	/**
	 * Replace Product Image markup with product gallery video markup.
	 *
	 * @param string $image_html Product Image HTML.
	 * @param array  $media      Video media data.
	 * @param array  $context    The block context.
	 * @return string
	 */
	private function replace_product_image_with_video( $image_html, $media, $context ) {
		$p = $this->get_product_image_processor( $image_html );

		if ( ! $p->next_tag( array( 'tag_name' => 'img' ) ) ) {
			return $image_html;
		}

		$attrs = ProductGalleryUtils::get_video_attributes( $media, 'gallery' );

		if ( empty( $attrs ) ) {
			return $image_html;
		}

		$image_style   = $p->get_attribute( 'style' );
		$video_classes = 'wc-block-woocommerce-product-gallery-large-image__image ' .
			'wc-block-woocommerce-product-gallery-large-image__video';

		if ( ! empty( $context['fullScreenOnClick'] ) ) {
			$video_classes             .= ' wc-block-woocommerce-product-gallery-large-image__image--full-screen-on-click';
			$attrs['data-wp-on--click'] = 'actions.openDialog';
		}

		$attrs = array_merge(
			$attrs,
			array(
				'class'                  => $video_classes,
				'data-wp-on--touchend'   => 'actions.onTouchEnd',
				'data-wp-on--touchmove'  => 'actions.onTouchMove',
				'data-wp-on--touchstart' => 'actions.onTouchStart',
				'draggable'              => 'false',
				'tabindex'               => '-1',
			)
		);

		if ( is_string( $image_style ) && '' !== $image_style ) {
			$attrs['style'] = $image_style;
		}

		$placeholder_attribute = 'data-wc-product-gallery-video-placeholder';

		if ( ! $p->set_attribute( $placeholder_attribute, '1' ) ) {
			return $image_html;
		}

		$updated_html      = $p->get_updated_html();
		$video_html        = ProductGalleryUtils::get_video_html( $attrs );
		$pattern           = sprintf(
			'/<img\b(?=[^>]*\s%s(?:\s*=\s*(?:"1"|\'1\'|1))?)[^>]*>/i',
			preg_quote( $placeholder_attribute, '/' )
		);
		$replacement_count = 0;
		$video_markup      = preg_replace_callback(
			$pattern,
			static function () use ( $video_html ) {
				return $video_html;
			},
			$updated_html,
			1,
			$replacement_count
		);

		return is_string( $video_markup ) && $replacement_count
			? $video_markup
			: $image_html;
	}

	/**
	 * Disable the editor style handle for this block type.
	 *
	 * @return null
	 */
	protected function get_block_type_editor_style() {
		return null;
	}

	/**
	 * Viewer renders inner blocks manually so we need to skip default
	 * rendering routine for its inner blocks
	 *
	 * @param array $settings Array of determined settings for registering a block type.
	 * @param array $metadata Metadata provided for registering a block type.
	 * @return array
	 */
	public function add_block_type_metadata_settings( $settings, $metadata ) {
		if ( ! empty( $metadata['name'] ) && 'woocommerce/product-gallery-large-image' === $metadata['name'] ) {
			$settings['skip_inner_blocks'] = true;
		}
		return $settings;
	}
}