Automattic\WooCommerce\Blocks\BlockTypes

ProductFilters{} │ WC 1.0└─ AbstractBlock

ProductFilters class.

Usage

$ProductFilters = new ProductFilters();
// use class methods

Methods

  1. protected enqueue_data( array $attributes = array() )
  2. private generate_navigation_id( $block )
  3. protected get_block_type_editor_style()
  4. protected get_block_type_script( $key = null )
  5. protected get_block_type_style()
  6. protected get_block_type_uses_context()
  7. private get_canonical_url_no_pagination( $filter_params )
  8. private get_css_variables( $attributes )
  9. private get_filter_params( $query_id )
  10. private get_overlay_breakpoint()
  11. private get_overlay_mode( $attributes )
  12. private get_responsive_styles()
  13. protected render( $attributes, $content, $block )

ProductFilters{} code WC 11.2.1

<?php
class ProductFilters extends AbstractBlock {

	/**
	 * Block name.
	 *
	 * @var string
	 */
	protected $block_name = 'product-filters';

	/**
	 * Register the context.
	 *
	 * @return string[]
	 */
	protected function get_block_type_uses_context() {
		return array( 'postId', 'query', 'queryId', 'forcePageReload' );
	}

	/**
	 * Extra data passed through from server to client for block.
	 *
	 * @param array $attributes  Any attributes that currently are available from the block.
	 *                           Note, this will be empty in the editor context when the block is
	 *                           not in the post content on editor load.
	 */
	protected function enqueue_data( array $attributes = array() ) {
		parent::enqueue_data( $attributes );

		$inline_style_handle = BlockEditorUnifiedAssets::is_enabled() && is_admin()
			? 'wc-block-library-style'
			: generate_block_asset_handle( $this->get_full_block_name(), 'style' );

		wp_add_inline_style(
			$inline_style_handle,
			$this->get_responsive_styles()
		);

		if ( is_admin() ) {
			$this->asset_data_registry->add( 'globalStylesColors', wp_get_global_styles( array( 'color' ) ) );
		}

		BlocksSharedState::load_store_config( 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WooCommerce' );

		// Classic themes do not support client-side navigation on product
		// archive pages, so disable it globally for the Interactivity Router.
		$is_product_archive = is_shop() || is_product_taxonomy() || ( is_search() && 'product' === get_post_type() );
		if ( ! wp_is_block_theme() && $is_product_archive ) {
			wp_interactivity_config( 'core/router', array( 'clientNavigationDisabled' => true ) );
		}
	}

	/**
	 * Resolve the responsive overlay breakpoint.
	 *
	 * @return string Responsive overlay breakpoint.
	 */
	private function get_overlay_breakpoint() {
		if ( function_exists( 'gutenberg_get_global_settings' ) ) {
			$viewport_settings = gutenberg_get_global_settings( array( 'viewport' ) );
		} else {
			$viewport_settings = wp_get_global_settings( array( 'viewport' ) );
		}

		if ( is_array( $viewport_settings ) ) {
			foreach ( array( 'tablet', 'mobile' ) as $viewport ) {
				if ( isset( $viewport_settings[ $viewport ] ) && is_string( $viewport_settings[ $viewport ] ) ) {
					return $viewport_settings[ $viewport ];
				}
			}
		}

		return '782px';
	}

	/**
	 * Generate viewport-aware Product Filters styles.
	 *
	 * @return string Responsive CSS.
	 */
	private function get_responsive_styles() {
		$overlay_breakpoint = $this->get_overlay_breakpoint();

		return <<<CSS
@media (width > {$overlay_breakpoint}) {
	:where(.wc-block-product-filters).is-mobile-overlay {
		display: flex;
	}

	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-header,
	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-footer,
	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__open-overlay {
		display: none;
	}

	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay {
		position: relative;
		pointer-events: auto;
		inset: 0;
		z-index: auto;
		background: inherit;
		color: inherit;
		transition: none;
		flex-grow: 1;
	}

	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-wrapper {
		width: auto;
		height: auto;
		background: inherit;
		color: inherit;
	}

	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-dialog {
		position: relative;
		width: auto;
		transform: none;
		transition: none;
		background: inherit;
		color: inherit;
	}

	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-content {
		padding: 0;
		overflow: visible;
		flex-grow: 1;
		background: inherit;
		color: inherit;
	}
}
CSS;
	}

	/**
	 * Resolve the canonical overlay mode with legacy drawer compatibility.
	 *
	 * @param array $attributes Block attributes.
	 * @return string Overlay mode.
	 */
	private function get_overlay_mode( $attributes ) {
		$overlay_mode = $attributes['overlayMode'] ?? null;
		if ( in_array( $overlay_mode, array( 'off', 'mobile', 'always' ), true ) ) {
			return $overlay_mode;
		}

		return isset( $attributes['showFilterDrawer'] ) && false === $attributes['showFilterDrawer'] ? 'off' : 'mobile';
	}

	/**
	 * 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 ) {
		if ( ! $block instanceof WP_Block ) {
			return $content;
		}

		wp_enqueue_script( 'wc-settings' );

		$query_id      = $block->context['queryId'] ?? 0;
		$filter_params = $this->get_filter_params( $query_id );

		wp_interactivity_config( $this->get_full_block_name(), [ 'canonicalUrl' => $this->get_canonical_url_no_pagination( $filter_params ) ] );

		/**
		 * Filter hook to modify the selected filter items.
		 *
		 * @since 9.7.0
		 */
		$active_filters = apply_filters( 'woocommerce_blocks_product_filters_selected_items', array(), $filter_params );

		usort(
			$active_filters,
			function ( $a, $b ) {
				return strnatcmp( $a['activeLabel'], $b['activeLabel'] );
			}
		);

		$block_context         = array_merge(
			$block->context,
			array(
				'filterParams'  => $filter_params,
				'activeFilters' => $active_filters,
			),
		);
		$inner_blocks          = array_reduce(
			$block->parsed_block['innerBlocks'],
			function ( $carry, $parsed_block ) use ( $block_context ) {
				$carry .= ( new \WP_Block( $parsed_block, $block_context ) )->render();
				return $carry;
			},
			''
		);
		$interactivity_context = array(
			'params'          => $filter_params,
			'activeFilters'   => $active_filters,
			// Null when not a descendant of a Product Collection block, so the
			// frontend can fall back to the global interactivity config.
			'forcePageReload' => isset( $block->context['forcePageReload'] ) ? (bool) $block->context['forcePageReload'] : null,
		);

		$overlay_mode     = $this->get_overlay_mode( $attributes );
		$has_overlay      = 'off' !== $overlay_mode;
		$overlay_position = isset( $attributes['overlayPosition'] ) && 'right' === $attributes['overlayPosition'] ? 'right' : 'left';
		$wrapper_classes  = array( 'wc-block-product-filters' );
		if ( ! $has_overlay ) {
			$wrapper_classes[] = 'is-filter-drawer-disabled';
		}
		if ( 'mobile' === $overlay_mode ) {
			$wrapper_classes[] = 'is-mobile-overlay';
		}
		if ( $has_overlay && 'right' === $overlay_position ) {
			$wrapper_classes[] = 'is-overlay-right';
		}

		$wrapper_attributes = array(
			'class'                         => implode( ' ', $wrapper_classes ),
			'data-wp-interactive'           => $this->get_full_block_name(),
			'data-wp-init--colors'          => 'callbacks.initColors',
			'data-wp-watch--active-filters' => 'callbacks.syncActiveFiltersWithServer',
			'data-wp-context'               => (string) wp_json_encode( $interactivity_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
			'style'                         => $this->get_css_variables( $attributes ),
		);

		if ( $has_overlay ) {
			$wrapper_attributes['data-wp-watch--scrolling']         = 'callbacks.scrollLimit';
			$wrapper_attributes['data-wp-on--keyup']                = 'actions.closeOverlayOnEscape';
			$wrapper_attributes['data-wp-class--is-overlay-opened'] = 'context.isOverlayOpened';
		}

		// Remove this conditional once the fix is released in WP. https://github.com/woocommerce/gutenberg/pull/4.
		if ( ! isset( $block->context['productCollectionLocation'] ) ) {
			$wrapper_attributes['data-wp-router-region'] = $this->generate_navigation_id( $block );
		}

		ob_start();
		?>
		<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
			<?php if ( $has_overlay ) : ?>
				<button
					type="button"
					class="wc-block-product-filters__open-overlay"
					data-wp-on--click="actions.openOverlay"
				>
					<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
						<path d="M10 17.5H14V16H10V17.5ZM6 6V7.5H18V6H6ZM8 12.5H16V11H8V12.5Z" fill="currentColor"/>
					</svg>
					<span><?php echo esc_html__( 'Filter products', 'woocommerce' ); ?></span>
				</button>
				<div class="wc-block-product-filters__overlay">
					<div class="wc-block-product-filters__overlay-wrapper" data-wp-on--click="actions.closeOverlayOnBackdrop">
						<div
							class="wc-block-product-filters__overlay-dialog"
							role="dialog"
							aria-label="<?php echo esc_html__( 'Product Filters', 'woocommerce' ); ?>"
						>
							<header class="wc-block-product-filters__overlay-header">
								<button
									type="button"
									class="wc-block-product-filters__close-overlay"
									data-wp-on--click="actions.closeOverlay"
									title="<?php echo esc_attr( __( 'Close', '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>
							</header>
							<div class="wc-block-product-filters__overlay-content">
								<?php echo $inner_blocks; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
							</div>
							<footer
								class="wc-block-product-filters__overlay-footer"
							>
								<button
									type="button"
									class="wc-block-product-filters__apply wp-element-button"
									data-wp-interactive="<?php echo esc_attr( $this->get_full_block_name() ); ?>"
									data-wp-on--click="actions.closeOverlay"
								>
									<span><?php echo esc_html__( 'Apply', 'woocommerce' ); ?></span>
								</button>
							</footer>
						</div>
					</div>
				</div>
			<?php else : ?>
				<div class="wc-block-product-filters__content">
					<?php echo $inner_blocks; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</div>
			<?php endif; ?>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * Get CSS custom properties from block attributes.
	 *
	 * @param array $attributes Block attributes.
	 * @return string CSS custom properties string.
	 */
	private function get_css_variables( $attributes ) {
		$styles = array();

		$bg = StyleAttributesUtils::get_background_color_class_and_style( $attributes );
		if ( ! empty( $bg['value'] ) ) {
			$styles[] = sprintf( '--wc-product-filters-background-color: %s', $bg['value'] );
		}

		$text = StyleAttributesUtils::get_text_color_class_and_style( $attributes );
		if ( ! empty( $text['value'] ) ) {
			$styles[] = sprintf( '--wc-product-filters-text-color: %s', $text['value'] );
		}

		$block_gap = $attributes['style']['spacing']['blockGap'] ?? '';
		if ( is_string( $block_gap ) && '' !== $block_gap ) {
			$styles[] = sprintf( '--wc-product-filter-block-spacing: %s', StyleAttributesUtils::get_spacing_value( $block_gap ) );
		}

		return $styles ? implode( ';', $styles ) . ';' : '';
	}

	/**
	 * Generate a unique navigation ID for the block.
	 *
	 * @param mixed $block - Block instance.
	 * @return string - Unique navigation ID.
	 */
	private function generate_navigation_id( $block ) {
		return sprintf(
			'wc-product-filters-%s',
			md5(
				wp_json_encode(
					array(
						'attrs'       => $block->parsed_block['attrs'] ?? array(),
						'innerBlocks' => $block->parsed_block['innerBlocks'] ?? array(),
					)
				)
			)
		);
	}

	/**
	 * Parse the filter parameters from the URL.
	 * For now we only get the global query params from the URL. In the future,
	 * we should get the query params based on $query_id.
	 *
	 * @param int $query_id Query ID.
	 * @return array Parsed filter params.
	 */
	private function get_filter_params( $query_id ) {
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';

		$parsed_url = wp_parse_url( esc_url_raw( $request_uri ) );

		if ( empty( $parsed_url['query'] ) ) {
			return array();
		}

		parse_str( $parsed_url['query'], $url_query_params );

		$filter_param_keys = wc_get_container()->get( Params::class )->get_param_keys();

		return array_filter(
			$url_query_params,
			function ( $key ) use ( $filter_param_keys ) {
				return in_array( $key, $filter_param_keys, true );
			},
			ARRAY_FILTER_USE_KEY
		);
	}

	/**
	 * Disable the style handle for this block type. We use block.json to load the style.
	 *
	 * @return null
	 */
	protected function get_block_type_style() {
		return null;
	}

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

	/**
	 * Disable the script handle for this block type. We use block.json to load the script.
	 *
	 * @param string|null $key The key of the script to get.
	 * @return null
	 */
	protected function get_block_type_script( $key = null ) {
		return null;
	}

	/**
	 * Get the canonical URL without pagination.
	 *
	 * @param array $filter_params Filter parameters.
	 * @return string Canonical URL without pagination.
	 */
	private function get_canonical_url_no_pagination( $filter_params ) {
		$canonical_url_no_pagination = is_singular() ? get_permalink() : get_pagenum_link( 1 );
		$decoded_url                 = html_entity_decode( $canonical_url_no_pagination, ENT_QUOTES, get_bloginfo( 'charset' ) );
		$parsed_url                  = wp_parse_url( $decoded_url );

		// If there are active filters, $parsed_url['query'] is empty for page or post but not empty for archives.
		if ( empty( $filter_params ) || empty( $parsed_url['query'] ) ) {
			return $decoded_url;
		}

		foreach ( array_keys( $filter_params ) as $key ) {
			$parsed_url['query'] = remove_query_arg( $key, $parsed_url['query'] );
		}

		$url = '';

		if ( isset( $parsed_url['scheme'] ) ) {
			$url .= $parsed_url['scheme'] . '://';
		}

		if ( isset( $parsed_url['host'] ) ) {
			$url .= $parsed_url['host'];
		}

		if ( isset( $parsed_url['port'] ) ) {
			$url .= ':' . $parsed_url['port'];
		}

		if ( isset( $parsed_url['path'] ) ) {
			$url .= $parsed_url['path'];
		}

		if ( ! empty( $parsed_url['query'] ) ) {
			$url .= '?' . $parsed_url['query'];
		}

		if ( isset( $parsed_url['fragment'] ) ) {
			$url .= '#' . $parsed_url['fragment'];
		}

		return $url;
	}
}