plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\Assets;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
use Exception;
use InvalidArgumentException;
@@ -89,6 +90,7 @@ class AssetDataRegistry {
'dateFormat' => wc_date_format(),
'homeUrl' => esc_url( home_url( '/' ) ),
'locale' => $this->get_locale_data(),
'isRemoteLoggingEnabled' => wc_get_container()->get( RemoteLogger::class )->is_remote_logging_allowed(),
'dashboardUrl' => wc_get_account_endpoint_url( 'dashboard' ),
'orderStatuses' => $this->get_order_statuses(),
'placeholderImgSrc' => wc_placeholder_img_src(),

View File

@@ -1,7 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate;
@@ -27,7 +26,7 @@ class BlockTemplatesController {
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_fallback_template_to_hierarchy' ), 10, 1 );
add_filter( 'block_type_metadata_settings', array( $this, 'add_plugin_templates_parts_support' ), 10, 2 );
add_filter( 'block_type_metadata_settings', array( $this, 'prevent_shortcodes_html_breakage' ), 10, 2 );
add_action( 'current_screen', array( $this, 'hide_template_selector_in_cart_checkout_pages' ), 10 );
@@ -54,6 +53,10 @@ class BlockTemplatesController {
* This function is used on the `pre_get_block_template` hook to return the fallback template from the db in case
* the template is eligible for it.
*
* Currently, the Products by Category, Products by Tag and Products by Attribute templates fall back to the
* Product Catalog template. That means that if there are customizations in the Product Catalog template,
* they are also reflected in the other templates as long as they haven't been customized as well.
*
* @param \WP_Block_Template|null $template Block template object to short-circuit the default query,
* or null to allow WP to run its normal queries.
* @param string $id Template unique identifier (example: theme_slug//template_slug).
@@ -76,13 +79,14 @@ class BlockTemplatesController {
$template_name_parts = explode( '//', $id );
$theme = $template_name_parts[0] ?? '';
$slug = $template_name_parts[1] ?? '';
$registered_template = BlockTemplateUtils::get_template( $slug );
if ( empty( $theme ) || empty( $slug ) || ! BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $slug ) ) {
if ( empty( $theme ) || empty( $slug ) || ! $registered_template || ! isset( $registered_template->fallback_template ) ) {
return null;
}
$wp_query_args = array(
'post_name__in' => array( ProductCatalogTemplate::SLUG, $slug ),
'post_name__in' => array( $registered_template->fallback_template, $slug ),
'post_type' => $template_type,
'post_status' => array( 'auto-draft', 'draft', 'publish', 'trash' ),
'no_found_rows' => true,
@@ -98,12 +102,12 @@ class BlockTemplatesController {
$posts = $template_query->posts;
// If we have more than one result from the query, it means that the current template is present in the db (has
// been customized by the user) and we should not return the `archive-product` template.
// been customized by the user) and we should not return the fallback template.
if ( count( $posts ) > 1 ) {
return null;
}
if ( count( $posts ) > 0 && ProductCatalogTemplate::SLUG === $posts[0]->post_name ) {
if ( count( $posts ) > 0 && $registered_template->fallback_template === $posts[0]->post_name ) {
$template = _build_block_template_result_from_post( $posts[0] );
if ( ! is_wp_error( $template ) ) {
@@ -121,26 +125,21 @@ class BlockTemplatesController {
}
/**
* Adds the `archive-product` template to the `taxonomy-product_cat`, `taxonomy-product_tag`, `taxonomy-attribute`
* templates to be able to fall back to it.
* Adds the fallback template to the template hierarchy.
*
* @param array $template_hierarchy A list of template candidates, in descending order of priority.
*/
public function add_archive_product_to_eligible_for_fallback_templates( $template_hierarchy ) {
public function add_fallback_template_to_hierarchy( $template_hierarchy ) {
$template_slugs = array_map(
'_strip_template_file_suffix',
$template_hierarchy
);
$templates_eligible_for_fallback = array_filter(
$template_slugs,
function ( $template_slug ) {
return BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $template_slug );
foreach ( $template_slugs as $template_slug ) {
$template = BlockTemplateUtils::get_template( $template_slug );
if ( $template && isset( $template->fallback_template ) ) {
$template_hierarchy[] = $template->fallback_template;
}
);
if ( count( $templates_eligible_for_fallback ) > 0 ) {
$template_hierarchy[] = ProductCatalogTemplate::SLUG;
}
return $template_hierarchy;
@@ -233,10 +232,12 @@ class BlockTemplatesController {
list( $template_id, $template_slug ) = $template_name_parts;
// If the theme has an archive-product.html template, but not a taxonomy-product_cat/tag/attribute.html template let's use the themes archive-product.html template.
if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback_from_theme( $template_slug ) ) {
$template_path = BlockTemplateUtils::get_theme_template_path( ProductCatalogTemplate::SLUG );
$template_object = BlockTemplateUtils::create_new_block_template_object( $template_path, $template_type, $template_slug, true );
// If the template is not present in the theme but its fallback template is,
// let's use the theme's fallback template.
if ( BlockTemplateUtils::template_is_eligible_for_fallback_from_theme( $template_slug ) ) {
$registered_template = BlockTemplateUtils::get_template( $template_slug );
$template_path = BlockTemplateUtils::get_theme_template_path( $registered_template->fallback_template );
$template_object = BlockTemplateUtils::create_new_block_template_object( $template_path, $template_type, $template_slug, true );
return BlockTemplateUtils::build_template_result_from_file( $template_object, $template_type );
}
@@ -441,7 +442,7 @@ class BlockTemplatesController {
continue;
}
if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback_from_db( $template_slug, $already_found_templates ) ) {
if ( BlockTemplateUtils::template_is_eligible_for_fallback_from_db( $template_slug, $already_found_templates ) ) {
$template = clone BlockTemplateUtils::get_fallback_template_from_db( $template_slug, $already_found_templates );
$template_id = explode( '//', $template->id );
$template->id = $template_id[0] . '//' . $template_slug;
@@ -452,10 +453,12 @@ class BlockTemplatesController {
continue;
}
// If the theme has an archive-product.html template, but not a taxonomy-product_cat/tag/attribute.html template let's use the themes archive-product.html template.
if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback_from_theme( $template_slug ) ) {
$template_file = BlockTemplateUtils::get_theme_template_path( ProductCatalogTemplate::SLUG );
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug, true );
// If the template is not present in the theme but its fallback template is,
// let's use the theme's fallback template.
if ( BlockTemplateUtils::template_is_eligible_for_fallback_from_theme( $template_slug ) ) {
$registered_template = BlockTemplateUtils::get_template( $template_slug );
$template_file = BlockTemplateUtils::get_theme_template_path( $registered_template->fallback_template );
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug, true );
continue;
}

View File

@@ -10,8 +10,6 @@ use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutHeaderTemplate;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonEntireSiteTemplate;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonStoreOnlyTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;

View File

@@ -450,14 +450,16 @@ abstract class AbstractBlock {
'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ),
];
if ( is_admin() && ! WC()->is_rest_api_request() ) {
$wc_blocks_config = array_merge(
$product_counts = wp_count_posts( 'product' );
$published_products = isset( $product_counts->publish ) ? $product_counts->publish : 0;
$wc_blocks_config = array_merge(
$wc_blocks_config,
[
// Note that while we don't have a consolidated way of doing feature-flagging
// we are borrowing from the WC Admin Features implementation. Also note we cannot
// use the wcAdminFeatures global because it's not always enqueued in the context of blocks.
'experimentalBlocksEnabled' => Features::is_enabled( 'experimental-blocks' ),
'productCount' => array_sum( (array) wp_count_posts( 'product' ) ),
'productCount' => $published_products,
]
);
}

View File

@@ -192,7 +192,7 @@ class AddToCartForm extends AbstractBlock {
}
/**
* It isn't necessary register block assets because it is a server side block.
* It isn't necessary to register block assets because it is a server side block.
*/
protected function register_block_type_assets() {
return null;

View File

@@ -245,7 +245,6 @@ 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() );
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() );
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings();
$this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] );

View File

@@ -156,35 +156,82 @@ class ProductCollection extends AbstractBlock {
}
/**
* Enhances the Product Collection block with client-side pagination.
* Check if next tag is a PC block.
*
* This function identifies Product Collection blocks and adds necessary data attributes
* to enable client-side navigation and animation effects. It also enqueues the Interactivity API runtime.
* @param WP_HTML_Tag_processor $p Initial tag processor.
*
* @return bool Answer if PC block is available.
*/
private function is_next_tag_product_collection( $p ) {
return $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) );
}
/**
* Set PC block namespace for Interactivity API.
*
* @param WP_HTML_Tag_processor $p Initial tag processor.
*/
private function set_product_collection_namespace( $p ) {
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
}
/**
* Attach the init directive to Product Collection block to call
* the onRender callback.
*
* @param string $block_content The HTML content of the block.
* @param array $block Block details, including its attributes.
* @param string $collection Collection type.
*
* @return string Updated block content with added interactivity attributes.
* @return string Updated HTML content.
*/
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
$is_enhanced_pagination_enabled = ! ( $block['attrs']['forcePageReload'] ?? false );
if ( $is_product_collection_block && $is_enhanced_pagination_enabled ) {
// Enqueue the Interactivity API runtime.
wp_enqueue_script( 'wc-interactivity' );
private function add_rendering_callback( $block_content, $collection ) {
$p = new \WP_HTML_Tag_Processor( $block_content );
$p = new \WP_HTML_Tag_Processor( $block_content );
// Add `data-wc-navigation-id to the product collection block.
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) ) ) {
$p->set_attribute(
'data-wc-navigation-id',
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
);
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
// Add `data-init to the product collection block so we trigger JS event on render.
if ( $this->is_next_tag_product_collection( $p ) ) {
$p->set_attribute(
'data-wc-init',
'callbacks.onRender'
);
if ( $collection ) {
$p->set_attribute(
'data-wc-context',
wp_json_encode(
array(
'collection' => $collection,
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
}
}
return $p->get_updated_html();
}
/**
* Attach all the Interactivity API directives responsible
* for client-side navigation.
*
* @param string $block_content The HTML content of the block.
*
* @return string Updated HTML content.
*/
private function enable_client_side_navigation( $block_content ) {
$p = new \WP_HTML_Tag_Processor( $block_content );
// Add `data-wc-navigation-id to the product collection block.
if ( $this->is_next_tag_product_collection( $p ) ) {
$p->set_attribute(
'data-wc-navigation-id',
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
);
$current_context = json_decode( $p->get_attribute( 'data-wc-context' ) ?? '{}', true );
$p->set_attribute(
'data-wc-context',
wp_json_encode(
array_merge(
$current_context,
array(
// The message to be announced by the screen reader when the page is loading or loaded.
'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
@@ -193,19 +240,20 @@ class ProductCollection extends AbstractBlock {
// This way we avoid prefetching when the page loads.
'isPrefetchNextOrPreviousLink' => false,
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
$block_content = $p->get_updated_html();
}
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
$block_content = $p->get_updated_html();
}
/**
* Add two div's:
* 1. Pagination animation for visual users.
* 2. Accessibility div for screen readers, to announce page load states.
*/
$last_tag_position = strripos( $block_content, '</div>' );
$accessibility_and_animation_html = '
/**
* Add two div's:
* 1. Pagination animation for visual users.
* 2. Accessibility div for screen readers, to announce page load states.
*/
$last_tag_position = strripos( $block_content, '</div>' );
$accessibility_and_animation_html = '
<div
data-wc-interactive="{&quot;namespace&quot;:&quot;woocommerce/product-collection&quot;}"
class="wc-block-product-collection__pagination-animation"
@@ -219,12 +267,44 @@ class ProductCollection extends AbstractBlock {
data-wc-text="context.accessibilityMessage">
</div>
';
$block_content = substr_replace(
$block_content,
$accessibility_and_animation_html,
$last_tag_position,
0
);
return substr_replace(
$block_content,
$accessibility_and_animation_html,
$last_tag_position,
0
);
}
/**
* Enhances the Product Collection block with client-side pagination.
*
* This function identifies Product Collection blocks and adds necessary data attributes
* to enable client-side navigation and animation effects. It also enqueues the Interactivity API runtime.
*
* @param string $block_content The HTML content of the block.
* @param array $block Block details, including its attributes.
*
* @return string Updated block content with added interactivity attributes.
*/
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
if ( $is_product_collection_block ) {
// Enqueue the Interactivity API runtime and set the namespace.
wp_enqueue_script( 'wc-interactivity' );
$p = new \WP_HTML_Tag_Processor( $block_content );
if ( $this->is_next_tag_product_collection( $p ) ) {
$this->set_product_collection_namespace( $p );
}
$block_content = $p->get_updated_html();
$collection = $block['attrs']['collection'] ?? '';
$block_content = $this->add_rendering_callback( $block_content, $collection );
$is_enhanced_pagination_enabled = ! ( $block['attrs']['forcePageReload'] ?? false );
if ( $is_enhanced_pagination_enabled ) {
$block_content = $this->enable_client_side_navigation( $block_content );
}
}
return $block_content;
@@ -295,7 +375,7 @@ class ProductCollection extends AbstractBlock {
'class_name' => $class_name,
)
) ) {
$processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$this->set_product_collection_namespace( $processor );
$processor->set_attribute( 'data-wc-on--click', 'actions.navigate' );
$processor->set_attribute( 'data-wc-key', $key_prefix . '--' . esc_attr( wp_rand() ) );
@@ -314,9 +394,11 @@ class ProductCollection extends AbstractBlock {
*/
private function is_block_compatible( $block_name ) {
// Check for explicitly unsupported blocks.
if ( 'core/post-content' === $block_name ||
if (
'core/post-content' === $block_name ||
'woocommerce/mini-cart' === $block_name ||
'woocommerce/featured-product' === $block_name ) {
'woocommerce/featured-product' === $block_name
) {
return false;
}
@@ -835,7 +917,7 @@ class ProductCollection extends AbstractBlock {
if ( ! isset( $base[ $key ] ) ) {
$base[ $key ] = array();
}
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
} else {
$base[ $key ] = $value;
}
@@ -1092,7 +1174,7 @@ class ProductCollection extends AbstractBlock {
$max_price_query = empty( $max_price ) ? array() : array(
'key' => '_price',
'value' => $max_price,
'compare' => '<',
'compare' => '<=',
'type' => 'numeric',
);

View File

@@ -30,6 +30,34 @@ final class ProductFilterAttribute extends AbstractBlock {
add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 );
add_filter( 'collection_active_filters_data', array( $this, 'register_active_filters_data' ), 10, 2 );
add_action( 'deleted_transient', array( $this, 'delete_default_attribute_id_transient' ) );
add_action( 'wp_loaded', array( $this, 'register_block_patterns' ) );
}
/**
* 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 );
if ( is_admin() ) {
$this->asset_data_registry->add( 'defaultProductFilterAttribute', $this->get_default_product_attribute() );
}
}
/**
* Delete the default attribute id transient when the attribute taxonomies are deleted.
*
* @param string $transient The transient name.
*/
public function delete_default_attribute_id_transient( $transient ) {
if ( 'wc_attribute_taxonomies' === $transient ) {
delete_transient( 'wc_block_product_filter_attribute_default_attribute' );
}
}
/**
@@ -43,7 +71,7 @@ final class ProductFilterAttribute extends AbstractBlock {
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
$attribute_param_keys = array_filter(
$url_param_keys,
function( $param ) {
function ( $param ) {
return strpos( $param, 'filter_' ) === 0 || strpos( $param, 'query_type_' ) === 0;
}
);
@@ -64,7 +92,7 @@ final class ProductFilterAttribute extends AbstractBlock {
public function register_active_filters_data( $data, $params ) {
$product_attributes_map = array_reduce(
wc_get_attribute_taxonomies(),
function( $acc, $attribute_object ) {
function ( $acc, $attribute_object ) {
$acc[ $attribute_object->attribute_name ] = $attribute_object->attribute_label;
return $acc;
},
@@ -73,7 +101,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$active_product_attributes = array_reduce(
array_keys( $params ),
function( $acc, $attribute ) {
function ( $acc, $attribute ) {
if ( strpos( $attribute, 'filter_' ) === 0 ) {
$acc[] = str_replace( 'filter_', '', $attribute );
}
@@ -84,7 +112,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$active_product_attributes = array_filter(
$active_product_attributes,
function( $item ) use ( $product_attributes_map ) {
function ( $item ) use ( $product_attributes_map ) {
return in_array( $item, array_keys( $product_attributes_map ), true );
}
);
@@ -96,7 +124,7 @@ final class ProductFilterAttribute extends AbstractBlock {
// Get attribute term by slug.
$terms = array_map(
function( $term ) use ( $product_attribute, $action_namespace ) {
function ( $term ) use ( $product_attribute, $action_namespace ) {
$term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" );
return array(
'title' => $term_object->name,
@@ -134,6 +162,11 @@ final class ProductFilterAttribute extends AbstractBlock {
* @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;
}
// don't render if its admin, or ajax in progress.
if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) {
return '';
@@ -169,7 +202,7 @@ final class ProductFilterAttribute extends AbstractBlock {
);
$attribute_options = array_map(
function( $term ) use ( $attribute_counts, $selected_terms ) {
function ( $term ) use ( $attribute_counts, $selected_terms ) {
$term = (array) $term;
$term['count'] = $attribute_counts[ $term['term_id'] ];
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
@@ -180,7 +213,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$filtered_options = array_filter(
$attribute_options,
function( $option ) {
function ( $option ) {
return $option['count'] > 0;
}
);
@@ -192,7 +225,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$context = array(
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
'queryType' => $attributes['queryType'],
'selectType' => $attributes['selectType'],
'selectType' => 'multiple',
);
return sprintf(
@@ -243,7 +276,7 @@ final class ProductFilterAttribute extends AbstractBlock {
'items' => $list_items,
'action' => "{$this->get_full_block_name()}::actions.navigate",
'selected_items' => $selected_items,
'select_type' => $attributes['selectType'] ?? 'multiple',
'select_type' => 'multiple',
// translators: %s is a product attribute name.
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ),
)
@@ -265,7 +298,7 @@ final class ProductFilterAttribute extends AbstractBlock {
$show_counts = $attributes['showCounts'] ?? false;
$list_options = array_map(
function( $option ) use ( $show_counts ) {
function ( $option ) use ( $show_counts ) {
return array(
'id' => $option['slug'] . '-' . $option['term_id'],
'checked' => $option['selected'],
@@ -325,9 +358,128 @@ final class ProductFilterAttribute extends AbstractBlock {
$acc[ $count['term'] ] = $count['count'];
return $acc;
},
[]
array()
);
return $attribute_counts;
}
/**
* Get the attribute if with most term but closest to 30 terms.
*
* @return object
*/
private function get_default_product_attribute() {
// Cache this variable in memory to prevent repeated database queries to check
// for transient in the same request.
static $cached = null;
if ( $cached ) {
return $cached;
}
$cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' );
if ( $cached ) {
return $cached;
}
$attributes = wc_get_attribute_taxonomies();
$attributes_count = array_map(
function ( $attribute ) {
return intval(
wp_count_terms(
array(
'taxonomy' => 'pa_' . $attribute->attribute_name,
'hide_empty' => false,
)
)
);
},
$attributes
);
asort( $attributes_count );
$search = 30;
$closest = null;
$attribute_id = null;
foreach ( $attributes_count as $id => $count ) {
if ( null === $closest || abs( $search - $closest ) > abs( $count - $search ) ) {
$closest = $count;
$attribute_id = $id;
}
if ( $closest && $count >= $search ) {
break;
}
}
$default_attribute = (object) array(
'attribute_id' => '0',
'attribute_name' => 'attribute',
'attribute_label' => __( 'Attribute', 'woocommerce' ),
'attribute_type' => 'select',
'attribute_orderby' => 'menu_order',
'attribute_public' => 0,
);
if ( $attribute_id ) {
$default_attribute = $attributes[ $attribute_id ];
}
set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute );
return $default_attribute;
}
/**
* Register pattern for default product attribute.
*/
public function register_block_patterns() {
$default_attribute = $this->get_default_product_attribute();
register_block_pattern(
'woocommerce/default-attribute-filter',
array(
'title' => '',
'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-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 -->
',
array(
'{{attribute_id}}' => intval( $default_attribute->attribute_id ),
'{{attribute_label}}' => esc_html( $default_attribute->attribute_label ),
)
),
)
);
}
}

View File

@@ -171,7 +171,8 @@ final class ProductFilterPrice extends AbstractBlock {
type="text"
value="%s"
data-wc-bind--value="state.formattedMinPrice"
data-wc-on--change="actions.updateProducts"
data-wc-on--input="actions.updateProducts"
data-wc-on--focus="actions.selectInputContent"
pattern=""
/>',
wp_strip_all_tags( $formatted_min_price )
@@ -189,7 +190,8 @@ final class ProductFilterPrice extends AbstractBlock {
type="text"
value="%s"
data-wc-bind--value="state.formattedMaxPrice"
data-wc-on--change="actions.updateProducts"
data-wc-on--input="actions.updateProducts"
data-wc-on--focus="actions.selectInputContent"
/>',
wp_strip_all_tags( $formatted_max_price )
) : sprintf(

View File

@@ -11,4 +11,87 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
* @var string
*/
protected $block_name = 'product-filters-overlay-navigation';
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
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.
*
* @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 ) {
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => 'wc-block-product-filters-overlay-navigation',
)
);
$overlay_mode = $block->context['woocommerce/product-filters/overlay'];
if ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) {
return null;
}
$html_content = strtr(
'<div {{wrapper_attributes}}>
{{primary_content}}
{{secondary_content}}
</div>',
array(
'{{wrapper_attributes}}' => $wrapper_attributes,
'{{primary_content}}' => 'open-overlay' === $attributes['triggerType'] ? $this->render_icon( $attributes ) : $this->render_label( $attributes ),
'{{secondary_content}}' => 'open-overlay' === $attributes['triggerType'] ? $this->render_label( $attributes ) : $this->render_icon( $attributes ),
)
);
return $html_content;
}
/**
* Gets the icon to render depending on the triggerType attribute.
*
* @param array $attributes Block attributes.
*
* @return string Label to render on the block
*/
private function render_icon( $attributes ) {
if ( 'open-overlay' === $attributes['triggerType'] ) {
return '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" style="width: 16px; height: 16px;"><path d="M10 17.5H14V16H10V17.5ZM6 6V7.5H18V6H6ZM8 12.5H16V11H8V12.5Z" fill="currentColor"></path></svg>';
}
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true" focusable="false" style="width: 16px; height: 16px;"><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>';
}
/**
* Gets the label to render depending on the triggerType.
*
* @param array $attributes Block attributes.
*
* @return string Label to render on the block
*/
private function render_label( $attributes ) {
return sprintf(
'<span>%s</span>',
'open-overlay' === $attributes['triggerType'] ? __( 'Filters', 'woocommerce' ) : __( 'Close', 'woocommerce' )
);
}
}

View File

@@ -74,6 +74,10 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
return '';
}
$product_gallery = $product->get_gallery_image_ids();
if ( empty( $product_gallery ) ) {

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ProductMeta class.
*/
class ProductMeta extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-meta';
/**
* Get the editor script data for this block type.
*
* @param string $key Data to get, or default to everything.
* @return null
*/
protected function get_block_type_editor_script( $key = null ) {
return null;
}
/**
* Get the editor style handle for this block type.
*
* @return null
*/
protected function get_block_type_editor_style() {
return null;
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
* @return null
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -619,7 +619,7 @@ class ProductQuery extends AbstractBlock {
$max_price_query = empty( $max_price ) ? array() : [
'key' => '_price',
'value' => $max_price,
'compare' => '<',
'compare' => '<=',
'type' => 'numeric',
];

View File

@@ -17,6 +17,18 @@ class ProductTemplate extends AbstractBlock {
*/
protected $block_name = 'product-template';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
* - Hook into pre_render_block to update the query.
*/
protected function initialize() {
add_filter( 'block_type_metadata_settings', array( $this, 'add_block_type_metadata_settings' ), 10, 2 );
parent::initialize();
}
/**
* Get the frontend script handle for this block type.
*
@@ -134,4 +146,19 @@ class ProductTemplate extends AbstractBlock {
return false;
}
/**
* Product Template renders inner blocks manually so we need to skip default
* rendering routine for its inner blocks
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
* @return array
*/
public function add_block_type_metadata_settings( $settings, $metadata ) {
if ( ! empty( $metadata['name'] ) && 'woocommerce/product-template' === $metadata['name'] ) {
$settings['skip_inner_blocks'] = true;
}
return $settings;
}
}

View File

@@ -350,6 +350,7 @@ final class BlockTypesController {
'ProductGalleryThumbnails',
'ProductImage',
'ProductImageGallery',
'ProductMeta',
'ProductNew',
'ProductOnSale',
'ProductPrice',
@@ -437,7 +438,6 @@ final class BlockTypesController {
$block_types = array_diff(
$block_types,
array(
'AddToCartForm',
'Breadcrumbs',
'CatalogSorting',
'ClassicTemplate',

View File

@@ -508,6 +508,7 @@ class CheckoutFields {
'hidden' => false,
'autocomplete' => 'email',
'autocapitalize' => 'none',
'type' => 'email',
'index' => 0,
],
'country' => [
@@ -780,7 +781,7 @@ class CheckoutFields {
* @return mixed
*/
public function update_default_locale_with_fields( $locale ) {
foreach ( $this->fields_locations['address'] as $field_id => $additional_field ) {
foreach ( $this->get_fields_for_location( 'address' ) as $field_id => $additional_field ) {
if ( empty( $locale[ $field_id ] ) ) {
$locale[ $field_id ] = $additional_field;
}

View File

@@ -28,50 +28,82 @@ class CheckboxList {
$items = $props['items'] ?? array();
$checkbox_list_context = array( 'items' => $items );
$on_change = $props['on_change'] ?? '';
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
$checked_items = array_filter(
$items,
function ( $item ) {
return $item['checked'];
}
);
$show_initially = $props['show_initially'] ?? 15;
$remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items );
$count = 0;
ob_start();
?>
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
<div data-wc-context='<?php echo wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); ?>' >
<div class="wc-block-stock-filter style-list">
<ul class="wc-block-components-checkbox-list">
<?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'] ); ?>">
<div class="wc-block-components-checkbox">
<label for="<?php echo esc_attr( $item['id'] ); ?>">
<input
id="<?php echo esc_attr( $item['id'] ); ?>"
class="wc-block-components-checkbox__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( $on_change ); ?>"
value="<?php echo esc_attr( $item['value'] ); ?>"
<?php checked( $item['checked'], 1 ); ?>
>
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
</svg>
<span class="wc-block-components-checkbox__label">
<?php // The label can be HTML, so we don't want to escape it. ?>
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $item['label']; ?>
</span>
</label>
</div>
</li>
<?php } ?>
</ul>
</div>
</div>
<div
class="wc-block-interactivity-components-checkbox-list"
data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'
data-wc-context='<?php echo wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); ?>'
>
<ul class="wc-block-interactivity-components-checkbox-list__list">
<?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['checked'] ) :
if ( $count >= $remaining_initial_unchecked ) :
?>
class="wc-block-interactivity-components-checkbox-list__item hidden"
data-wc-class--hidden="!context.showAll"
<?php else : ?>
<?php ++$count; ?>
<?php endif; ?>
<?php endif; ?>
class="wc-block-interactivity-components-checkbox-list__item"
>
<label
class="wc-block-interactivity-components-checkbox-list__label"
for="<?php echo esc_attr( $item['id'] ); ?>"
>
<span class="wc-block-interactivity-components-checkbox-list__input-wrapper">
<input
id="<?php echo esc_attr( $item['id'] ); ?>"
class="wc-block-interactivity-components-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( $on_change ); ?>"
value="<?php echo esc_attr( $item['value'] ); ?>"
<?php checked( $item['checked'], 1 ); ?>
>
<svg class="wc-block-interactivity-components-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-interactivity-components-checkbox-list__text">
<?php echo wp_kses_post( $item['label'] ); ?>
</span>
</label>
</li>
<?php } ?>
</ul>
<?php if ( count( $items ) > $show_initially ) : ?>
<span
role="button"
class="wc-block-interactivity-components-checkbox-list__show-more"
data-wc-class--hidden="context.showAll"
data-wc-on--click="actions.showAllItems"
>
<small role="presentation"><?php echo esc_html__( 'Show more...', 'woocommerce' ); ?></small>
</span>
<?php endif; ?>
</div>
<?php
return ob_get_clean();

View File

@@ -14,7 +14,6 @@ abstract class AbstractPageTemplate extends AbstractTemplate {
*/
public function init() {
add_filter( 'page_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 );
add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) );
}
/**
@@ -48,7 +47,10 @@ abstract class AbstractPageTemplate extends AbstractTemplate {
}
/**
* Filter the page title when the template is active.
* Forces the page title to match the template title when this template is active.
*
* Only applies when hooked into `pre_get_document_title`. Most templates used for pages will not require this because
* the page title should be used instead.
*
* @param string $title Page title.
* @return string

View File

@@ -20,7 +20,7 @@ class OrderConfirmationTemplate extends AbstractPageTemplate {
*/
public function init() {
add_action( 'wp_before_admin_bar_render', array( $this, 'remove_edit_page_link' ) );
add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) );
parent::init();
}

View File

@@ -55,6 +55,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
* Since that there is a custom logic for the first and last block, we have to inject the hooks manually.
* The first block supports the following hooks:
* woocommerce_before_single_product
* woocommerce_before_single_product_summary
*
* The last block supports the following hooks:
* woocommerce_after_single_product
@@ -69,6 +70,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
'before' => array(
'woocommerce_before_main_content' => $this->hook_data['woocommerce_before_main_content'],
'woocommerce_before_single_product' => $this->hook_data['woocommerce_before_single_product'],
'woocommerce_before_single_product_summary' => $this->hook_data['woocommerce_before_single_product_summary'],
),
'after' => array(),
);
@@ -195,7 +197,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
),
),
'woocommerce_before_single_product_summary' => array(
'block_names' => array( 'core/post-excerpt' ),
'block_names' => array(),
'position' => 'before',
'hooked' => array(
'woocommerce_show_product_sale_flash' => 10,

View File

@@ -509,23 +509,6 @@ class BlockTemplateUtils {
return false;
}
/**
* Checks if we can fall back to the `archive-product` template for a given slug.
*
* `taxonomy-product_cat`, `taxonomy-product_tag`, `taxonomy-product_attribute` templates can
* generally use the `archive-product` as a fallback if there are no specific overrides.
*
* @param string $template_slug Slug to check for fallbacks.
* @return boolean
*/
public static function template_is_eligible_for_product_archive_fallback( $template_slug ) {
$registered_template = self::get_template( $template_slug );
if ( $registered_template && isset( $registered_template->fallback_template ) ) {
return ProductCatalogTemplate::SLUG === $registered_template->fallback_template;
}
return false;
}
/**
* Checks if we can fall back to an `archive-product` template stored on the db for a given slug.
*
@@ -533,20 +516,21 @@ class BlockTemplateUtils {
* @param array $db_templates Templates that have already been found on the db.
* @return boolean
*/
public static function template_is_eligible_for_product_archive_fallback_from_db( $template_slug, $db_templates ) {
$eligible_for_fallback = self::template_is_eligible_for_product_archive_fallback( $template_slug );
if ( ! $eligible_for_fallback ) {
return false;
public static function template_is_eligible_for_fallback_from_db( $template_slug, $db_templates ) {
$registered_template = self::get_template( $template_slug );
if ( $registered_template && isset( $registered_template->fallback_template ) ) {
$array_filter = array_filter(
$db_templates,
function ( $template ) use ( $registered_template ) {
return isset( $registered_template->fallback_template ) && $registered_template->fallback_template === $template->slug;
}
);
return count( $array_filter ) > 0;
}
$array_filter = array_filter(
$db_templates,
function ( $template ) use ( $template_slug ) {
return ProductCatalogTemplate::SLUG === $template->slug;
}
);
return count( $array_filter ) > 0;
return false;
}
/**
@@ -557,14 +541,13 @@ class BlockTemplateUtils {
* @return boolean|object
*/
public static function get_fallback_template_from_db( $template_slug, $db_templates ) {
$eligible_for_fallback = self::template_is_eligible_for_product_archive_fallback( $template_slug );
if ( ! $eligible_for_fallback ) {
return false;
}
$registered_template = self::get_template( $template_slug );
foreach ( $db_templates as $template ) {
if ( ProductCatalogTemplate::SLUG === $template->slug ) {
return $template;
if ( $registered_template && isset( $registered_template->fallback_template ) ) {
foreach ( $db_templates as $template ) {
if ( $registered_template->fallback_template === $template->slug ) {
return $template;
}
}
}
@@ -580,10 +563,12 @@ class BlockTemplateUtils {
* @param string $template_slug Slug to check for fallbacks.
* @return boolean
*/
public static function template_is_eligible_for_product_archive_fallback_from_theme( $template_slug ) {
return self::template_is_eligible_for_product_archive_fallback( $template_slug )
public static function template_is_eligible_for_fallback_from_theme( $template_slug ) {
$registered_template = self::get_template( $template_slug );
return $registered_template && isset( $registered_template->fallback_template )
&& ! self::theme_has_template( $template_slug )
&& self::theme_has_template( ProductCatalogTemplate::SLUG );
&& self::theme_has_template( $registered_template->fallback_template );
}
/**
@@ -609,7 +594,7 @@ class BlockTemplateUtils {
$query_result_template->slug === $template->slug
&& $query_result_template->theme === $template->theme
) {
if ( self::template_is_eligible_for_product_archive_fallback_from_theme( $template->slug ) ) {
if ( self::template_is_eligible_for_fallback_from_theme( $template->slug ) ) {
$query_result_template->has_theme_file = true;
}