plugin updates

This commit is contained in:
Tony Volpe
2024-11-15 13:53:04 -05:00
parent 1293d604ca
commit 0238f0c4ca
2009 changed files with 163492 additions and 89543 deletions

View File

@@ -8,6 +8,8 @@ use Automattic\Jetpack\Connection\Utils;
/**
* Class Configuration
*
* @internal
*/
class Configuration {

View File

@@ -10,6 +10,8 @@ use WpOrg\Requests\Requests;
/**
* Class Connection
*
* @internal
*/
class Connection {
const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion';

View File

@@ -10,6 +10,8 @@ use WP_Error;
* ContentProcessor class.
*
* Process images for content
*
* @internal
*/
class ContentProcessor {

View File

@@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* Patterns Dictionary class.
*
* @internal
*/
class PatternsDictionary {
/**

View File

@@ -6,6 +6,8 @@ use WP_Error;
/**
* Patterns Helper class.
*
* @internal
*/
class PatternsHelper {
/**

View File

@@ -7,6 +7,8 @@ use WP_Error;
/**
* Pattern Images class.
*
* @internal
*/
class UpdatePatterns {

View File

@@ -4,8 +4,11 @@ namespace Automattic\WooCommerce\Blocks\AIContent;
use Automattic\WooCommerce\Blocks\AI\Connection;
use WP_Error;
/**
* Pattern Images class.
*
* @internal
*/
class UpdateProducts {
@@ -471,11 +474,11 @@ class UpdateProducts {
/**
* Update the product with the new content.
*
* @param \WC_Product $product The product.
* @param int $product_image_id The product image ID.
* @param string $product_title The product title.
* @param string $product_description The product description.
* @param int $product_price The product price.
* @param \WC_Product $product The product.
* @param int|string|WP_Error $product_image_id The product image ID.
* @param string $product_title The product title.
* @param string $product_description The product description.
* @param int $product_price The product price.
*
* @return int|\WP_Error
*/

View File

@@ -41,6 +41,13 @@ class Api {
*/
private $script_data = null;
/**
* Tracks whether script_data was modified during the current request.
*
* @var boolean
*/
private $script_data_modified = false;
/**
* Stores the hash for the script data, made up of the site url, plugin version and package path.
*
@@ -171,6 +178,9 @@ class Api {
if ( is_null( $this->script_data ) || $this->disable_cache ) {
return;
}
if ( ! $this->script_data_modified ) {
return;
}
set_transient(
$this->script_data_transient_key,
wp_json_encode(
@@ -216,6 +226,7 @@ class Api {
'version' => ! empty( $asset['version'] ) ? $asset['version'] : $this->get_file_version( $relative_src ),
'dependencies' => ! empty( $asset['dependencies'] ) ? $asset['dependencies'] : [],
);
$this->script_data_modified = true;
}
// Return asset details as well as the requested dependencies array.

View File

@@ -80,8 +80,6 @@ class BlockPatterns {
$this->pattern_registry = $pattern_registry;
$this->ptk_patterns_store = $ptk_patterns_store;
$this->dictionary = PatternsHelper::get_patterns_dictionary();
add_action( 'init', array( $this, 'register_block_patterns' ) );
if ( Features::is_enabled( 'pattern-toolkit-full-composability' ) ) {
@@ -89,6 +87,19 @@ class BlockPatterns {
}
}
/**
* Returns the Patterns dictionary.
*
* @return array|WP_Error
*/
private function get_patterns_dictionary() {
if ( null === $this->dictionary ) {
$this->dictionary = PatternsHelper::get_patterns_dictionary();
}
return $this->dictionary;
}
/**
* Register block patterns from core.
*
@@ -123,7 +134,7 @@ class BlockPatterns {
foreach ( $files as $file ) {
$pattern_data = get_file_data( $file, $default_headers );
$this->pattern_registry->register_block_pattern( $file, $pattern_data, $this->dictionary );
$this->pattern_registry->register_block_pattern( $file, $pattern_data, $this->get_patterns_dictionary() );
}
}
@@ -139,11 +150,20 @@ class BlockPatterns {
return;
}
// The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
// cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
$has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
$patterns = $this->ptk_patterns_store->get_patterns();
if ( empty( $patterns ) ) {
wc_get_logger()->warning(
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
);
// By only logging when patterns are empty and no fetch is scheduled,
// we ensure that warnings are only generated in genuinely problematic situations,
// such as when the pattern fetching mechanism has failed entirely.
if ( ! call_user_func( $has_scheduled_action, 'fetch_patterns' ) ) {
wc_get_logger()->warning(
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
);
}
return;
}
@@ -153,7 +173,7 @@ class BlockPatterns {
$pattern['slug'] = $pattern['name'];
$pattern['content'] = $pattern['html'];
$this->pattern_registry->register_block_pattern( $pattern['ID'], $pattern, $this->dictionary );
$this->pattern_registry->register_block_pattern( $pattern['ID'], $pattern, $this->get_patterns_dictionary() );
}
}

View File

@@ -34,14 +34,16 @@ class Breadcrumbs extends AbstractBlock {
return;
}
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
return sprintf(
'<div class="woocommerce wc-block-breadcrumbs %1$s %2$s" style="%3$s">%4$s</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $classes_and_styles['styles'] ),
'<div %1$s>%2$s</div>',
get_block_wrapper_attributes(
array(
'class' => 'wc-block-breadcrumbs woocommerce ' . esc_attr( $classes_and_styles['classes'] ),
'style' => $classes_and_styles['styles'],
)
),
$breadcrumb
);
}

View File

@@ -245,8 +245,12 @@ class Cart extends AbstractBlock {
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) );
$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings();
$local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids();
$this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] );
$this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids );
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {

View File

@@ -11,4 +11,18 @@ class CartExpressPaymentBlock extends AbstractInnerBlock {
* @var string
*/
protected $block_name = 'cart-express-payment-block';
/**
* Uniform default_styles for the express payment buttons
*
* @var boolean
*/
protected $default_styles = null;
/**
* Current styles for the express payment buttons
*
* @var boolean
*/
protected $current_styles = null;
}

View File

@@ -370,8 +370,11 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings();
$local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids();
$this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] );
$this->asset_data_registry->add( 'localPickupText', $pickup_location_settings['title'] );
$this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids );
$is_block_editor = $this->is_block_editor();
@@ -385,8 +388,8 @@ class Checkout extends AbstractBlock {
$shipping_methods = WC()->shipping()->get_shipping_methods();
$formatted_shipping_methods = array_reduce(
$shipping_methods,
function ( $acc, $method ) {
if ( in_array( $method->id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
function ( $acc, $method ) use ( $local_pickup_method_ids ) {
if ( in_array( $method->id, $local_pickup_method_ids, true ) ) {
return $acc;
}
if ( $method->supports( 'settings' ) ) {

View File

@@ -1,6 +1,9 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Exception;
/**
* CheckoutExpressPaymentBlock class.
*/
@@ -11,4 +14,128 @@ class CheckoutExpressPaymentBlock extends AbstractInnerBlock {
* @var string
*/
protected $block_name = 'checkout-express-payment-block';
/**
* Default styles for the express payment buttons
*
* @var boolean
*/
protected $default_styles = null;
/**
* Current styles for the express payment buttons
*
* @var boolean
*/
protected $current_styles = null;
/**
* Initialise the block
*/
protected function initialize() {
parent::initialize();
$this->default_styles = array(
'showButtonStyles' => false,
'buttonHeight' => '48',
'buttonBorderRadius' => '4',
);
add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 );
}
/**
* Synchorize the express payment attributes between the Cart and Checkout pages.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
*/
public function sync_express_payment_attrs( $post_id, $post ) {
if ( wc_get_page_id( 'cart' ) === $post_id ) {
$cart_or_checkout = 'cart';
} elseif ( wc_get_page_id( 'checkout' ) === $post_id ) {
$cart_or_checkout = 'checkout';
} else {
return;
}
// This is not a proper save action, maybe an autosave, so don't continue.
if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) {
return;
}
$block_name = 'woocommerce/' . $cart_or_checkout;
$page_id = 'woocommerce_' . $cart_or_checkout . '_page_id';
$template_name = 'page-' . $cart_or_checkout;
// Check if we are editing the cart/checkout page and that it contains a Cart/Checkout block.
// Cast to string for Cart/Checkout page ID comparison because get_option can return it as a string, so better to compare both values as strings.
if ( ! empty( $post->post_type ) && 'wp_template' !== $post->post_type && ( false === has_block( $block_name, $post ) || (string) get_option( $page_id ) !== (string) $post_id ) ) {
return;
}
// Check if we are editing the Cart/Checkout template and that it contains a Cart/Checkout block.
if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && $template_name !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( $block_name, $post ) ) {
return;
}
if ( empty( $post->post_content ) ) {
return;
}
try {
// Parse the post content to get the express payment attributes of the current page.
$blocks = parse_blocks( $post->post_content );
$attrs = CartCheckoutUtils::find_express_checkout_attributes( $blocks, $cart_or_checkout );
if ( ! is_array( $attrs ) ) {
return;
}
$updated_attrs = array_merge( $this->default_styles, $attrs );
// We need to sync the attributes between the Cart and Checkout pages.
$other_page = 'cart' === $cart_or_checkout ? 'checkout' : 'cart';
$this->update_other_page_with_express_payment_attrs( $other_page, $updated_attrs );
} catch ( Exception $e ) {
wc_get_logger()->log( 'error', 'Error updating express payment attributes: ' . $e->getMessage() );
}
}
/**
* Update the express payment attributes in the other page (Cart or Checkout).
*
* @param string $cart_or_checkout The page to update.
* @param array $updated_attrs The updated attributes.
*/
private function update_other_page_with_express_payment_attrs( $cart_or_checkout, $updated_attrs ) {
$page_id = 'cart' === $cart_or_checkout ? wc_get_page_id( 'cart' ) : wc_get_page_id( 'checkout' );
if ( -1 === $page_id ) {
return;
}
$post = get_post( $page_id );
if ( empty( $post->post_content ) ) {
return;
}
$blocks = parse_blocks( $post->post_content );
CartCheckoutUtils::update_blocks_with_new_attrs( $blocks, $cart_or_checkout, $updated_attrs );
$updated_content = serialize_blocks( $blocks );
remove_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 );
wp_update_post(
array(
'ID' => $page_id,
'post_content' => $updated_content,
),
false,
false
);
add_action( 'save_post', array( $this, 'sync_express_payment_attrs' ), 10, 2 );
}
}

View File

@@ -68,6 +68,7 @@ class CustomerAccount extends AbstractBlock {
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
$parsed_hooked_block['attrs']['iconStyle'] = 'line';
$parsed_hooked_block['attrs']['iconClass'] = 'wc-block-customer-account__account-icon';
/*
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.

View File

@@ -55,11 +55,12 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
}
return $block_content ? sprintf(
'<div class="wc-block-%4$s %1$s" style="%2$s">%3$s</div>',
'<div class="wp-block-%5$s-%4$s wc-block-%4$s %1$s" style="%2$s">%3$s</div>',
esc_attr( trim( $classname ) ),
esc_attr( $classes_and_styles['styles'] ),
$block_content,
esc_attr( $this->block_name )
esc_attr( $this->block_name ),
esc_attr( $this->namespace )
) : '';
}
@@ -186,7 +187,13 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
*/
protected function is_email_verified( $order ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( empty( $_POST ) || ! isset( $_POST['email'] ) || ! wp_verify_nonce( $_POST['check_submission'] ?? '', 'wc_verify_email' ) ) {
if ( empty( $_POST ) || ! isset( $_POST['email'], $_POST['_wpnonce'] ) ) {
return false;
}
$nonce_value = sanitize_key( wp_unslash( $_POST['_wpnonce'] ?? '' ) );
if ( ! wp_verify_nonce( $nonce_value, 'wc_verify_email' ) && ! wp_verify_nonce( $nonce_value, 'wc_create_account' ) ) {
return false;
}

View File

@@ -0,0 +1,174 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
/**
* CreateAccount class.
*/
class CreateAccount extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-create-account';
/**
* Get the frontend script handle for this block type.
*
* @see $this->register_block_type()
* @param string $key Data to get, or default to everything.
* @return array|string
*/
protected function get_block_type_script( $key = null ) {
$script = [
'handle' => 'wc-order-confirmation-create-account-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( 'order-confirmation-create-account-frontend' ),
'dependencies' => [],
];
return $key ? $script[ $key ] : $script;
}
/**
* Process posted account form.
*
* @param \WC_Order $order Order object.
* @return \WP_Error|int
*/
protected function process_form_post( $order ) {
if ( ! isset( $_POST['create-account'], $_POST['email'], $_POST['password'], $_POST['_wpnonce'] ) ) {
return 0;
}
if ( ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'wc_create_account' ) ) {
return new \WP_Error( 'invalid_nonce', __( 'Unable to create account. Please try again.', 'woocommerce' ) );
}
$user_email = sanitize_email( wp_unslash( $_POST['email'] ) );
$password = wp_unslash( $_POST['password'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Does order already have user?
if ( $order->get_customer_id() ) {
return new \WP_Error( 'order_already_has_user', __( 'This order is already linked to a user account.', 'woocommerce' ) );
}
// Check given details match the current viewed order.
if ( $order->get_billing_email() !== $user_email ) {
return new \WP_Error( 'email_mismatch', __( 'The email address provided does not match the email address on this order.', 'woocommerce' ) );
}
if ( empty( $password ) || strlen( $password ) < 8 ) {
return new \WP_Error( 'password_too_short', __( 'Password must be at least 8 characters.', 'woocommerce' ) );
}
$customer_id = wc_create_new_customer(
$user_email,
'',
$password,
[
'first_name' => $order->get_billing_first_name(),
'last_name' => $order->get_billing_last_name(),
'source' => 'delayed-account-creation',
]
);
if ( is_wp_error( $customer_id ) ) {
return $customer_id;
}
// Associate customer with the order.
$order->set_customer_id( $customer_id );
$order->save();
// Associate addresses from the order with the customer.
$order_controller = new OrderController();
$order_controller->sync_customer_data_with_order( $order );
// Set the customer auth cookie.
wc_set_customer_auth_cookie( $customer_id );
return $customer_id;
}
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string|false $permission If the current user can view the order details or not.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission ) {
return '';
}
// Check registration is possible for this order/customer, and if not, return early.
if ( is_user_logged_in() || email_exists( $order->get_billing_email() ) ) {
return '';
}
$result = $this->process_form_post( $order );
if ( is_wp_error( $result ) ) {
$notice = wc_print_notice( $result->get_error_message(), 'error', [], true );
} elseif ( $result ) {
return $this->render_confirmation();
}
$processor = new \WP_HTML_Tag_Processor(
$content .
'<div class="woocommerce-order-confirmation-create-account-form-wrapper">' .
$notice .
'<div class="woocommerce-order-confirmation-create-account-form"></div>' .
'</div>'
);
if ( ! $processor->next_tag( array( 'class_name' => 'wp-block-woocommerce-order-confirmation-create-account' ) ) ) {
return $content;
}
$processor->set_attribute( 'class', '' );
$processor->set_attribute( 'style', '' );
$processor->add_class( 'woocommerce-order-confirmation-create-account-content' );
if ( ! $processor->next_tag( array( 'class_name' => 'woocommerce-order-confirmation-create-account-form' ) ) ) {
return $content;
}
$processor->set_attribute( 'data-customer-email', $order->get_billing_email() );
$processor->set_attribute( 'data-nonce-token', wp_create_nonce( 'wc_create_account' ) );
if ( ! empty( $attributes['hasDarkControls'] ) ) {
$processor->add_class( 'has-dark-controls' );
}
return $processor->get_updated_html();
}
/**
* Render the block when an account has been registered.
*
* @return string
*/
protected function render_confirmation() {
$content = '<div class="woocommerce-order-confirmation-create-account-success" id="create-account">';
$content .= '<h3>' . esc_html__( 'Your account has been successfully created', 'woocommerce' ) . '</h3>';
$content .= '<p>' . sprintf(
/* translators: 1: link to my account page, 2: link to shipping and billing addresses, 3: link to account details, 4: closing tag */
esc_html__( 'You can now %1$sview your recent orders%4$s, manage your %2$sshipping and billing addresses%4$s, and edit your %3$spassword and account details%4$s.', 'woocommerce' ),
'<a href="' . esc_url( wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) ) . '">',
'<a href="' . esc_url( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ) . '">',
'<a href="' . esc_url( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) ) . '">',
'</a>'
) . '</p>';
$content .= '</div>';
return $content;
}
}

View File

@@ -2,8 +2,6 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Status class.
*/
@@ -265,7 +263,7 @@ class Status extends AbstractOrderConfirmationBlock {
</p>',
esc_attr( 'verify-email-submit' ),
esc_html__( 'Confirm email and view order', 'woocommerce' ),
wp_nonce_field( 'wc_verify_email', 'check_submission', true, false ),
wp_nonce_field( 'wc_verify_email', '_wpnonce', true, false ),
esc_attr( wc_wp_theme_get_element_class_name( 'button' ) )
) .
'</form>';

View File

@@ -29,11 +29,11 @@ class Summary extends AbstractOrderConfirmationBlock {
}
$content = '<ul class="wc-block-order-confirmation-summary-list">';
$content .= $this->render_summary_row( __( 'Order number:', 'woocommerce' ), $order->get_order_number() );
$content .= $this->render_summary_row( __( 'Order #:', 'woocommerce' ), $order->get_order_number() );
$content .= $this->render_summary_row( __( 'Date:', 'woocommerce' ), wc_format_datetime( $order->get_date_created() ) );
$content .= $this->render_summary_row( __( 'Total:', 'woocommerce' ), $order->get_formatted_order_total() );
$content .= $this->render_summary_row( __( 'Email:', 'woocommerce' ), $order->get_billing_email() );
$content .= $this->render_summary_row( __( 'Payment method:', 'woocommerce' ), $order->get_payment_method_title() );
$content .= $this->render_summary_row( __( 'Payment:', 'woocommerce' ), $order->get_payment_method_title() );
$content .= '</ul>';
return $content;

View File

@@ -176,6 +176,10 @@ class ProductButton extends AbstractBlock {
data-wc-class--loading="context.isLoading"
';
$anchor_directive = '
data-wc-on--click="woocommerce/product-collection::actions.viewProduct"
';
$span_button_directives = '
data-wc-text="state.addToCartText"
data-wc-class--wc-block-slide-in="state.slideInAnimation"
@@ -219,7 +223,7 @@ class ProductButton extends AbstractBlock {
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
'{add_to_cart_text}' => esc_html( $initial_product_text ),
'{div_directives}' => $is_ajax_button ? $div_directives : '',
'{button_directives}' => $is_ajax_button ? $button_directives : '',
'{button_directives}' => $is_ajax_button ? $button_directives : $anchor_directive,
'{span_button_directives}' => $is_ajax_button ? $span_button_directives : '',
'{view_cart_html}' => $is_ajax_button ? $this->get_view_cart_html() : '',
)

View File

@@ -47,6 +47,18 @@ class ProductCollection extends AbstractBlock {
protected $custom_order_opts = array( 'popularity', 'rating' );
/**
* The render state of the product collection block.
*
* These props are runtime-based and reinitialize for every block on a page.
*
* @var array
*/
private $render_state = array(
'has_results' => false,
'has_no_results_block' => false,
);
/**
* Initialize this block type.
*
@@ -80,9 +92,32 @@ class ProductCollection extends AbstractBlock {
// Provide location context into block's context.
add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 );
// Disable block render if the ProductTemplate block is empty.
add_filter(
'render_block_woocommerce/product-template',
function ( $html ) {
$this->render_state['has_results'] = ! empty( $html );
return $html;
},
100,
1
);
// Enable block render if the ProductCollectionNoResults block is rendered.
add_filter(
'render_block_woocommerce/product-collection-no-results',
function ( $html ) {
$this->render_state['has_no_results_block'] = ! empty( $html );
return $html;
},
100,
1
);
// Interactivity API: Add navigation directives to the product collection block.
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'handle_rendering' ), 10, 2 );
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
add_filter( 'render_block_core/post-title', array( $this, 'add_product_title_click_event_directives' ), 10, 3 );
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
@@ -90,6 +125,46 @@ class ProductCollection extends AbstractBlock {
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
}
/**
* Handle the rendering of the block.
*
* @param string $block_content The block content about to be rendered.
* @param array $block The block being rendered.
*
* @return string
*/
public function handle_rendering( $block_content, $block ) {
if ( $this->should_prevent_render() ) {
return ''; // Prevent rendering.
}
// Reset the render state for the next render.
$this->reset_render_state();
return $this->enhance_product_collection_with_interactivity( $block_content, $block );
}
/**
* Check if the block should be prevented from rendering.
*
* @return bool
*/
private function should_prevent_render() {
return ! $this->render_state['has_results'] && ! $this->render_state['has_no_results_block'];
}
/**
* Reset the render state.
*/
private function reset_render_state() {
$this->render_state = array(
'has_results' => false,
'has_no_results_block' => false,
);
}
/**
* Provides the location context to each inner block of the product collection block.
* Hint: Only blocks using the 'query' context will be affected.
@@ -326,7 +401,7 @@ class ProductCollection extends AbstractBlock {
$is_enhanced_pagination_enabled = ! ( $this->parsed_block['attrs']['forcePageReload'] ?? false );
// Only proceed if the block is a product collection block,
// enhaced pagination is enabled and query IDs match.
// enhanced pagination is enabled and query IDs match.
if ( $is_product_collection_block && $is_enhanced_pagination_enabled && $query_id === $parsed_query_id ) {
$block_content = $this->process_pagination_links( $block_content );
}
@@ -334,6 +409,36 @@ class ProductCollection extends AbstractBlock {
return $block_content;
}
/**
* Add interactivity to the Product Title block within Product Collection.
* This enables the triggering of a custom event when the product title is clicked.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
* @return string Modified block content with added interactivity.
*/
public function add_product_title_click_event_directives( $block_content, $block, $instance ) {
$namespace = $instance->attributes['__woocommerceNamespace'] ?? '';
$is_product_title_block = 'woocommerce/product-collection/product-title' === $namespace;
$is_link = $instance->attributes['isLink'] ?? false;
// Only proceed if the block is a Product Title (Post Title variation) block.
if ( $is_product_title_block && $is_link ) {
$p = new \WP_HTML_Tag_Processor( $block_content );
$p->next_tag( array( 'class_name' => 'wp-block-post-title' ) );
$is_anchor = $p->next_tag( array( 'tag_name' => 'a' ) );
if ( $is_anchor ) {
$p->set_attribute( 'data-wc-on--click', 'woocommerce/product-collection::actions.viewProduct' );
$block_content = $p->get_updated_html();
}
}
return $block_content;
}
/**
* Process pagination links within the block content.
*
@@ -394,11 +499,18 @@ class ProductCollection extends AbstractBlock {
*/
private function is_block_compatible( $block_name ) {
// Check for explicitly unsupported blocks.
if (
'core/post-content' === $block_name ||
'woocommerce/mini-cart' === $block_name ||
'woocommerce/featured-product' === $block_name
) {
$unsupported_blocks = array(
'core/post-content',
'woocommerce/mini-cart',
'woocommerce/featured-product',
'woocommerce/active-filters',
'woocommerce/price-filter',
'woocommerce/stock-filter',
'woocommerce/attribute-filter',
'woocommerce/rating-filter',
);
if ( in_array( $block_name, $unsupported_blocks, true ) ) {
return false;
}
@@ -416,8 +528,8 @@ class ProductCollection extends AbstractBlock {
/**
* Check inner blocks of Product Collection block if there's one
* incompatible with Interactivity API and if so, disable client-side
* naviagtion.
* incompatible with the Interactivity API and if so, disable client-side
* navigation.
*
* @param array $parsed_block The block being rendered.
* @return string Returns the parsed block, unmodified.
@@ -869,7 +981,7 @@ class ProductCollection extends AbstractBlock {
* - For array items with numeric keys, we merge them as normal.
* - For array items with string keys:
*
* - If the value isn't array, we'll use the value comming from the merge array.
* - If the value isn't array, we'll use the value coming from the merge array.
* $base = ['orderby' => 'date']
* $new = ['orderby' => 'meta_value_num']
* Result: ['orderby' => 'meta_value_num']

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WP_HTML_Tag_Processor;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
@@ -50,8 +51,31 @@ class ProductDetails extends AbstractBlock {
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$hide_tab_title = isset( $attributes['hideTabTitle'] ) ? $attributes['hideTabTitle'] : false;
if ( $hide_tab_title ) {
add_filter( 'woocommerce_product_description_heading', '__return_empty_string' );
add_filter( 'woocommerce_product_additional_information_heading', '__return_empty_string' );
add_filter( 'woocommerce_reviews_title', '__return_empty_string' );
}
$tabs = $this->render_tabs();
if ( $hide_tab_title ) {
remove_filter( 'woocommerce_product_description_heading', '__return_empty_string' );
remove_filter( 'woocommerce_product_additional_information_heading', '__return_empty_string' );
remove_filter( 'woocommerce_reviews_title', '__return_empty_string' );
// Remove the first `h2` of every `.wc-tab`. This is required for the Reviews tabs when there are no reviews and for plugin tabs.
$tabs_html = new WP_HTML_Tag_Processor( $tabs );
while ( $tabs_html->next_tag( array( 'class_name' => 'wc-tab' ) ) ) {
if ( $tabs_html->next_tag( 'h2' ) ) {
$tabs_html->set_attribute( 'hidden', 'true' );
}
}
$tabs = $tabs_html->get_updated_html();
}
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );

View File

@@ -1,157 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
use WP_HTML_Tag_Processor;
/**
* Product Filter Block.
*/
final class ProductFilter extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-filter';
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Get the frontend script handle for this block type.
*
* @see $this->register_block_type()
* @param string $key Data to get, or default to everything.
* @return array|string|null
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* 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 = [] ) {
global $pagenow;
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
$this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() );
$this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow );
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow );
}
/**
* Check array for checked item.
*
* @param array $items Items to check.
*/
private function hasSelectedFilter( $items ) {
foreach ( $items as $key => $value ) {
if ( 'checked' === $key && true === $value ) {
return true;
}
if ( is_array( $value ) && $this->hasSelectedFilter( $value ) ) {
return true;
}
}
return false;
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( is_admin() ) {
return $content;
}
$tags = new WP_HTML_Tag_Processor( $content );
$has_selected_filter = false;
while ( $tags->next_tag( 'div' ) ) {
$items = $tags->get_attribute( 'data-wc-context' ) ? json_decode( $tags->get_attribute( 'data-wc-context' ), true ) : null;
// For checked box filters.
if ( $items && array_key_exists( 'items', $items ) ) {
$has_selected_filter = $this->hasSelectedFilter( $items['items'] );
break;
}
// For price range filter.
if ( $items && array_key_exists( 'minPrice', $items ) ) {
if ( $items['minPrice'] > $items['minRange'] || $items['maxPrice'] < $items['maxRange'] ) {
$has_selected_filter = true;
break;
}
}
// For dropdown filters.
if ( $items && array_key_exists( 'selectedItems', $items ) ) {
if ( count( $items['selectedItems'] ) > 0 ) {
$has_selected_filter = true;
break;
}
}
}
$attributes_data = array(
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'class' => 'wc-block-product-filters',
);
if ( ! isset( $block->context['queryId'] ) ) {
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
}
$tags = new WP_HTML_Tag_Processor( $content );
while ( $tags->next_tag( 'div' ) ) {
if ( 'yes' === $tags->get_attribute( 'data-has-filter' ) ) {
return sprintf(
'<nav %1$s>%2$s</nav>',
get_block_wrapper_attributes( $attributes_data ),
$content
);
}
}
return sprintf(
'<nav %1$s></nav>',
get_block_wrapper_attributes( $attributes_data ),
);
}
/**
* 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-filter-%s',
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
);
}
}

View File

@@ -48,16 +48,9 @@ final class ProductFilterActive extends AbstractBlock {
*/
$active_filters = apply_filters( 'collection_active_filters_data', array(), $this->get_filter_query_params( $query_id ) );
$context = array(
'queryId' => $query_id,
'params' => array_keys( $this->get_filter_query_params( $query_id ) ),
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes',
)
);

View File

@@ -1,10 +1,10 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
/**
@@ -26,6 +26,7 @@ final class ProductFilterAttribute extends AbstractBlock {
* - Register the block with WordPress.
*/
protected function initialize() {
add_filter( 'block_type_metadata_settings', array( $this, 'add_block_type_metadata_settings' ), 10, 2 );
parent::initialize();
add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 );
@@ -129,10 +130,10 @@ final class ProductFilterAttribute extends AbstractBlock {
return array(
'title' => $term_object->name,
'attributes' => array(
'data-wc-on--click' => "$action_namespace::actions.removeFilter",
'value' => $term,
'data-wc-on--click' => "$action_namespace::actions.toggleFilter",
'data-wc-context' => "$action_namespace::" . wp_json_encode(
array(
'value' => $term,
'attributeSlug' => $product_attribute,
'queryType' => get_query_var( "query_type_{$product_attribute}" ),
),
@@ -156,24 +157,24 @@ final class ProductFilterAttribute extends AbstractBlock {
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @param array $block_attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( empty( $attributes['attributeId'] ) ) {
$default_product_attribute = $this->get_default_product_attribute();
$attributes['attributeId'] = $default_product_attribute->attribute_id;
protected function render( $block_attributes, $content, $block ) {
if ( empty( $block_attributes['attributeId'] ) ) {
$default_product_attribute = $this->get_default_product_attribute();
$block_attributes['attributeId'] = $default_product_attribute->attribute_id;
}
// don't render if its admin, or ajax in progress.
if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) {
if ( is_admin() || wp_doing_ajax() || empty( $block_attributes['attributeId'] ) ) {
return '';
}
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
$attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] );
$product_attribute = wc_get_attribute( $block_attributes['attributeId'] );
$attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $block_attributes['queryType'] );
if ( empty( $attribute_counts ) ) {
return sprintf(
@@ -181,7 +182,6 @@ final class ProductFilterAttribute extends AbstractBlock {
get_block_wrapper_attributes(
array(
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => 'no',
)
),
);
@@ -202,118 +202,56 @@ final class ProductFilterAttribute extends AbstractBlock {
);
$attribute_options = array_map(
function ( $term ) use ( $attribute_counts, $selected_terms ) {
function ( $term ) use ( $block_attributes, $attribute_counts, $selected_terms ) {
$term = (array) $term;
$term['count'] = $attribute_counts[ $term['term_id'] ];
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
return $term;
return array(
'label' => $block_attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $term['name'], $term['count'] ) : $term['name'],
'value' => $term['slug'],
'selected' => $term['selected'],
'rawData' => $term,
);
},
$attribute_terms
);
$filtered_options = array_filter(
$attribute_options,
function ( $option ) {
return $option['count'] > 0;
function ( $option ) use ( $block_attributes ) {
$hide_empty = $block_attributes['hideEmpty'] ?? true;
if ( $hide_empty ) {
return $option['rawData']['count'] > 0;
}
return true;
}
);
$filter_content = 'dropdown' === $attributes['displayStyle'] ?
$this->render_attribute_dropdown( $filtered_options, $attributes ) :
$this->render_attribute_checkbox_list( $filtered_options, $attributes );
$filter_context = array(
'action' => "{$this->get_full_block_name()}::actions.toggleFilter",
'items' => $filtered_options,
);
foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) {
$content .= ( new \WP_Block( $inner_block, array( 'filterData' => $filter_context ) ) )->render();
}
$context = array(
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
'queryType' => $attributes['queryType'],
'selectType' => 'multiple',
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
'queryType' => $block_attributes['queryType'],
'selectType' => 'multiple',
'hasSelectedFilters' => count( $selected_terms ) > 0,
);
return sprintf(
'<div %1$s>%2$s%3$s</div>',
'<div %1$s>%2$s</div>',
get_block_wrapper_attributes(
array(
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'data-has-filter' => 'yes',
)
),
$content,
$filter_content
);
}
/**
* Render the dropdown.
*
* @param array $options Data to render the dropdown.
* @param bool $attributes Block attributes.
*/
private function render_attribute_dropdown( $options, $attributes ) {
if ( empty( $options ) ) {
return '';
}
$list_items = array();
$selected_items = array();
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
foreach ( $options as $option ) {
$item = array(
'label' => $attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'],
'value' => $option['slug'],
);
$list_items[] = $item;
if ( $option['selected'] ) {
$selected_items[] = $item;
}
}
return Dropdown::render(
array(
'items' => $list_items,
'action' => "{$this->get_full_block_name()}::actions.navigate",
'selected_items' => $selected_items,
'select_type' => 'multiple',
// translators: %s is a product attribute name.
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ),
)
);
}
/**
* Render the attribute filter checkbox list.
*
* @param mixed $options Attribute filter options to render in the checkbox list.
* @param mixed $attributes Block attributes.
* @return string
*/
private function render_attribute_checkbox_list( $options, $attributes ) {
if ( empty( $options ) ) {
return '';
}
$show_counts = $attributes['showCounts'] ?? false;
$list_options = array_map(
function ( $option ) use ( $show_counts ) {
return array(
'id' => $option['slug'] . '-' . $option['term_id'],
'checked' => $option['selected'],
'label' => $show_counts ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'],
'value' => $option['slug'],
);
},
$options
);
return CheckboxList::render(
array(
'items' => $list_options,
'on_change' => "{$this->get_full_block_name()}::actions.updateProducts",
)
$content
);
}
@@ -380,7 +318,16 @@ final class ProductFilterAttribute extends AbstractBlock {
$cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' );
if ( $cached ) {
if (
$cached &&
isset( $cached->attribute_id ) &&
isset( $cached->attribute_name ) &&
isset( $cached->attribute_label ) &&
isset( $cached->attribute_type ) &&
isset( $cached->attribute_orderby ) &&
isset( $cached->attribute_public ) &&
'0' !== $cached->attribute_id
) {
return $cached;
}
@@ -428,10 +375,9 @@ final class ProductFilterAttribute extends AbstractBlock {
if ( $attribute_id ) {
$default_attribute = $attributes[ $attribute_id ];
set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute, DAY_IN_SECONDS );
}
set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute );
return $default_attribute;
}
@@ -447,32 +393,29 @@ final class ProductFilterAttribute extends AbstractBlock {
'inserter' => false,
'content' => strtr(
'
<!-- wp:woocommerce/product-filter {"filterType":"attribute-filter","attributeId":{{attribute_id}}} -->
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group">
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">{{attribute_label}}</h3>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-filter-attribute {"attributeId":{{attribute_id}}} -->
<div class="wp-block-woocommerce-product-filter-attribute">
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group">
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">{{attribute_label}}</h3>
<!-- /wp:heading -->
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true}} -->
<!-- wp:buttons {"layout":{"type":"flex"}} -->
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->
<!-- /wp:woocommerce/product-filter-clear-button --></div>
<!-- /wp:group -->
<!-- wp:woocommerce/product-filter-checkbox-list {"lock":{"remove":true}} -->
<div class="wp-block-woocommerce-product-filter-checkbox-list wc-block-product-filter-checkbox-list"></div>
<!-- /wp:woocommerce/product-filter-checkbox-list -->
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true,"move":false}} -->
<!-- wp:buttons {"layout":{"type":"flex"}} -->
<div class="wp-block-buttons">
<!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
<div
class="wp-block-button wc-block-product-filter-clear-button is-style-outline"
style="text-decoration: underline"
>
<a class="wp-block-button__link wp-element-button" style="border-style: none; border-width: 0px">Clear</a>
</div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
<!-- /wp:woocommerce/product-filter-clear-button -->
</div>
<!-- /wp:group -->
<!-- wp:woocommerce/product-filter-attribute {"attributeId":{{attribute_id}},"lock":{"remove":true}} /-->
<!-- /wp:woocommerce/product-filter -->
<!-- /wp:woocommerce/product-filter-attribute -->
',
array(
'{{attribute_id}}' => intval( $default_attribute->attribute_id ),
@@ -482,4 +425,18 @@ final class ProductFilterAttribute extends AbstractBlock {
)
);
}
/**
* Skip default rendering routine for 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/{$this->block_name}" === $metadata['name'] ) {
$settings['skip_inner_blocks'] = true;
}
return $settings;
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* Product Filter: Checkbox List Block.
*/
final class ProductFilterCheckboxList extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-filter-checkbox-list';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$context = $block->context['filterData'];
$items = $context['items'] ?? array();
$checkbox_list_context = array( 'items' => $items );
$action = $context['action'] ?? '';
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
$classes = '';
$style = '';
$tags = new \WP_HTML_Tag_Processor( $content );
if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-checkbox-list' ) ) ) {
$classes = $tags->get_attribute( 'class' );
$style = $tags->get_attribute( 'style' );
}
$checked_items = array_filter(
$items,
function ( $item ) {
return $item['selected'];
}
);
$show_initially = $context['show_initially'] ?? 15;
$remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items );
$count = 0;
$wrapper_attributes = array(
'data-wc-interactive' => esc_attr( $namespace ),
'data-wc-context' => wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'class' => esc_attr( $classes ),
'style' => esc_attr( $style ),
);
ob_start();
?>
<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<ul class="wc-block-product-filter-checkbox-list__list" aria-label="<?php echo esc_attr__( 'Filter Options', 'woocommerce' ); ?>">
<?php foreach ( $items as $item ) { ?>
<?php
$item['id'] = $item['id'] ?? uniqid( 'checkbox-' );
// translators: %s: checkbox label.
$i18n_label = sprintf( __( 'Checkbox: %s', 'woocommerce' ), $item['aria_label'] ?? '' );
?>
<li
data-wc-key="<?php echo esc_attr( $item['id'] ); ?>"
<?php
if ( ! $item['selected'] ) :
if ( $count >= $remaining_initial_unchecked ) :
?>
class="wc-block-product-filter-checkbox-list__item"
data-wc-bind--hidden="!context.showAll"
hidden
<?php else : ?>
<?php ++$count; ?>
<?php endif; ?>
<?php endif; ?>
class="wc-block-product-filter-checkbox-list__item"
>
<label
class="wc-block-product-filter-checkbox-list__label"
for="<?php echo esc_attr( $item['id'] ); ?>"
>
<span class="wc-block-product-filter-checkbox-list__input-wrapper">
<input
id="<?php echo esc_attr( $item['id'] ); ?>"
class="wc-block-product-filter-checkbox-list__input"
type="checkbox"
aria-invalid="false"
aria-label="<?php echo esc_attr( $i18n_label ); ?>"
data-wc-on--change--select-item="actions.selectCheckboxItem"
data-wc-on--change--parent-action="<?php echo esc_attr( $action ); ?>"
value="<?php echo esc_attr( $item['value'] ); ?>"
<?php checked( $item['selected'], 1 ); ?>
>
<svg class="wc-block-product-filter-checkbox-list__mark" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25 1.19922L3.75 6.69922L1 3.94922" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<span class="wc-block-product-filter-checkbox-list__text">
<?php echo wp_kses_post( $item['label'] ); ?>
</span>
</label>
</li>
<?php } ?>
</ul>
<?php if ( count( $items ) > $show_initially ) : ?>
<button
class="wc-block-product-filter-checkbox-list__show-more"
data-wc-bind--hidden="context.showAll"
data-wc-on--click="actions.showAllItems"
hidden
>
<?php echo esc_html__( 'Show more...', 'woocommerce' ); ?>
</button>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* Product Filter: Chips Block.
*/
final class ProductFilterChips extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-filter-chips';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$classes = '';
$style = '';
$context = $block->context['filterData'];
$items = $context['items'] ?? array();
$checkbox_list_context = array( 'items' => $items );
$action = $context['action'] ?? '';
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-chips' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
$tags = new \WP_HTML_Tag_Processor( $content );
if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-chips' ) ) ) {
$classes = $tags->get_attribute( 'class' );
$style = $tags->get_attribute( 'style' );
}
$checked_items = array_filter(
$items,
function ( $item ) {
return $item['selected'];
}
);
$show_initially = $context['show_initially'] ?? 15;
$remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items );
$count = 0;
$wrapper_attributes = array(
'data-wc-interactive' => esc_attr( $namespace ),
'data-wc-context' => wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
'class' => esc_attr( $classes ),
'style' => esc_attr( $style ),
);
ob_start();
?>
<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div class="wc-block-product-filter-chips__items" aria-label="<?php echo esc_attr__( 'Filter Options', 'woocommerce' ); ?>">
<?php foreach ( $items as $item ) { ?>
<?php $item['id'] = $item['id'] ?? uniqid( 'chips-' ); ?>
<button
data-wc-key="<?php echo esc_attr( $item['id'] ); ?>"
<?php
if ( ! $item['selected'] ) :
if ( $count >= $remaining_initial_unchecked ) :
?>
class="wc-block-product-filter-chips__item"
data-wc-bind--hidden="!context.showAll"
hidden
<?php else : ?>
<?php ++$count; ?>
<?php endif; ?>
<?php endif; ?>
class="wc-block-product-filter-chips__item"
data-wc-on--click--select-item="actions.selectItem"
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
value="<?php echo esc_attr( $item['value'] ); ?>"
aria-checked="<?php echo $item['selected'] ? 'true' : 'false'; ?>"
>
<span class="wc-block-product-filter-chips__label">
<?php echo wp_kses_post( $item['label'] ); ?>
</span>
</button>
<?php } ?>
</div>
<?php if ( count( $items ) > $show_initially ) : ?>
<button
class="wc-block-product-filter-chips__show-more"
data-wc-bind--hidden="context.showAll"
data-wc-on--click="actions.showAllItems"
hidden
>
<?php echo esc_html__( 'Show more...', 'woocommerce' ); ?>
</button>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -38,14 +38,14 @@ final class ProductFilterClearButton extends AbstractBlock {
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-wc-bind--hidden' => '!context.hasSelectedFilter',
'data-wc-bind--hidden' => '!context.hasSelectedFilters',
)
);
$p = new \WP_HTML_Tag_Processor( $content );
if ( $p->next_tag( array( 'class_name' => 'wp-block-button__link' ) ) ) {
$p->set_attribute( 'data-wc-on--click', 'actions.clear' );
$p->set_attribute( 'data-wc-on--click', 'actions.clearFilters' );
$style = $p->get_attribute( 'style' );
$p->set_attribute( 'style', 'outline:none;' . $style );

View File

@@ -234,6 +234,7 @@ final class ProductFilterPrice extends AbstractBlock {
data-wc-bind--max="context.maxRange"
data-wc-bind--value="context.minPrice"
data-wc-on--change="actions.updateProducts"
data-wc-on--input="actions.updateRange"
>
<input
type="range"
@@ -246,6 +247,7 @@ final class ProductFilterPrice extends AbstractBlock {
data-wc-bind--max="context.maxRange"
data-wc-bind--value="context.maxPrice"
data-wc-on--change="actions.updateProducts"
data-wc-on--input="actions.updateRange"
>
</div>
<div class="wp-block-woocommerce-product-filter-price-content-right-input text">

View File

@@ -1,6 +1,8 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductFilters class.
*/
@@ -18,16 +20,108 @@ class ProductFilters extends AbstractBlock {
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'postId' ];
return array( 'postId' );
}
/**
* Get the frontend style handle for this block type.
* Extra data passed through from server to client for block.
*
* @return null
* @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 get_block_type_style() {
return null;
protected function enqueue_data( array $attributes = array() ) {
global $pagenow;
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
$this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() );
$this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow );
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow );
}
/**
* Return the dialog content.
*
* @return string
*/
protected function render_dialog() {
$template_part = BlockTemplateUtils::get_template_part( 'product-filters-overlay' );
$html = $this->render_template_part( $template_part );
$html = strtr(
'<dialog hidden role="dialog" aria-modal="true">
{{html}}
</dialog>',
array(
'{{html}}' => $html,
)
);
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute( 'data-wc-bind--hidden', '!state.isDialogOpen' );
$p->set_attribute( 'data-wc-class--wc-block-product-filters--dialog-open', 'state.isDialogOpen' );
$p->set_attribute( 'data-wc-class--wc-block-product-filters--with-admin-bar', 'context.hasPageWithWordPressAdminBar' );
$html = $p->get_updated_html();
}
return $html;
}
/**
* This method is used to render the template part. For each template part, we parse the blocks and render them.
*
* @param string $template_part The template part to render.
* @return string The rendered template part.
*/
protected function render_template_part( $template_part ) {
$parsed_blocks = parse_blocks( $template_part );
$wrapper_template_part_block = $parsed_blocks[0];
$html = $wrapper_template_part_block['innerHTML'];
$target_div = '</div>';
$template_part_content_html = array_reduce(
$wrapper_template_part_block['innerBlocks'],
function ( $carry, $item ) {
if ( 'core/template-part' === $item['blockName'] ) {
$inner_template_part = BlockTemplateUtils::get_template_part( $item['attrs']['slug'] );
$inner_template_part_content_html = $this->render_template_part( $inner_template_part );
return $carry . $inner_template_part_content_html;
}
return $carry . render_block( $item );
},
''
);
$html = str_replace( $target_div, $template_part_content_html . $target_div, $html );
return $html;
}
/**
* Inject dialog into the product filters HTML.
*
* @param string $product_filters_html The Product Filters HTML.
* @param string $dialog_html The dialog HTML.
*
* @return string
*/
protected function inject_dialog( $product_filters_html, $dialog_html ) {
// Find the position of the last </div>.
$pos = strrpos( $product_filters_html, '</div>' );
if ( $pos ) {
// Inject the dialog_html at the correct position.
$html = substr_replace( $product_filters_html, $dialog_html, $pos, 0 );
return $html;
}
return $product_filters_html;
}
/**
@@ -39,6 +133,86 @@ class ProductFilters extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
return $content;
$tags = new \WP_HTML_Tag_Processor( $content );
if ( $tags->next_tag() ) {
$tags->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/' . $this->block_name ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$tags->set_attribute(
'data-wc-context',
wp_json_encode(
array(
'isDialogOpen' => false,
'hasPageWithWordPressAdminBar' => false,
'params' => $this->get_filter_query_params( 0 ),
'originalParams' => $this->get_filter_query_params( 0 ),
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
$tags->set_attribute( 'data-wc-navigation-id', $this->generate_navigation_id( $block ) );
$tags->set_attribute( 'data-wc-watch', 'callbacks.maybeNavigate' );
if (
'always' === $attributes['overlay'] ||
( 'mobile' === $attributes['overlay'] && wp_is_mobile() )
) {
return $this->inject_dialog( $tags->get_updated_html(), $this->render_dialog() );
}
return $tags->get_updated_html();
}
}
/**
* 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( $block->parsed_block['innerBlocks'] ) )
);
}
/**
* 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_query_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 );
/**
* Filters the active filter data provided by filter blocks.
*
* @since 11.7.0
*
* @param array $filter_param_keys The active filters data
* @param array $url_param_keys The query param parsed from the URL.
*
* @return array Active filters params.
*/
$filter_param_keys = array_unique( apply_filters( 'collection_filter_query_param_keys', array(), array_keys( $url_query_params ) ) );
return array_filter(
$url_query_params,
function ( $key ) use ( $filter_param_keys ) {
return in_array( $key, $filter_param_keys, true );
},
ARRAY_FILTER_USE_KEY
);
}
}

View File

@@ -21,17 +21,6 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
return [ 'woocommerce/product-filters/overlay' ];
}
/**
* Get the frontend script handle for this block type.
*
* @see $this->register_block_type()
* @param string $key Data to get, or default to everything.
* @return array|string|null
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Include and render the block.
*
@@ -46,13 +35,13 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
'class' => 'wc-block-product-filters-overlay-navigation',
)
);
$overlay_mode = $block->context['woocommerce/product-filters/overlay'];
$overlay_mode = isset( $block->context['woocommerce/product-filters/overlay'] ) ? $block->context['woocommerce/product-filters/overlay'] : 'never';
if ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) {
if ( 'open-overlay' === $attributes['triggerType'] && ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) ) {
return null;
}
$html_content = strtr(
$html = strtr(
'<div {{wrapper_attributes}}>
{{primary_content}}
{{secondary_content}}
@@ -63,7 +52,20 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
'{{secondary_content}}' => 'open-overlay' === $attributes['triggerType'] ? $this->render_label( $attributes ) : $this->render_icon( $attributes ),
)
);
return $html_content;
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute(
'data-wc-on--click',
'open-overlay' === $attributes['triggerType'] ? 'actions.openDialog' : 'actions.closeDialog'
);
$p->set_attribute( 'data-wc-class--hidden', 'open-overlay' === $attributes['triggerType'] ? 'state.isDialogOpen' : '!state.isDialogOpen' );
$html = $p->get_updated_html();
}
return $html;
}
/**

View File

@@ -110,11 +110,14 @@ class ProductGallery extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'] ?? '';
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
return '';
}
$product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array() );
$classname_single_image = '';
// This is a temporary solution. We have to refactor this code when the block will have to be addable on every page/post https://github.com/woocommerce/woocommerce-blocks/issues/10882.
global $product;
if ( count( $product_gallery_images ) < 2 ) {
// The gallery consists of a single image.
@@ -124,8 +127,6 @@ class ProductGallery extends AbstractBlock {
$number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0;
$classname = $attributes['className'] ?? '';
$dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : '';
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
$product_gallery_first_image = ProductGalleryUtils::get_product_gallery_image_ids( $product, 1 );
$product_gallery_first_image_id = reset( $product_gallery_first_image );
$product_id = strval( $product->get_id() );

View File

@@ -125,12 +125,15 @@ class ProductImage extends AbstractBlock {
private function render_anchor( $product, $on_sale_badge, $product_image, $attributes ) {
$product_permalink = $product->get_permalink();
$pointer_events = false === $attributes['showProductLink'] ? 'pointer-events: none;' : '';
$is_link = true === $attributes['showProductLink'];
$pointer_events = $is_link ? '' : 'pointer-events: none;';
$directive = $is_link ? 'data-wc-on--click="woocommerce/product-collection::actions.viewProduct"' : '';
return sprintf(
'<a href="%1$s" style="%2$s">%3$s %4$s</a>',
'<a href="%1$s" style="%2$s" %3$s>%4$s %5$s</a>',
$product_permalink,
$pointer_events,
$directive,
$on_sale_badge,
$product_image
);

View File

@@ -778,7 +778,7 @@ class ProductQuery extends AbstractBlock {
* - For array items with numeric keys, we merge them as normal.
* - For array items with string keys:
*
* - If the value isn't array, we'll use the value comming from the merge array.
* - If the value isn't array, we'll use the value coming from the merge array.
* $base = ['orderby' => 'date']
* $new = ['orderby' => 'meta_value_num']
* Result: ['orderby' => 'meta_value_num']

View File

@@ -114,7 +114,9 @@ class ProductRating extends AbstractBlock {
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product && $product->get_review_count() > 0 ) {
if ( $product && $product->get_review_count() > 0
&& $product->get_reviews_allowed()
&& wc_reviews_enabled() ) {
$product_reviews_count = $product->get_review_count();
$product_rating = $product->get_average_rating();
$parsed_attributes = $this->parse_attributes( $attributes );

View File

@@ -69,14 +69,27 @@ class ProductSKU extends AbstractBlock {
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$prefix = isset( $attributes['prefix'] ) ? wp_kses_post( ( $attributes['prefix'] ) ) : __( 'SKU: ', 'woocommerce' );
if ( ! empty( $prefix ) ) {
$prefix = sprintf( '<span class="prefix">%s</span>', $prefix );
}
$suffix = isset( $attributes['suffix'] ) ? wp_kses_post( ( $attributes['suffix'] ) ) : '';
if ( ! empty( $suffix ) ) {
$suffix = sprintf( '<span class="suffix">%s</span>', $suffix );
}
return sprintf(
'<div class="wc-block-components-product-sku wc-block-grid__product-sku wp-block-woocommerce-product-sku product_meta %1$s" style="%2$s">
SKU:
<strong class="sku">%3$s</strong>
%3$s
<strong class="sku">%4$s</strong>
%5$s
</div>',
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$product_sku
$prefix,
$product_sku,
$suffix
);
}
}

View File

@@ -85,6 +85,7 @@ class ProductTemplate extends AbstractBlock {
// Get an instance of the current Post Template block.
$block_instance = $block->parsed_block;
$product_id = get_the_ID();
// Set the block name to one that does not correspond to an existing registered block.
// This ensures that for the inner instances of the Post Template block, we do not render any block supports.
@@ -97,14 +98,39 @@ class ProductTemplate extends AbstractBlock {
$block_instance,
array(
'postType' => get_post_type(),
'postId' => get_the_ID(),
'postId' => $product_id,
)
)
)->render( array( 'dynamic' => false ) );
$interactive = array(
'namespace' => 'woocommerce/product-collection',
);
$context = array(
'productId' => $product_id,
);
$li_directives = '
data-wc-interactive=\'' . wp_json_encode( $interactive, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\'
data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) . '\'
data-wc-key="product-item-' . $product_id . '"
';
// Wrap the render inner blocks in a `li` element with the appropriate post classes.
$post_classes = implode( ' ', get_post_class( 'wc-block-product' ) );
$content .= '<li data-wc-key="product-item-' . get_the_ID() . '" class="' . esc_attr( $post_classes ) . '">' . $block_content . '</li>';
$content .= strtr(
'<li class="{classes}"
{li_directives}
>
{content}
</li>',
array(
'{classes}' => esc_attr( $post_classes ),
'{li_directives}' => $li_directives,
'{content}' => $block_content,
)
);
}
/*

View File

@@ -45,17 +45,15 @@ class StoreNotices extends AbstractBlock {
return;
}
$classname = isset( $attributes['className'] ) ? $attributes['className'] : '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
if ( isset( $attributes['align'] ) ) {
$classname .= " align{$attributes['align']}";
}
return sprintf(
'<div class="woocommerce wc-block-store-notices %1$s %2$s">%3$s</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
'<div %1$s>%2$s</div>',
get_block_wrapper_attributes(
array(
'class' => 'wc-block-store-notices woocommerce ' . esc_attr( $classes_and_styles['classes'] ),
)
),
wc_kses_notice( $notices )
);
}

View File

@@ -285,7 +285,7 @@ final class BlockTypesController {
* and prevent them from showing as an option in the Legacy Widget block.
*
* @param array $widget_types An array of widgets hidden in core.
* @return array $widget_types An array inluding the WooCommerce widgets to hide.
* @return array $widget_types An array including the WooCommerce widgets to hide.
*/
public function hide_legacy_widgets_with_block_equivalent( $widget_types ) {
array_push(
@@ -404,7 +404,6 @@ final class BlockTypesController {
// Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
// when modifying this list.
if ( Features::is_enabled( 'experimental-blocks' ) ) {
$block_types[] = 'ProductFilter';
$block_types[] = 'ProductFilters';
$block_types[] = 'ProductFiltersOverlay';
$block_types[] = 'ProductFiltersOverlayNavigation';
@@ -414,6 +413,9 @@ final class BlockTypesController {
$block_types[] = 'ProductFilterRating';
$block_types[] = 'ProductFilterActive';
$block_types[] = 'ProductFilterClearButton';
$block_types[] = 'ProductFilterCheckboxList';
$block_types[] = 'ProductFilterChips';
$block_types[] = 'OrderConfirmation\CreateAccount';
}
/**

View File

@@ -169,8 +169,6 @@ class Bootstrap {
$this->container->get( BlockPatterns::class );
$this->container->get( BlockTypesController::class );
$this->container->get( ClassicTemplatesCompatibility::class );
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
$this->container->get( SingleProductTemplateCompatibility::class )->init();
$this->container->get( Notices::class )->init();
$this->container->get( PTKPatternsStore::class );
$this->container->get( TemplateOptions::class )->init();
@@ -276,18 +274,6 @@ class Bootstrap {
return new ClassicTemplatesCompatibility( $asset_data_registry );
}
);
$this->container->register(
ArchiveProductTemplatesCompatibility::class,
function () {
return new ArchiveProductTemplatesCompatibility();
}
);
$this->container->register(
SingleProductTemplateCompatibility::class,
function () {
return new SingleProductTemplateCompatibility();
}
);
$this->container->register(
DraftOrders::class,
function ( Container $container ) {

View File

@@ -8,6 +8,8 @@ use Automattic\WooCommerce\Blocks\Images\Pexels;
/**
* AIPatterns class.
*
* @internal
*/
class AIPatterns {
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';

View File

@@ -5,6 +5,8 @@ use WP_Error;
/**
* PatternsToolkit class.
*
* @internal
*/
class PTKClient {
/**

View File

@@ -7,6 +7,8 @@ use WP_Upgrader;
/**
* PTKPatterns class.
*
* @internal
*/
class PTKPatternsStore {
const TRANSIENT_NAME = 'ptk_patterns';
@@ -92,7 +94,11 @@ class PTKPatternsStore {
*/
private function schedule_action_if_not_pending( $action ) {
$last_request = get_transient( 'last_fetch_patterns_request' );
if ( as_has_scheduled_action( $action ) || false !== $last_request ) {
// The most efficient way to check for an existing action is to use `as_has_scheduled_action`, but in unusual
// cases where another plugin has loaded a very old version of Action Scheduler, it may not be available to us.
$has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action';
if ( call_user_func( $has_scheduled_action, $action ) || false !== $last_request ) {
return;
}

View File

@@ -5,25 +5,22 @@ use Automattic\WooCommerce\Admin\Features\Features;
/**
* PatternRegistry class.
*
* @internal
*/
class PatternRegistry {
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
/**
* Associates pattern slugs with their localized labels for categorization.
* Returns pattern slugs with their localized labels for categorization.
*
* Each key represents a unique pattern slug, while the value is the localized label.
*
* @var array $category_labels
* @return array<string, string>
*/
private $category_labels;
/**
* Constructor.
*/
public function __construct() {
$this->category_labels = [
private function get_category_labels() {
return [
'woo-commerce' => __( 'WooCommerce', 'woocommerce' ),
'intro' => __( 'Intro', 'woocommerce' ),
'featured-selling' => __( 'Featured Selling', 'woocommerce' ),
@@ -146,10 +143,10 @@ class PatternRegistry {
}
}
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
$pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', 'woocommerce' );
if ( ! empty( $pattern_data['description'] ) ) {
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woocommerce' );
}
@@ -184,13 +181,15 @@ class PatternRegistry {
}
}
$category_labels = $this->get_category_labels();
if ( ! empty( $pattern_data['categories'] ) ) {
foreach ( $pattern_data['categories'] as $key => $category ) {
$category_slug = _wp_to_kebab_case( $category );
$pattern_data['categories'][ $key ] = $category_slug;
$label = isset( $this->category_labels[ $category_slug ] ) ? $this->category_labels[ $category_slug ] : self::kebab_to_capital_case( $category_slug );
$label = $category_labels[ $category_slug ] ?? self::kebab_to_capital_case( $category_slug );
register_block_pattern_category(
$category_slug,

View File

@@ -18,7 +18,7 @@ final class QueryFilters {
}
/**
* Filter the posts clauses of the main query to suport global filters.
* Filter the posts clauses of the main query to support global filters.
*
* @param array $args Query args.
* @param \WP_Query $wp_query WP_Query object.

View File

@@ -60,8 +60,6 @@ class ShippingController {
}
);
}
$this->asset_data_registry->add( 'collectableMethodIds', array( 'Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils', 'get_local_pickup_method_ids' ) );
$this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' );
add_action( 'rest_api_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );

View File

@@ -21,10 +21,6 @@ abstract class AbstractTemplateCompatibility {
* Initialization method.
*/
public function init() {
if ( ! wc_current_theme_is_fse_theme() ) {
return;
}
$this->set_hook_data();
add_filter(
@@ -61,9 +57,9 @@ abstract class AbstractTemplateCompatibility {
* @since 7.6.0
* @param boolean.
*/
$is_disabled_compatility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
if ( $is_disabled_compatility_layer ) {
if ( $is_disabled_compatibility_layer ) {
return $block_content;
}

View File

@@ -320,7 +320,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
/**
* Check if block is within the product-query namespace
* Check whether block is within the product-query namespace.
*
* @param array $block Parsed block data.
*/
@@ -331,7 +331,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
/**
* Check if block has isInherited attribute asigned
* Check whether block has isInherited attribute assigned.
*
* @param array $block Parsed block data.
*/
@@ -357,7 +357,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
/**
* Check if block is a Post template
* Check whether block is a Post template.
*
* @param string $block_name Block name.
*/
@@ -366,7 +366,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
/**
* Check if block is a Product Template
* Check whether block is a Product Template.
*
* @param string $block_name Block name.
*/
@@ -375,7 +375,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
/**
* Check if block is eaither a Post template or Product Template
* Check if block is either a Post template or a Product Template
*
* @param string $block_name Block name.
*/

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
@@ -61,6 +62,9 @@ class ProductAttributeTemplate extends AbstractTemplate {
}
if ( isset( $queried_object->taxonomy ) && taxonomy_is_product_attribute( $queried_object->taxonomy ) ) {
$compatibility_layer = new ArchiveProductTemplatesCompatibility();
$compatibility_layer->init();
$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
@@ -49,6 +50,9 @@ class ProductCatalogTemplate extends AbstractTemplate {
*/
public function render_block_template() {
if ( ! is_embed() && ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) ) {
$compatibility_layer = new ArchiveProductTemplatesCompatibility();
$compatibility_layer->init();
$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
@@ -55,6 +56,9 @@ class ProductCategoryTemplate extends AbstractTemplate {
*/
public function render_block_template() {
if ( ! is_embed() && is_product_taxonomy() && is_tax( 'product_cat' ) ) {
$compatibility_layer = new ArchiveProductTemplatesCompatibility();
$compatibility_layer->init();
$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
@@ -48,6 +49,9 @@ class ProductSearchResultsTemplate extends AbstractTemplate {
*/
public function render_block_template() {
if ( ! is_embed() && is_post_type_archive( 'product' ) && is_search() ) {
$compatibility_layer = new ArchiveProductTemplatesCompatibility();
$compatibility_layer->init();
$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
@@ -55,6 +56,9 @@ class ProductTagTemplate extends AbstractTemplate {
*/
public function render_block_template() {
if ( ! is_embed() && is_product_taxonomy() && is_tax( 'product_tag' ) ) {
$compatibility_layer = new ArchiveProductTemplatesCompatibility();
$compatibility_layer->init();
$templates = get_block_templates( array( 'slug__in' => array( self::SLUG ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {

View File

@@ -23,7 +23,7 @@ class SingleProductTemplate extends AbstractTemplate {
*/
public function init() {
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'get_block_templates', array( $this, 'update_single_product_content' ), 11, 3 );
add_filter( 'get_block_templates', array( $this, 'update_single_product_content' ), 11, 1 );
}
/**
@@ -51,13 +51,34 @@ class SingleProductTemplate extends AbstractTemplate {
if ( ! is_embed() && is_singular( 'product' ) ) {
global $post;
$valid_slugs = array( self::SLUG );
if ( 'product' === $post->post_type && $post->post_name ) {
$compatibility_layer = new SingleProductTemplateCompatibility();
$compatibility_layer->init();
$valid_slugs = array( self::SLUG );
$single_product_slug = 'product' === $post->post_type && $post->post_name ? 'single-product-' . $post->post_name : '';
if ( $single_product_slug ) {
$valid_slugs[] = 'single-product-' . $post->post_name;
}
$templates = get_block_templates( array( 'slug__in' => $valid_slugs ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
if ( count( $templates ) === 0 ) {
return;
}
// Use the first template by default.
$template = $templates[0];
// Check if there is a template matching the slug `single-product-{post_name}`.
if ( count( $valid_slugs ) > 1 && count( $templates ) > 1 ) {
foreach ( $templates as $t ) {
if ( $single_product_slug === $t->slug ) {
$template = $t;
break;
}
}
}
if ( isset( $template ) && BlockTemplateUtils::template_has_legacy_template_block( $template ) ) {
add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
}
@@ -68,12 +89,10 @@ class SingleProductTemplate extends AbstractTemplate {
/**
* Add the block template objects to be used.
*
* @param array $query_result Array of template objects.
* @param array $query Optional. Arguments to retrieve templates.
* @param string $template_type wp_template or wp_template_part.
* @param array $query_result Array of template objects.
* @return array
*/
public function update_single_product_content( $query_result, $query, $template_type ) {
public function update_single_product_content( $query_result ) {
$query_result = array_map(
function ( $template ) {
if ( str_contains( $template->slug, self::SLUG ) ) {

View File

@@ -1,6 +1,8 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* SingleProductTemplateCompatibility class.
*
@@ -12,7 +14,6 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
const IS_FIRST_BLOCK = '__wooCommerceIsFirstBlock';
const IS_LAST_BLOCK = '__wooCommerceIsLastBlock';
/**
* Inject hooks to rendered content of corresponding blocks.
*
@@ -257,15 +258,11 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
* @return string
*/
public static function add_compatibility_layer( $template_content ) {
$parsed_blocks = parse_blocks( $template_content );
if ( ! self::has_single_product_template_blocks( $parsed_blocks ) ) {
$template = self::inject_custom_attributes_to_first_and_last_block_single_product_template( $parsed_blocks );
return self::serialize_blocks( $template );
$blocks = parse_blocks( $template_content );
if ( self::has_single_product_template_blocks( $blocks ) ) {
$blocks = self::wrap_single_product_template( $template_content );
}
$wrapped_blocks = self::wrap_single_product_template( $template_content );
$template = self::inject_custom_attributes_to_first_and_last_block_single_product_template( $wrapped_blocks );
$template = self::inject_custom_attributes_to_first_and_last_block_single_product_template( $blocks );
return self::serialize_blocks( $template );
}
@@ -385,7 +382,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
/**
* Check if the Single Product template has a single product template block:
* woocommerce/product-gallery-image, woocommerce/product-details, woocommerce/add-to-cart-form]
* woocommerce/product-gallery-image, woocommerce/product-details, woocommerce/add-to-cart-form, etc.
*
* @param array $parsed_blocks Array of parsed block objects.
* @return bool True if the template has a single product template block, false otherwise.
@@ -393,19 +390,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
private static function has_single_product_template_blocks( $parsed_blocks ) {
$single_product_template_blocks = array( 'woocommerce/product-image-gallery', 'woocommerce/product-details', 'woocommerce/add-to-cart-form', 'woocommerce/product-meta', 'woocommerce/product-price', 'woocommerce/breadcrumbs' );
$found = false;
foreach ( $parsed_blocks as $block ) {
if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $single_product_template_blocks, true ) ) {
$found = true;
break;
}
$found = self::has_single_product_template_blocks( $block['innerBlocks'], $single_product_template_blocks );
if ( $found ) {
break;
}
}
return $found;
return BlockTemplateUtils::has_block_including_patterns( $single_product_template_blocks, $parsed_blocks );
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\Utils;
use WP_Block_Patterns_Registry;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Options;
use Automattic\WooCommerce\Blocks\Package;
@@ -480,7 +481,7 @@ class BlockTemplateUtils {
* @return boolean
*/
public static function theme_has_template( $template_name ) {
return ! ! self::get_theme_template_path( $template_name, 'wp_template' );
return (bool) self::get_theme_template_path( $template_name, 'wp_template' );
}
/**
@@ -490,7 +491,7 @@ class BlockTemplateUtils {
* @return boolean
*/
public static function theme_has_template_part( $template_name ) {
return ! ! self::get_theme_template_path( $template_name, 'wp_template_part' );
return (bool) self::get_theme_template_path( $template_name, 'wp_template_part' );
}
/**
@@ -696,6 +697,38 @@ class BlockTemplateUtils {
return wc_string_to_bool( $use_blockified_templates );
}
/**
* Determines whether the provided $blocks contains any of the $block_names,
* or if they contain a pattern that contains any of the $block_names.
*
* @param string[] $block_names Full block types to look for.
* @param WP_Block[] $blocks Array of block objects.
* @return bool Whether the content contains the specified block.
*/
public static function has_block_including_patterns( $block_names, $blocks ) {
$flattened_blocks = self::flatten_blocks( $blocks );
foreach ( $flattened_blocks as &$block ) {
if ( isset( $block['blockName'] ) && in_array( $block['blockName'], $block_names, true ) ) {
return true;
}
if (
'core/pattern' === $block['blockName'] &&
isset( $block['attrs']['slug'] )
) {
$registry = WP_Block_Patterns_Registry::get_instance();
$pattern = $registry->get_registered( $block['attrs']['slug'] );
$pattern_blocks = parse_blocks( $pattern['content'] );
if ( self::has_block_including_patterns( $block_names, $pattern_blocks ) ) {
return true;
}
}
}
return false;
}
/**
* Returns whether the passed `$template` has the legacy template block.
*
@@ -703,7 +736,13 @@ class BlockTemplateUtils {
* @return boolean
*/
public static function template_has_legacy_template_block( $template ) {
return has_block( 'woocommerce/legacy-template', $template->content );
if ( has_block( 'woocommerce/legacy-template', $template->content ) ) {
return true;
}
$blocks = parse_blocks( $template->content );
return self::has_block_including_patterns( array( 'woocommerce/legacy-template' ), $blocks );
}
/**

View File

@@ -108,7 +108,7 @@ class CartCheckoutUtils {
}
$array_without_accents = array_map(
function( $value ) {
function ( $value ) {
return is_array( $value )
? self::deep_sort_with_accents( $value )
: remove_accents( wc_strtolower( html_entity_decode( $value ) ) );
@@ -129,7 +129,7 @@ class CartCheckoutUtils {
$shipping_zones = \WC_Shipping_Zones::get_zones();
$formatted_shipping_zones = array_reduce(
$shipping_zones,
function( $acc, $zone ) {
function ( $acc, $zone ) {
$acc[] = [
'id' => $zone['id'],
'title' => $zone['zone_name'],
@@ -146,4 +146,47 @@ class CartCheckoutUtils {
];
return $formatted_shipping_zones;
}
/**
* Recursively search the checkout block to find the express checkout block and
* get the button style attributes
*
* @param array $blocks Blocks to search.
* @param string $cart_or_checkout The block type to check.
*/
public static function find_express_checkout_attributes( $blocks, $cart_or_checkout ) {
$express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block';
foreach ( $blocks as $block ) {
if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] && ! empty( $block['attrs'] ) ) {
return $block['attrs'];
}
if ( ! empty( $block['innerBlocks'] ) ) {
$answer = self::find_express_checkout_attributes( $block['innerBlocks'], $cart_or_checkout );
if ( $answer ) {
return $answer;
}
}
}
}
/**
* Given an array of blocks, find the express payment block and update its attributes.
*
* @param array $blocks Blocks to search.
* @param string $cart_or_checkout The block type to check.
* @param array $updated_attrs The new attributes to set.
*/
public static function update_blocks_with_new_attrs( &$blocks, $cart_or_checkout, $updated_attrs ) {
$express_block_name = 'woocommerce/' . $cart_or_checkout . '-express-payment-block';
foreach ( $blocks as $key => &$block ) {
if ( ! empty( $block['blockName'] ) && $express_block_name === $block['blockName'] ) {
$blocks[ $key ]['attrs'] = $updated_attrs;
}
if ( ! empty( $block['innerBlocks'] ) ) {
self::update_blocks_with_new_attrs( $block['innerBlocks'], $cart_or_checkout, $updated_attrs );
}
}
}
}

View File

@@ -26,7 +26,7 @@ class ProductGalleryUtils {
$product_gallery_images = array();
$product = wc_get_product( $post_id );
if ( $product ) {
if ( $product instanceof \WC_Product ) {
$all_product_gallery_image_ids = self::get_product_gallery_image_ids( $product );
if ( 'full' === $size || 'full' !== $size && count( $all_product_gallery_image_ids ) > 1 ) {